diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml new file mode 100644 index 0000000..36297b9 --- /dev/null +++ b/.github/workflows/publish_release.yml @@ -0,0 +1,89 @@ +# This workflow will build and publish Docker image to ghcr.io + +# This workflow runs when changes are detected in the `main` branch, which +# include an update to the `docker/service_version.txt` file. The workflow can +# also be manually triggered by a repository maintainer. + +# IF all pre-requisite tests pass, this workflow will build the docker images, +# push them to ghcr.io and publish a GitHub release. +name: Publish SMAP L2 Gridding Service + +on: + push: + branches: [ main ] + paths: docker/service_version.txt + workflow_dispatch: + +env: + IMAGE_NAME: ${{ github.repository }} + REGISTRY: ghcr.io + +jobs: + run_service_tests: + uses: ./.github/workflows/run_service_tests.yml + + run_lib_tests: + uses: ./.github/workflows/run_lib_tests.yml + + mypy: + uses: ./.github/workflows/mypy.yml + + build_and_publish: + needs: [run_service_tests, run_lib_tests, mypy] + runs-on: ubuntu-latest + environment: release + permissions: + # write permission is required to create a GitHub release + contents: write + id-token: write + packages: write + strategy: + fail-fast: false + + steps: + - name: Checkout smap-l2-gridder repository + uses: actions/checkout@v4 + with: + lfs: true + + - name: Extract semantic version number + run: echo "semantic_version=$(cat docker/service_version.txt)" >> $GITHUB_ENV + + - name: Extract release version notes + run: | + version_release_notes=$(./bin/extract-release-notes.sh) + echo "RELEASE_NOTES<> $GITHUB_ENV + echo "${version_release_notes}" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + - name: Log-in to ghcr.io registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Add tags to the Docker image + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}},value=${{ env.semantic_version }} + + - name: Push Docker image + uses: docker/build-push-action@v3 + with: + context: . + file: docker/service.Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Publish GitHub release + uses: ncipollo/release-action@v1 + with: + body: ${{ env.RELEASE_NOTES }} + commit: main + name: Version ${{ env.semantic_version }} + tag: ${{ env.semantic_version }} diff --git a/.github/workflows/run_lib_tests.yml b/.github/workflows/run_lib_tests.yml new file mode 100644 index 0000000..6d44d51 --- /dev/null +++ b/.github/workflows/run_lib_tests.yml @@ -0,0 +1,34 @@ +# This workflow will run the appropriate library tests across a python matrix of versions. +name: Run Python library tests + +on: + workflow_call + +jobs: + build_and_test_lib: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.10', '3.11', '3.12'] + + steps: + - name: Checkout smap-l2-gridder repository + uses: actions/checkout@v4 + with: + lfs: true + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + pip install -r pip_requirements.txt -r tests/pip_test_requirements.txt + + - name: Run science tests while excluding the service tests. + run: | + pytest tests --ignore tests/test_service diff --git a/.github/workflows/run_service_tests.yml b/.github/workflows/run_service_tests.yml new file mode 100644 index 0000000..543065c --- /dev/null +++ b/.github/workflows/run_service_tests.yml @@ -0,0 +1,37 @@ +# This workflow will build the service and test Docker images for smap-l2-gridder, +# then run the `pytest` suite within a test Docker container, reporting +# test results and code coverage as artefacts. It will be called by the +# workflow that run tests against new PRs and as a first step in the workflow +# that publishes new Docker images. + +name: Run Python Service Tests + +on: + workflow_call + +jobs: + build_and_test_service: + runs-on: ubuntu-latest + strategy: + fail-fast: false + + steps: + - name: Checkout smap-l2-gridder repository + uses: actions/checkout@v4 + with: + lfs: true + + - name: Build service image + run: ./bin/build-image + + - name: Build test image + run: ./bin/build-test + + - name: Run test image + run: ./bin/run-test + + - name: Archive test results and coverage + uses: actions/upload-artifact@v4 + with: + name: reports + path: reports/**/* diff --git a/.github/workflows/run_tests_on_pull_requests.yml b/.github/workflows/run_tests_on_pull_requests.yml index e57c98a..1c71b9c 100644 --- a/.github/workflows/run_tests_on_pull_requests.yml +++ b/.github/workflows/run_tests_on_pull_requests.yml @@ -11,5 +11,11 @@ on: workflow_dispatch: jobs: + build_and_test_service: + uses: ./.github/workflows/run_service_tests.yml + + run_lib_tests: + uses: ./.github/workflows/run_lib_tests.yml + mypy: uses: ./.github/workflows/mypy.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index b9ba90f..96dd8da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [v0.0.1] - 2024-11-27 ### Added - Initial codebase that transforms SPL2SMP_E granules into NetCDF4-CF grids. [#1](https://github.com/nasa/harmony-SMAP-L2-gridding-service/pull/1) - Code and configuration to wrap gridding logic into a Harmony Service [#3](https://github.com/nasa/harmony-SMAP-L2-gridding-service/pull/3 ) +- GitHub actions CI configuration [#4](https://github.com/nasa/harmony-SMAP-L2-gridding-service/pull/4 ) + + +[v0.0.1]: https://github.com/nasa/harmony-SMAP-L2-gridding-service/releases/tag/0.0.1 diff --git a/README.md b/README.md index faa89c6..41db0bb 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ pip install pre-commit pre-commit install ``` -## Versioning: +## Versioning Docker service images for the `smap_l2_gridder` adhere to [semantic version](https://semver.org/) numbers: major.minor.patch. @@ -113,13 +113,54 @@ version](https://semver.org/) numbers: major.minor.patch. * Minor increments: These are backwards compatible API changes. * Patch increments: These updates do not affect the API to the service. -## CI/CD: +## CI/CD The CI/CD for SMAP-L2-Gridding-Service is run on github actions with the workflows in the `.github/workflows` directory: -* [TODO: complete this section when the above statement is true] +* `run_lib_tests.yml` - A reusable workflow that tests the library functions + against the supported python versions. +* `run_service_tests.yml` - A reusable workflow that builds the service and + test Docker images, then runs the Python unit test suite in an instance of + the test Docker container. +* `run_tests_on_pull_requests.yml` - Triggered for all PRs against the `main` + branch. It runs the workflow in `run_service_tests.yml` and + `run_lib_tests.yml` to ensure all tests pass for the new code. +* `publish_docker_image.yml` - Triggered either manually or for commits to the + `main` branch that contain changes to the `docker/service_version.txt` file. +* `publish_release.yml` - workflow runs + automatically when there is a change to the `docker/service_version.txt` + file on the main branch. This workflow will: + * Run the full unit test suite, to prevent publication of broken code. + * Extract the semantic version number from `docker/service_version.txt`. + * Extract the released notes for the most recent version from `CHANGELOG.md`. + * Build and deploy a this service's docker image to `ghcr.io`. + * Publish a GitHub release under the semantic version number, with associated + git tag. + ## Releasing -* [TODO: complete when implemented] +A release consists of a new Docker image for the harmony-SMAP-L2-gridding-service +published to github's container repository. + +A release is made automatically when a commit to the main branch contains a +changes in the `docker/service_version.txt` file, see the [publish_release](#release-workflow) workflow in the CI/CD section above. + +Before **merging** a PR that will trigger a release, ensure these two files are updated: + +* `CHANGELOG.md` - Notes should be added to capture the changes to the service and a link to the current pull request should be included. +* `docker/service_version.txt` - The semantic version number should be updated to trigger the release. + +The `CHANGELOG.md` file requires a specific format for a new release, as it +looks for the following string to define the newest release of the code +(starting at the top of the file). + +``` +## [vX.Y.Z] - YYYY-MM-DD +``` + +Where the markdown reference needs to be updated at the bottom of the file following the existing pattern. +``` +[vX.Y.Z]: https://github.com/nasa/harmony-SMAP-L2-gridding-service/releases/tag/X.Y.Z +``` diff --git a/bin/extract-release-notes.sh b/bin/extract-release-notes.sh new file mode 100755 index 0000000..7d32cbd --- /dev/null +++ b/bin/extract-release-notes.sh @@ -0,0 +1,26 @@ +#!/bin/bash +############################################################################### +# +# A bash script to extract only the notes related to the most recent version of +# SMAP L2 Gridding Service from CHANGELOG.md +# +############################################################################### + +CHANGELOG_FILE="CHANGELOG.md" + +## captures versions +## >## v1.0.0 +## >## [v1.0.0] +VERSION_PATTERN="^## [\[]v" + +## captures url links +## [unreleased]:https://github.com/nasa/harmony-browse-image-generator/compare/1.2.0..HEAD +## [v1.2.0]: https://github.com/nasa/harmony-browse-image-generator/compare/1.1.0..1.2.0 +LINK_PATTERN="^\[.*\].*\.\..*" + +# Read the file and extract text between the first two occurrences of the +# VERSION_PATTERN +result=$(awk "/$VERSION_PATTERN/{c++; if(c==2) exit;} c==1" "$CHANGELOG_FILE") + +# Print the result +echo "$result" | grep -v "$VERSION_PATTERN" | grep -v "$LINK_PATTERN" diff --git a/docker/service_version.txt b/docker/service_version.txt index 77d6f4c..8acdd82 100644 --- a/docker/service_version.txt +++ b/docker/service_version.txt @@ -1 +1 @@ -0.0.0 +0.0.1 diff --git a/smap_l2_gridder/crs.py b/smap_l2_gridder/crs.py index b9de5c4..2681824 100644 --- a/smap_l2_gridder/crs.py +++ b/smap_l2_gridder/crs.py @@ -38,84 +38,13 @@ def col_row_to_xy(self, col: int, row: int) -> tuple[np.float64, np.float64]: return x, y -# The authoritative value of well known text strings is from epsg.org - -# The pyproj CRS created from this WKT string is the same as a CRS that has been -# round tripped through the CRS creation process. But the output value on the -# files CRS metadata may not match the authoritative value because of the -# different varieties of WKT. That said, the CRS created by pyproj is the same. -# i.e. -# pyproj.crs.CRS.from_wkt(EPSG_6933_WKT).to_wkt() != EPSG_6933_WKT -# but -# pyproj.crs.CRS.from_wkt(pyproj.crs.CRS.from_wkt(EPSG_6933_WKT).to_wkt()) -# == pyproj.crs.CRS.from_wkt(EPSG_6933_WKT) - # NSIDC EASE-Grid 2.0 Global CRS definition # from: https://epsg.org/crs/wkt/id/6933 -EPSG_6933_WKT = ( - 'PROJCRS["WGS 84 / NSIDC EASE-Grid 2.0 Global",' - 'BASEGEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble", ' - 'MEMBER["World Geodetic System 1984 (Transit)", ID["EPSG",1166]], ' - 'MEMBER["World Geodetic System 1984 (G730)", ID["EPSG",1152]], ' - 'MEMBER["World Geodetic System 1984 (G873)", ID["EPSG",1153]], ' - 'MEMBER["World Geodetic System 1984 (G1150)", ID["EPSG",1154]], ' - 'MEMBER["World Geodetic System 1984 (G1674)", ID["EPSG",1155]], ' - 'MEMBER["World Geodetic System 1984 (G1762)", ID["EPSG",1156]], ' - 'MEMBER["World Geodetic System 1984 (G2139)", ID["EPSG",1309]], ' - 'MEMBER["World Geodetic System 1984 (G2296)", ID["EPSG",1383]], ' - 'ELLIPSOID["WGS 84",6378137,298.257223563,' - 'LENGTHUNIT["metre",1,ID["EPSG",9001]],ID["EPSG",7030]], ' - 'ENSEMBLEACCURACY[2],ID["EPSG",6326]],ID["EPSG",4326]],' - 'CONVERSION["US NSIDC EASE-Grid 2.0 Global",' - 'METHOD["Lambert Cylindrical Equal Area",ID["EPSG",9835]],' - 'PARAMETER["Latitude of 1st standard parallel",30,' - 'ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9102]],' - 'ID["EPSG",8823]],PARAMETER["Longitude of natural origin",0,' - 'ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9102]],' - 'ID["EPSG",8802]],PARAMETER["False easting",0,' - 'LENGTHUNIT["metre",1,ID["EPSG",9001]],ID["EPSG",8806]],' - 'PARAMETER["False northing",0,LENGTHUNIT["metre",1,ID["EPSG",9001]],' - 'ID["EPSG",8807]],ID["EPSG",6928]],CS[Cartesian,2,ID["EPSG",4499]],' - 'AXIS["Easting (X)",east],AXIS["Northing (Y)",north],' - 'LENGTHUNIT["metre",1,ID["EPSG",9001]],ID["EPSG",6933]]' -) +EPSG_6933_WKT = CRS.from_epsg(6933).to_wkt() # NSIDC EASE-Grid 2.0 North CRS definition # from: https://epsg.org/crs/wkt/id/6931 -EPSG_6931_WKT = ( - 'PROJCRS["WGS 84 / NSIDC EASE-Grid 2.0 North",' - 'BASEGEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble", ' - 'MEMBER["World Geodetic System 1984 (Transit)", ID["EPSG",1166]], ' - 'MEMBER["World Geodetic System 1984 (G730)", ID["EPSG",1152]], ' - 'MEMBER["World Geodetic System 1984 (G873)", ID["EPSG",1153]], ' - 'MEMBER["World Geodetic System 1984 (G1150)", ID["EPSG",1154]], ' - 'MEMBER["World Geodetic System 1984 (G1674)", ID["EPSG",1155]], ' - 'MEMBER["World Geodetic System 1984 (G1762)", ID["EPSG",1156]], ' - 'MEMBER["World Geodetic System 1984 (G2139)", ID["EPSG",1309]], ' - 'MEMBER["World Geodetic System 1984 (G2296)", ID["EPSG",1383]], ' - 'ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1,' - 'ID["EPSG",9001]],ID["EPSG",7030]], ENSEMBLEACCURACY[2],' - 'ID["EPSG",6326]],ID["EPSG",4326]],' - 'CONVERSION["US NSIDC EASE-Grid 2.0 North",' - 'METHOD["Lambert Azimuthal Equal Area",' - 'ID["EPSG",9820]],PARAMETER["Latitude of natural origin",90,' - 'ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9102]],ID["EPSG",8801]],' - 'PARAMETER["Longitude of natural origin",0,' - 'ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9102]],ID["EPSG",8802]],' - 'PARAMETER["False easting",0,LENGTHUNIT["metre",1,ID["EPSG",9001]],' - 'ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1,' - 'ID["EPSG",9001]],ID["EPSG",8807]],ID["EPSG",6929]],CS[Cartesian,2,' - 'ID["EPSG",4469]],AXIS["Easting (X)",South,MERIDIAN[90.0,' - 'ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9102]]]],' - 'AXIS["Northing (Y)",South,MERIDIAN[180.0,' - 'ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9102]]]],' - 'LENGTHUNIT["metre",1,ID["EPSG",9001]],ID["EPSG",6931]]' -) - -GPD_TO_WKT = { - 'EASE2_N09km.gpd': EPSG_6931_WKT, - 'EASE2_M09km.gpd': EPSG_6933_WKT, -} +EPSG_6931_WKT = CRS.from_epsg(6931).to_wkt() def geotransform_from_target_info(target_info: dict) -> Geotransform: