-
Notifications
You must be signed in to change notification settings - Fork 0
285 lines (265 loc) · 11.8 KB
/
Copy pathdocker.yml
File metadata and controls
285 lines (265 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
name: Publish image
# Build and push the canonical GMAT base image (Dockerfile in repo root) to
# ghcr.io/astro-tools/gmat for every supported GMAT version on each SemVer
# release tag and on workflow_dispatch.
#
# Tag pattern is 'v*.*.*' (not 'v*') so the floating major-version alias `v0`
# does not trigger a redundant republish each time it is re-pointed; only the
# SemVer tags cut by semantic-release (#64) drive a publish.
on:
push:
tags: ['v*.*.*']
workflow_dispatch:
inputs:
version:
description: 'GMAT version to build (matrix is filtered to this one)'
required: true
type: choice
options:
- R2022a
- R2025a
- R2026a
concurrency:
group: docker-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
IMAGE: ghcr.io/astro-tools/gmat
# Newest supported version. The matrix cell matching this also pushes the
# floating `latest` tag. Bump in lockstep when a new GMAT release is added
# to the action's supported set.
LATEST_GMAT_VERSION: R2026a
jobs:
# Emit the version matrix dynamically: full set for tag pushes, single cell
# for workflow_dispatch. Done in a setup job because the `matrix` context is
# not available in job-level `if` expressions, so filtering cells inline is
# not possible.
setup:
name: setup
runs-on: ubuntu-latest
timeout-minutes: 1
outputs:
matrix: ${{ steps.matrix.outputs.value }}
steps:
- id: matrix
shell: bash
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
versions='["${{ inputs.version }}"]'
else
versions='["R2022a","R2025a","R2026a"]'
fi
echo "value={\"version\":${versions}}" >> "$GITHUB_OUTPUT"
echo "Matrix: {\"version\":${versions}}"
publish:
name: publish (${{ matrix.version }})
needs: setup
runs-on: ubuntu-latest
timeout-minutes: 60
# Job-level (not workflow-level) so the token's elevated `packages: write`
# and the OIDC `id-token: write` (used by cosign keyless signing to
# exchange a workflow-issued token at Fulcio) are scoped only to where
# they are needed.
permissions:
contents: read
packages: write
id-token: write
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
steps:
- uses: actions/checkout@v5
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Stream-hash the upstream installer to populate the gmat.installer-sha256
# OCI label and to give stage 1 of the Dockerfile something to verify
# against. The Dockerfile re-downloads in its own build context — if the
# SourceForge mirror serves a different blob between this step and the
# build, the build fails the SHA check loudly rather than silently
# publishing a mislabeled image.
- name: Compute installer SHA-256
id: sha
shell: bash
run: |
set -euo pipefail
url="https://sourceforge.net/projects/gmat/files/GMAT/GMAT-${{ matrix.version }}/gmat-ubuntu-x64-${{ matrix.version }}.tar.gz/download"
sha=$(curl --location --fail --silent --show-error \
--retry 5 --retry-all-errors "$url" \
| sha256sum | awk '{print $1}')
echo "Installer SHA-256: $sha"
echo "value=$sha" >> "$GITHUB_OUTPUT"
- name: Compute image tags
id: tags
shell: bash
run: |
set -euo pipefail
tags="${IMAGE}:${{ matrix.version }}"
if [ "${{ github.event_name }}" = "push" ] && [ "${{ matrix.version }}" = "${LATEST_GMAT_VERSION}" ]; then
tags="${tags},${IMAGE}:latest"
fi
echo "value=$tags" >> "$GITHUB_OUTPUT"
printf 'Tags:\n%s\n' "${tags//,/$'\n'}"
# Build into the runner's local docker daemon (load: true) under a
# throwaway tag so the smoke step below can `docker run` it before any
# registry interaction. The image is re-tagged and pushed only after
# smoke passes — see "Push to GHCR" below. Charter §4 v0.3 acceptance:
# "a non-zero exit blocks the release" (#62).
- uses: docker/build-push-action@v6
with:
context: .
load: true
tags: gmat-smoke:${{ matrix.version }}
build-args: |
GMAT_VERSION=${{ matrix.version }}
INSTALLER_SHA256=${{ steps.sha.outputs.value }}
ACTION_VERSION=${{ github.ref_name }}
# Run the smoke script under every Python the image supports for gmatpy
# — i.e. the intersection of (pyenv-installed interpreters) and
# (gmatpy/_pyXY directories shipped with this GMAT version). Per-version
# coverage varies: R2022a's Linux gmatpy ships only _py36..._py310 (per
# ci.yml self-test matrix), R2025a/R2026a ship _py312 universally and
# R2026a Linux additionally ships _py310/_py311. Detecting the
# intersection inside the container avoids hard-coding the matrix here
# and stays correct as future GMAT versions add or drop interpreters.
# Any failure propagates verbatim (no piping/grep) and aborts the cell
# via `set -e`, blocking the push step below. An empty intersection is
# itself a release-blocker — it means none of the installed Pythons can
# actually `import gmatpy` for this GMAT version. Sample is the smallest
# .script in samples/ that exercises the propagator: R2026a renamed it
# from Ex_R2014a_HighFidelitySRP.script to Ex_HighFidelitySRP.script,
# mirroring the SMOKE_SAMPLES map in src/smoke.ts. See #62.
- name: Smoke (gmatpy import + stock-sample propagation per supported Python)
shell: bash
env:
# Wall-clock RunScript() duration is the elapsed-time metric: GMAT
# Spacecraft objects retain script-time config after RunScript (the
# propagated state lives in the runtime sandbox and isn't readable
# via GetObject), so probing sat.GetField('A1ModJulian') or 'X'
# would still return the initial values. Wall-clock is the robust
# signal: combined with LoadScript()/RunScript() returning True it
# proves gmatpy executed the propagator end-to-end.
SMOKE_PY: |
import sys, os, time
import gmatpy as gmat
gmat.Setup('/opt/gmat/bin/api_startup_file.txt')
os.chdir('/opt/gmat/samples')
sample = os.environ['SMOKE_SAMPLE']
if not gmat.LoadScript(sample):
sys.exit(f'LoadScript failed for {sample}')
t0 = time.perf_counter()
if not gmat.RunScript():
sys.exit('RunScript failed')
elapsed = time.perf_counter() - t0
if elapsed <= 0:
sys.exit(f'No elapsed time: {elapsed}')
print(
f'OK py{sys.version_info.major}.{sys.version_info.minor}: '
f'gmatpy + {sample} in {elapsed:.4f}s'
)
run: |
set -euo pipefail
image="gmat-smoke:${{ matrix.version }}"
case "${{ matrix.version }}" in
R2026a) sample="Ex_HighFidelitySRP.script" ;;
*) sample="Ex_R2014a_HighFidelitySRP.script" ;;
esac
installed_pys=$(docker run --rm "$image" pyenv versions --bare \
| awk -F. 'NF>=2 {print $1"."$2}' | sort -u)
gmatpy_pys=$(docker run --rm "$image" sh -c 'ls -1 /opt/gmat/bin/gmatpy/' \
| awk '/^_py[0-9]+$/ {v=substr($0,4); print substr(v,1,1)"."substr(v,2)}' | sort -u)
to_smoke=$(comm -12 <(echo "$installed_pys") <(echo "$gmatpy_pys"))
if [ -z "$to_smoke" ]; then
echo "::error::No Python interpreter is both pyenv-installed AND supported by gmatpy in the ${{ matrix.version }} image"
echo " pyenv installed: $(echo $installed_pys)"
echo " gmatpy supports: $(echo $gmatpy_pys)"
exit 1
fi
echo "Smoking Python(s) for ${{ matrix.version }} (sample: ${sample}):" $(echo $to_smoke)
for py in $to_smoke; do
echo "::group::Smoke (Python ${py}, sample ${sample})"
docker run --rm \
-e SMOKE_SAMPLE="$sample" \
"$image" "python${py}" -c "$SMOKE_PY"
echo "::endgroup::"
done
# Re-tag the smoke-tested local image to its release tag(s) and push.
# Using `docker tag` + `docker push` (rather than a second
# build-push-action invocation) guarantees the published image is
# bit-identical to the image that just passed smoke.
#
# After each push we read the manifest digest from the registry via
# `docker buildx imagetools inspect` and emit `<repo>@sha256:<digest>`
# references on a step output. The next step signs by digest, not by
# tag — cosign's standard pattern, since signing the immutable manifest
# means a future re-push of the same tag against a different digest
# cannot inherit the signature. When `latest` shares a digest with the
# version tag (the LATEST_GMAT_VERSION cell), the dedup happens
# naturally below.
- name: Push to GHCR
id: push
shell: bash
run: |
set -euo pipefail
src="gmat-smoke:${{ matrix.version }}"
IFS=',' read -r -a release_tags <<< "${{ steps.tags.outputs.value }}"
declare -A digest_seen=()
digests=""
for tag in "${release_tags[@]}"; do
echo "::group::Push ${tag}"
docker tag "$src" "$tag"
docker push "$tag"
digest=$(docker buildx imagetools inspect "$tag" \
--format '{{.Manifest.Digest}}')
repo="${tag%:*}"
ref="${repo}@${digest}"
echo "Pushed ${tag} → ${ref}"
if [ -z "${digest_seen[$ref]:-}" ]; then
digest_seen[$ref]=1
digests="${digests}${ref}"$'\n'
fi
echo "::endgroup::"
done
{
echo 'digests<<EOF'
printf '%s' "$digests"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
# Pin to v3 (current major) per the issue. cosign-installer puts
# `cosign` on PATH for subsequent steps in this job.
- name: Install cosign
uses: sigstore/cosign-installer@v3
# Keyless OIDC signing: cosign exchanges the workflow's id-token for a
# short-lived Fulcio cert tied to this repo + workflow + ref, signs the
# image manifest, and uploads the signature as a sibling OCI artifact
# (no long-lived secrets). `--yes` skips the interactive Rekor prompt.
# Sequenced after smoke (earlier in this job) and after push, so a
# smoke or push failure aborts the job before any signature is issued.
- name: Sign images
shell: bash
env:
DIGESTS: ${{ steps.push.outputs.digests }}
run: |
set -euo pipefail
if [ -z "${DIGESTS//[[:space:]]/}" ]; then
echo "::error::No digests captured from push step"
exit 1
fi
while IFS= read -r ref; do
[ -z "$ref" ] && continue
echo "::group::Sign ${ref}"
cosign sign --yes "$ref"
echo "::endgroup::"
done <<< "$DIGESTS"
# Print the labels of the just-pushed image so a release can be traced
# back to its installer SHA from the action log without pulling.
- name: Inspect image labels
shell: bash
run: |
set -euo pipefail
docker buildx imagetools inspect "${IMAGE}:${{ matrix.version }}" \
--format '{{ range $k, $v := .Image.Config.Labels }}{{ printf "%s=%s\n" $k $v }}{{ end }}' \
| grep -E '^(gmat\.|setup-gmat\.)'