Skip to content

Commit 4521957

Browse files
Merge pull request #58 from NGWPC/development
Merge development into ngwpc-candidate for release 3.1.2.3.0-rc1
2 parents e2be847 + 70644a5 commit 4521957

7 files changed

Lines changed: 806 additions & 544 deletions

File tree

.github/workflows/ci-cd.yml

Lines changed: 157 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on:
88
- main
99
- nwm-main
1010
- development
11+
- development-pw
1112
- release-candidate
1213
push:
1314
branches:
@@ -16,6 +17,7 @@ on:
1617
- main
1718
- nwm-main
1819
- development
20+
- development-pw
1921
- release-candidate
2022
workflow_dispatch:
2123
inputs:
@@ -67,8 +69,12 @@ jobs:
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

Comments
 (0)