88 - main
99 - nwm-main
1010 - development
11+ - development-pw
1112 - release-candidate
1213 push :
1314 branches :
1617 - main
1718 - nwm-main
1819 - development
20+ - development-pw
1921 - release-candidate
2022 workflow_dispatch :
2123 inputs :
6769 test_image_tag : ${{ steps.vars.outputs.test_image_tag }}
6870 alias_tag : ${{ steps.vars.outputs.alias_tag }}
6971 clean_ref : ${{ steps.vars.outputs.clean_ref }}
72+ default_ref : ${{ steps.vars.outputs.default_ref }}
73+ default_base_tag : ${{ steps.vars.outputs.default_base_tag }}
7074 ngen_image_digest : ${{ steps.vars.outputs.ngen_image_digest }}
7175 ngen_image_revision : ${{ steps.vars.outputs.ngen_image_revision }}
76+ ewts_revision : ${{ steps.vars.outputs.ewts_revision }}
77+ msw_mgr_revision : ${{ steps.vars.outputs.msw_mgr_revision }}
7278 steps :
7379 - name : Compute image vars
7480 id : vars
@@ -100,6 +106,29 @@ jobs:
100106 CLEAN_REF=$(echo "$REAL_REF" | tr '[:upper:]' '[:lower:]' | sed 's/\//-/g')
101107 SHORT_SHA="${REAL_SHA:0:7}"
102108
109+ # default source-repo ref: follow the release-line branch we're building from
110+ # (development / development-pw / ngwpc-candidate / ngwpc-release); tags and
111+ # other branches fall back to development. Override per source repo via the
112+ # *_REF dispatch inputs.
113+ case "${GITHUB_REF_TYPE}:${GITHUB_REF_NAME}" in
114+ branch:development|branch:development-pw|branch:ngwpc-candidate|branch:ngwpc-release)
115+ DEFAULT_REF="$GITHUB_REF_NAME" ;;
116+ *)
117+ DEFAULT_REF="development" ;;
118+ esac
119+ # use an explicit *_REF input if provided, else DEFAULT_REF
120+ ref_or_default() { [ -n "$1" ] && echo "$1" || echo "$DEFAULT_REF"; }
121+
122+ # base-image alias follows the lane: development-pw (or a PR into it) pulls
123+ # the rocky 'latest-pw' alias, every other ref the bookworm 'latest'. An
124+ # explicit NGEN_IMAGE_TAG input always wins over this default.
125+ LANE_REF="${GITHUB_BASE_REF:-$GITHUB_REF_NAME}"
126+ if [ "$LANE_REF" = "development-pw" ]; then
127+ DEFAULT_BASE_TAG="latest-pw"
128+ else
129+ DEFAULT_BASE_TAG="latest"
130+ fi
131+
103132 # logic for the tags:
104133 # test_image_tag (commit short sha): used for the initial build and test
105134 # alias_tag: used for final tagging on successful tests
@@ -125,22 +154,47 @@ jobs:
125154 fi
126155
127156 # base image (ngen) metadata for Dockerfile labels
128- NGEN_IMAGE_TAG="${{ inputs.NGEN_IMAGE_TAG || 'latest' }}"
157+ NGEN_IMAGE_TAG="${{ inputs.NGEN_IMAGE_TAG }}"
158+ NGEN_IMAGE_TAG="${NGEN_IMAGE_TAG:-$DEFAULT_BASE_TAG}"
129159 NGEN_IMAGE="ghcr.io/${ORG}/ngen:${NGEN_IMAGE_TAG}"
130- NGEN_INSPECT=$(skopeo inspect "docker://${NGEN_IMAGE}" 2>/dev/null || echo '{}')
160+ NGEN_INSPECT=$(skopeo inspect --override-os linux --override-arch amd64 "docker://${NGEN_IMAGE}" 2>/dev/null || echo '{}')
131161 NGEN_IMAGE_DIGEST=$(echo "$NGEN_INSPECT" | jq -r '.Digest // "unknown"')
132162 NGEN_IMAGE_REVISION=$(echo "$NGEN_INSPECT" | jq -r '.Labels["org.opencontainers.image.revision"] // "unknown"')
133163
164+ # resolve each source repo's ref (branch/tag/SHA) to its commit SHA for revision labels
165+ resolve_sha() {
166+ local url="$1" ref="$2" out sha
167+ # a full 40-char SHA can't be looked up via ls-remote; use it directly
168+ if [[ "$ref" =~ ^[0-9a-f]{40}$ ]]; then echo "$ref"; return; fi
169+ out=$(git ls-remote "$url" "$ref" "refs/tags/${ref}^{}" 2>/dev/null)
170+ # prefer the dereferenced commit (^{}) for annotated tags; else first match
171+ sha=$(echo "$out" | grep '\^{}$' | head -n1 | cut -f1)
172+ [ -z "$sha" ] && sha=$(echo "$out" | head -n1 | cut -f1)
173+ echo "${sha:-unknown}"
174+ }
175+
176+ # Each *_REVISION is the dependency's resolved commit SHA: it labels the
177+ # image AND doubles as the per-dependency cache-bust build-arg, so a new
178+ # commit on the requested ref reinstalls that dep instead of a stale cache.
179+ EWTS_REVISION=$(resolve_sha "https://github.com/${{ inputs.EWTS_ORG || github.repository_owner }}/nwm-ewts.git" "$(ref_or_default "${{ inputs.EWTS_REF }}")")
180+ 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 }}")")
181+
134182 # save outputs
135- echo "org=${ORG}" >> "$GITHUB_OUTPUT"
136- echo "image_base=${IMAGE_BASE}" >> "$GITHUB_OUTPUT"
137- echo "test_image_tag=${TEST_TAG}" >> "$GITHUB_OUTPUT"
138- echo "alias_tag=${ALIAS}" >> "$GITHUB_OUTPUT"
139- echo "commit_sha=${REAL_SHA}" >> "$GITHUB_OUTPUT"
140- echo "commit_sha_short=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
141- echo "clean_ref=${CLEAN_REF}" >> "$GITHUB_OUTPUT"
142- echo "ngen_image_digest=${NGEN_IMAGE_DIGEST}" >> "$GITHUB_OUTPUT"
143- echo "ngen_image_revision=${NGEN_IMAGE_REVISION}" >> "$GITHUB_OUTPUT"
183+ cat >> "$GITHUB_OUTPUT" <<EOF
184+ org=${ORG}
185+ image_base=${IMAGE_BASE}
186+ test_image_tag=${TEST_TAG}
187+ alias_tag=${ALIAS}
188+ commit_sha=${REAL_SHA}
189+ commit_sha_short=${SHORT_SHA}
190+ clean_ref=${CLEAN_REF}
191+ default_ref=${DEFAULT_REF}
192+ default_base_tag=${DEFAULT_BASE_TAG}
193+ ngen_image_digest=${NGEN_IMAGE_DIGEST}
194+ ngen_image_revision=${NGEN_IMAGE_REVISION}
195+ ewts_revision=${EWTS_REVISION}
196+ msw_mgr_revision=${MSW_MGR_REVISION}
197+ EOF
144198
145199 # CodeQL scan
146200 codeql-scan :
@@ -198,13 +252,17 @@ jobs:
198252 tags : ${{ needs.setup.outputs.image_base }}:${{ needs.setup.outputs.test_image_tag }}
199253 build-args : |
200254 GHCR_ORG=${{ inputs.GHCR_ORG || needs.setup.outputs.org }}
201- NGEN_IMAGE_TAG= ${{ inputs.NGEN_IMAGE_TAG || 'latest' }}
202- BASE_IMAGE_DIGEST =${{ needs.setup.outputs.ngen_image_digest }}
203- BASE_IMAGE_REVISION =${{ needs.setup.outputs.ngen_image_revision }}
255+ NGEN_IMAGE=ghcr.io/ ${{ needs.setup.outputs.org }}/ngen:${{ inputs.NGEN_IMAGE_TAG || needs.setup.outputs.default_base_tag }}
256+ NGEN_IMAGE_DIGEST =${{ needs.setup.outputs.ngen_image_digest }}
257+ NGEN_IMAGE_REVISION =${{ needs.setup.outputs.ngen_image_revision }}
204258 EWTS_ORG=${{ inputs.EWTS_ORG || github.repository_owner }}
205- EWTS_REF=${{ inputs.EWTS_REF || 'development' }}
259+ EWTS_REF=${{ inputs.EWTS_REF || needs.setup.outputs.default_ref }}
260+ EWTS_REVISION=${{ needs.setup.outputs.ewts_revision }}
261+ EWTS_CACHE_BUST=${{ needs.setup.outputs.ewts_revision }}
206262 MSW_MGR_ORG=${{ inputs.MSW_MGR_ORG || github.repository_owner }}
207- MSW_MGR_REF=${{ inputs.MSW_MGR_REF || 'development' }}
263+ MSW_MGR_REF=${{ inputs.MSW_MGR_REF || needs.setup.outputs.default_ref }}
264+ MSW_MGR_REVISION=${{ needs.setup.outputs.msw_mgr_revision }}
265+ MSW_MGR_CACHE_BUST=${{ needs.setup.outputs.msw_mgr_revision }}
208266 IMAGE_SOURCE=https://github.com/${{ github.repository }}
209267 IMAGE_VENDOR=${{ github.repository_owner }}
210268 IMAGE_VERSION=${{ needs.setup.outputs.clean_ref }}
@@ -299,11 +357,93 @@ jobs:
299357 --all \
300358 "docker://${IMAGE_BASE}:${TEST_TAG}" "docker://${IMAGE_BASE}:${ALIAS_TAG}"
301359
302- # tag with 'latest' on development branch
360+ # Branch-keyed promotion: development promotes 'latest' (bookworm/AWS
361+ # lane); development-pw promotes 'latest-pw' (rocky/PW lane). Do not
362+ # manually dispatch a cross-lane build from either branch: promotion
363+ # keys off the branch name, so the tested image would promote under
364+ # the wrong alias.
303365 if [ "$GITHUB_REF_NAME" = "development" ]; then
304366 skopeo copy \
305367 --src-creds "${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}" \
306368 --dest-creds "${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}" \
307369 --all \
308370 "docker://${IMAGE_BASE}:${TEST_TAG}" "docker://${IMAGE_BASE}:latest"
371+ elif [ "$GITHUB_REF_NAME" = "development-pw" ]; then
372+ skopeo copy \
373+ --src-creds "${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}" \
374+ --dest-creds "${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}" \
375+ --all \
376+ "docker://${IMAGE_BASE}:${TEST_TAG}" "docker://${IMAGE_BASE}:latest-pw"
377+ fi
378+
379+ # Build the Apptainer .sif from the tested + promoted image and publish it to
380+ # GHCR as an OCI artifact (oras://). AWS PCS compute nodes pull this onto the
381+ # shared EFS and run it with `apptainer run`. SIF images are NOT baked into the
382+ # compute AMI. The AMI carries only the Apptainer runtime; the images ship here
383+ # and are staged on EFS.
384+ build-sif :
385+ name : build-sif
386+ # Only on real branch builds (push) or manual dispatch, not PRs (a multi-GB
387+ # SIF build on every PR is wasteful). Runs after the image is tested + promoted.
388+ if : |
389+ (github.event_name == 'push') ||
390+ (github.event_name == 'workflow_dispatch')
391+ runs-on : ubuntu-latest
392+ needs :
393+ - setup
394+ - promote-tags
395+ env :
396+ # Keep Apptainer's build temp + layer cache off the small root volume; /mnt
397+ # is the roomy ephemeral disk on GitHub-hosted runners (a SIF needs room for
398+ # the uncompressed image during mksquashfs).
399+ APPTAINER_TMPDIR : /mnt/apptainer-tmp
400+ APPTAINER_CACHEDIR : /mnt/apptainer-cache
401+ steps :
402+ - name : Install Apptainer
403+ uses : eWaterCycle/setup-apptainer@v2
404+ with :
405+ apptainer-version : 1.3.6
406+
407+ - name : Log in to GHCR
408+ uses : docker/login-action@v4
409+ with :
410+ registry : ${{ env.REGISTRY }}
411+ username : ${{ github.actor }}
412+ password : ${{ secrets.GITHUB_TOKEN }}
413+
414+ - name : Build .sif from the promoted image and push to GHCR (oras)
415+ shell : bash
416+ run : |
417+ set -euo pipefail
418+ sudo mkdir -p "$APPTAINER_TMPDIR" "$APPTAINER_CACHEDIR"
419+ sudo chown -R "$USER":"$USER" "$APPTAINER_TMPDIR" "$APPTAINER_CACHEDIR"
420+
421+ IMAGE_BASE="${{ needs.setup.outputs.image_base }}" # ghcr.io/<org>/nwm-fcst-mgr
422+ ALIAS_TAG="${{ needs.setup.outputs.alias_tag }}" # the tested + promoted tag
423+ SIF_REPO="${IMAGE_BASE}-sif" # ghcr.io/<org>/nwm-fcst-mgr-sif
424+
425+ # Convert the promoted OCI image to a SIF. docker/login-action wrote
426+ # ~/.docker/config.json, which Apptainer reads for the docker:// pull and
427+ # the oras:// push. Unprivileged build uses --fakeroot automatically
428+ # (GitHub-hosted runners have unprivileged user namespaces enabled).
429+ apptainer build --force nwm-fcst-mgr.sif "docker://${IMAGE_BASE}:${ALIAS_TAG}"
430+
431+ # Publish as an OCI artifact. A SIF pushed via oras:// MUST be pulled via
432+ # oras:// (not docker://). The in-VPC staging step pulls this onto EFS.
433+ apptainer push nwm-fcst-mgr.sif "oras://${SIF_REPO}:${ALIAS_TAG}"
434+ echo "Published oras://${SIF_REPO}:${ALIAS_TAG}"
435+
436+ # Branch-keyed SIF promotion, mirroring the image promotion above:
437+ # development also publishes ':latest', development-pw ':latest-pw'.
438+ # The registry already holds the SIF blob from the push above, so this
439+ # re-push uploads only the manifest and tag, not the multi-GB SIF. Do
440+ # not manually dispatch a cross-lane build from either branch:
441+ # promotion keys off the branch name, so the SIF would promote under
442+ # the wrong alias.
443+ if [ "$GITHUB_REF_NAME" = "development" ]; then
444+ apptainer push nwm-fcst-mgr.sif "oras://${SIF_REPO}:latest"
445+ echo "Promoted oras://${SIF_REPO}:latest"
446+ elif [ "$GITHUB_REF_NAME" = "development-pw" ]; then
447+ apptainer push nwm-fcst-mgr.sif "oras://${SIF_REPO}:latest-pw"
448+ echo "Promoted oras://${SIF_REPO}:latest-pw"
309449 fi
0 commit comments