diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 108044c..d7f698c 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -8,6 +8,7 @@ on: - main - nwm-main - development + - development-pw - release-candidate push: branches: @@ -16,6 +17,7 @@ on: - main - nwm-main - development + - development-pw - release-candidate workflow_dispatch: inputs: @@ -68,6 +70,7 @@ jobs: alias_tag: ${{ steps.vars.outputs.alias_tag }} clean_ref: ${{ steps.vars.outputs.clean_ref }} default_ref: ${{ steps.vars.outputs.default_ref }} + default_base_tag: ${{ steps.vars.outputs.default_base_tag }} ngen_image_digest: ${{ steps.vars.outputs.ngen_image_digest }} ngen_image_revision: ${{ steps.vars.outputs.ngen_image_revision }} ewts_revision: ${{ steps.vars.outputs.ewts_revision }} @@ -104,10 +107,11 @@ jobs: SHORT_SHA="${REAL_SHA:0:7}" # default source-repo ref: follow the release-line branch we're building from - # (development / ngwpc-candidate / ngwpc-release); tags and other branches fall - # back to development. Override per source repo via the *_REF dispatch inputs. + # (development / development-pw / ngwpc-candidate / ngwpc-release); tags and + # other branches fall back to development. Override per source repo via the + # *_REF dispatch inputs. case "${GITHUB_REF_TYPE}:${GITHUB_REF_NAME}" in - branch:development|branch:ngwpc-candidate|branch:ngwpc-release) + branch:development|branch:development-pw|branch:ngwpc-candidate|branch:ngwpc-release) DEFAULT_REF="$GITHUB_REF_NAME" ;; *) DEFAULT_REF="development" ;; @@ -115,6 +119,16 @@ jobs: # use an explicit *_REF input if provided, else DEFAULT_REF ref_or_default() { [ -n "$1" ] && echo "$1" || echo "$DEFAULT_REF"; } + # base-image alias follows the lane: development-pw (or a PR into it) pulls + # the rocky 'latest-pw' alias, every other ref the bookworm 'latest'. An + # explicit NGEN_IMAGE_TAG input always wins over this default. + LANE_REF="${GITHUB_BASE_REF:-$GITHUB_REF_NAME}" + if [ "$LANE_REF" = "development-pw" ]; then + DEFAULT_BASE_TAG="latest-pw" + else + DEFAULT_BASE_TAG="latest" + fi + # logic for the tags: # test_image_tag (commit short sha): used for the initial build and test # alias_tag: used for final tagging on successful tests @@ -140,7 +154,8 @@ jobs: fi # base image (ngen) metadata for Dockerfile labels - NGEN_IMAGE_TAG="${{ inputs.NGEN_IMAGE_TAG || 'latest' }}" + NGEN_IMAGE_TAG="${{ inputs.NGEN_IMAGE_TAG }}" + NGEN_IMAGE_TAG="${NGEN_IMAGE_TAG:-$DEFAULT_BASE_TAG}" NGEN_IMAGE="ghcr.io/${ORG}/ngen:${NGEN_IMAGE_TAG}" NGEN_INSPECT=$(skopeo inspect --override-os linux --override-arch amd64 "docker://${NGEN_IMAGE}" 2>/dev/null || echo '{}') NGEN_IMAGE_DIGEST=$(echo "$NGEN_INSPECT" | jq -r '.Digest // "unknown"') @@ -158,6 +173,9 @@ jobs: echo "${sha:-unknown}" } + # Each *_REVISION is the dependency's resolved commit SHA: it labels the + # image AND doubles as the per-dependency cache-bust build-arg, so a new + # commit on the requested ref reinstalls that dep instead of a stale cache. EWTS_REVISION=$(resolve_sha "https://github.com/${{ inputs.EWTS_ORG || github.repository_owner }}/nwm-ewts.git" "$(ref_or_default "${{ inputs.EWTS_REF }}")") MSW_MGR_REVISION=$(resolve_sha "https://github.com/${{ inputs.MSW_MGR_ORG || github.repository_owner }}/nwm-msw-mgr.git" "$(ref_or_default "${{ inputs.MSW_MGR_REF }}")") @@ -171,6 +189,7 @@ jobs: commit_sha_short=${SHORT_SHA} clean_ref=${CLEAN_REF} default_ref=${DEFAULT_REF} + default_base_tag=${DEFAULT_BASE_TAG} ngen_image_digest=${NGEN_IMAGE_DIGEST} ngen_image_revision=${NGEN_IMAGE_REVISION} ewts_revision=${EWTS_REVISION} @@ -233,9 +252,9 @@ jobs: tags: ${{ needs.setup.outputs.image_base }}:${{ needs.setup.outputs.test_image_tag }} build-args: | GHCR_ORG=${{ inputs.GHCR_ORG || needs.setup.outputs.org }} - NGEN_IMAGE_TAG=${{ inputs.NGEN_IMAGE_TAG || 'latest' }} - BASE_IMAGE_DIGEST=${{ needs.setup.outputs.ngen_image_digest }} - BASE_IMAGE_REVISION=${{ needs.setup.outputs.ngen_image_revision }} + NGEN_IMAGE=ghcr.io/${{ needs.setup.outputs.org }}/ngen:${{ inputs.NGEN_IMAGE_TAG || needs.setup.outputs.default_base_tag }} + NGEN_IMAGE_DIGEST=${{ needs.setup.outputs.ngen_image_digest }} + NGEN_IMAGE_REVISION=${{ needs.setup.outputs.ngen_image_revision }} EWTS_ORG=${{ inputs.EWTS_ORG || github.repository_owner }} EWTS_REF=${{ inputs.EWTS_REF || needs.setup.outputs.default_ref }} EWTS_REVISION=${{ needs.setup.outputs.ewts_revision }} @@ -338,23 +357,33 @@ jobs: --all \ "docker://${IMAGE_BASE}:${TEST_TAG}" "docker://${IMAGE_BASE}:${ALIAS_TAG}" - # tag with 'latest' on development branch + # Branch-keyed promotion: development promotes 'latest' (bookworm/AWS + # lane); development-pw promotes 'latest-pw' (rocky/PW lane). Do not + # manually dispatch a cross-lane build from either branch: promotion + # keys off the branch name, so the tested image would promote under + # the wrong alias. if [ "$GITHUB_REF_NAME" = "development" ]; then skopeo copy \ --src-creds "${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}" \ --dest-creds "${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}" \ --all \ "docker://${IMAGE_BASE}:${TEST_TAG}" "docker://${IMAGE_BASE}:latest" + elif [ "$GITHUB_REF_NAME" = "development-pw" ]; then + skopeo copy \ + --src-creds "${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}" \ + --dest-creds "${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}" \ + --all \ + "docker://${IMAGE_BASE}:${TEST_TAG}" "docker://${IMAGE_BASE}:latest-pw" fi # Build the Apptainer .sif from the tested + promoted image and publish it to # GHCR as an OCI artifact (oras://). AWS PCS compute nodes pull this onto the # shared EFS and run it with `apptainer run`. SIF images are NOT baked into the - # compute AMI — the AMI carries only the Apptainer runtime; the images ship here + # compute AMI. The AMI carries only the Apptainer runtime; the images ship here # and are staged on EFS. See nwm-ngencerf-infra-learning/docs/design-decisions.md. build-sif: name: build-sif - # Only on real branch builds (push) or manual dispatch — not PRs (a multi-GB + # Only on real branch builds (push) or manual dispatch, not PRs (a multi-GB # SIF build on every PR is wasteful). Runs after the image is tested + promoted. if: | (github.event_name == 'push') || diff --git a/Dockerfile b/Dockerfile index 5b0637e..9bae35d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,8 @@ ############################################################################ # Change/Verify these values when adopting this Dockerfile into another org: # GH_ORG, GHCR_ORG, IMAGE_NAMESPACE, APP_DIR, -# MSW_MGR_ORG, MSW_MGR_REF, EWTS_ORG, EWTS_REF +# MSW_MGR_ORG, MSW_MGR_REF, EWTS_ORG, EWTS_REF, +# USE_EWTS, EWTS_CACHE_BUST ############################################################################ # Ownership / branding overrides @@ -62,25 +63,29 @@ ARG APP_DIR # OCI Metadata Arguments # -# BASE_IMAGE_* refers to the ngen image this image is built FROM. -ARG BASE_IMAGE_DIGEST="unknown" -ARG BASE_IMAGE_REVISION="unknown" +# NGEN_IMAGE_* refers to the ngen image this image is built FROM. +ARG NGEN_IMAGE_DIGEST="unknown" +ARG NGEN_IMAGE_REVISION="unknown" ARG IMAGE_SOURCE="unknown" ARG IMAGE_VENDOR="unknown" ARG IMAGE_VERSION="unknown" ARG IMAGE_REVISION="unknown" +ARG EWTS_REVISION="unknown" ARG MSW_MGR_REVISION="unknown" # Image Labels: OCI-spec annotations followed by custom source-repo metadata. LABEL org.opencontainers.image.base.name="${NGEN_IMAGE}" \ - org.opencontainers.image.base.digest="${BASE_IMAGE_DIGEST}" \ + org.opencontainers.image.base.digest="${NGEN_IMAGE_DIGEST}" \ org.opencontainers.image.source="${IMAGE_SOURCE}" \ org.opencontainers.image.vendor="${IMAGE_VENDOR}" \ org.opencontainers.image.version="${IMAGE_VERSION}" \ org.opencontainers.image.revision="${IMAGE_REVISION}" \ org.opencontainers.image.title="NGEN Forecast/Hindcast Manager" \ org.opencontainers.image.description="Docker image for the NGEN Forecast/Hindcast application" \ - io.${IMAGE_NAMESPACE}.image.base.revision="${BASE_IMAGE_REVISION}" \ + io.${IMAGE_NAMESPACE}.image.base.revision="${NGEN_IMAGE_REVISION}" \ + io.${IMAGE_NAMESPACE}.ewts.org="${EWTS_ORG}" \ + io.${IMAGE_NAMESPACE}.ewts.ref="${EWTS_REF}" \ + io.${IMAGE_NAMESPACE}.ewts.revision="${EWTS_REVISION}" \ io.${IMAGE_NAMESPACE}.msw.mgr.org="${MSW_MGR_ORG}" \ io.${IMAGE_NAMESPACE}.msw.mgr.ref="${MSW_MGR_REF}" \ io.${IMAGE_NAMESPACE}.msw.mgr.revision="${MSW_MGR_REVISION}"