You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(promote-release): use crane tag so per-arch moving tags stay signed (#197)
* feat(promote-release): use crane tag so per-arch moving tags stay signed
docker buildx imagetools create is digest-preserving only for an already
multi-arch index. On a bare single-platform manifest (per-arch tags like
:vX.Y.Z-amd64) it wraps the manifest in a NEW index, changing the digest and
orphaning the digest-scoped cosign signature. That blocks promoting the
per-arch moving tags (latest-amd64, latest-fips-arm64v8, ...), which GHCR
pull stats show are heavily used.
Switch the retag primitive to `crane tag`, which re-points a tag at the exact
same manifest digest for both single-platform manifests and multi-arch
indexes -- one uniform path covering the whole moving-tag matrix, per-arch
included, with signatures intact. Pre-flight existence check moves from
`imagetools inspect` to `crane digest`. action.yml installs crane
(imjasonh/setup-crane) and crane reads the docker config the existing GHCR
login writes, so no auth change is needed.
Verified live (real GHCR + cosign): crane preserves per-arch and index
digests and cosign verify passes on the retagged per-arch tag, while
imagetools changes the per-arch digest and cosign verify then fails.
DEVOPS-1083
* chore(promote-release): skip crane install on dry-run
Match the guard on the GHCR login step (if: inputs.dry-run != 'true').
crane is only invoked on a real run (DRY_RUN != "true"), so installing it
during an exact dry-run is wasted work; the guard keeps the two setup
steps consistent.
* test(promote-release): assert dry-run prints the full retag matrix
The dry-run test spot-checked only the first planned `crane tag` line, so an
early loop exit that only manifested under dry-run would still pass. Pin the
second entry's line too, so the plural "prints planned retags" claim holds
across entries.
* docs(promote-release): fix stale test stub reference (docker -> crane)
The Testing section still said the bats suite runs with a stubbed `docker`
on PATH; this branch deletes docker_mock.bash and stubs `crane` instead.
| docker-username | string | true || Username paired with github-token for the <br>GHCR login (GHCR checks the token, but docker/login-action requires a username value). |
50
-
| dry-run | string | false |`"false"`| Fail-closed: a real promotion runs only <br>on an exact "false" (the default, so the release:released trigger still promotes for real). Any <br>other value ("true", a typo, etc.) is a dry-run <br>that only prints the planned retags/promotion. |
51
-
| github-token | string | true || Token with GHCR write:packages, and contents:write <br>on oss-repo and homebrew-tap-repo if set. |
52
-
| homebrew-formula-paths | string | false |`"[]"`| JSON array of formula file paths <br>within homebrew-tap-repo to update, e.g. ["Formula/vcluster.rb"]. <br>Required if homebrew-tap-repo is set. |
53
-
| homebrew-tap-repo | string | false || owner/repo of a Homebrew tap to <br>promote (e.g. loft-sh/homebrew-tap). Requires oss-repo to be <br>set, since checksums come from its <br>release. Leave empty to skip. |
54
-
| images | string | true || JSON array of image entries to <br>retag, each `{"image": "ghcr.io/loft-sh/x", "suffix": ""}` (suffix optional, default <br>""). For each entry, copies `<image>:<version><suffix>` <br>to `<image>:latest<suffix>`, `<image>:<major><suffix>`, and `<image>:<major>.<minor><suffix>`. |
| docker-username | string | true ||Username paired with github-token for the <br>GHCR login (GHCR checks the token, but docker/login-action requires a username value).|
59
+
| dry-run | string | false |`"false"`|Fail-closed: a real promotion runs only <br>on an exact "false" (the default, so the release:released trigger still promotes for real). Any <br>other value ("true", a typo, etc.) is a dry-run <br>that only prints the planned retags/promotion.|
60
+
| github-token | string | true ||Token with GHCR write:packages, and contents:write <br>on oss-repo and homebrew-tap-repo if set.|
61
+
| homebrew-formula-paths | string | false |`"[]"`|JSON array of formula file paths <br>within homebrew-tap-repo to update, e.g. ["Formula/vcluster.rb"]. <br>Required if homebrew-tap-repo is set.|
62
+
| homebrew-tap-repo | string | false ||owner/repo of a Homebrew tap to <br>promote (e.g. loft-sh/homebrew-tap). Requires oss-repo to be <br>set, since checksums come from its <br>release. Leave empty to skip.|
63
+
| images | string | true || JSON array of image entries to <br>retag, each `{"image": "ghcr.io/loft-sh/x", "suffix": ""}` (suffix optional, default <br>""). For each entry, copies `<image>:<version><suffix>` <br>to `<image>:latest<suffix>`, `<image>:<major><suffix>`, and `<image>:<major>.<minor><suffix>`. The <br>suffix is also how per-arch moving <br>tags are promoted: an entry with <br>suffix `-amd64` retags `<image>:<version>-amd64` (a bare single-platform manifest) to <br>`<image>:latest-amd64` etc. crane preserves its digest, <br>so its cosign signature stays valid. |
# Only acts on a stable vX.Y.Z version; any other shape (has a "-" suffix) is
10
19
# a no-op, since moving tags and "latest" promotion aren't meaningful for
@@ -197,7 +206,7 @@ for ((i = 0; i < IMAGE_COUNT; i++)); do
197
206
echo"::error::images[$i] is missing required \"image\" field: ${entry}">&2
198
207
exit 1
199
208
fi
200
-
if [[ "${DRY_RUN}"!="true" ]] &&!docker buildx imagetools inspect"${image}:${VERSION}${suffix}">/dev/null 2>&1;then
209
+
if [[ "${DRY_RUN}"!="true" ]] &&!crane digest"${image}:${VERSION}${suffix}">/dev/null 2>&1;then
201
210
echo"::error::source manifest ${image}:${VERSION}${suffix} does not exist; refusing to start retagging">&2
202
211
exit 1
203
212
fi
@@ -221,7 +230,10 @@ for ((i = 0; i < IMAGE_COUNT; i++)); do
221
230
formovingin"${moving_tags[@]}";do
222
231
dest="${image}:${moving}${suffix}"
223
232
echo"Retagging ${dest} -> ${src}"
224
-
run docker buildx imagetools create --tag "${dest}""${src}"
233
+
# crane tag SRC NEWTAG re-points NEWTAG (in SRC's repo) at SRC's exact
234
+
# manifest digest -- digest-preserving for both single-platform manifests
235
+
# and indexes, so per-arch moving tags stay cosign-verifiable (see header).
236
+
run crane tag "${src}""${moving}${suffix}"
225
237
done
226
238
done
227
239
@@ -406,7 +418,7 @@ promote_homebrew_formula() {
406
418
-f message="chore: bump ${formula_path} to ${VERSION}" \
407
419
-f content="${new_content_b64}" \
408
420
-f sha="${current_sha}">/dev/null;then
409
-
echo"::warning::failed to update ${tap_repo}/${formula_path} to ${VERSION}; docker retags (and oss-repo promotion, if configured) already succeeded. Re-run this action to retry the tap update - it is idempotent (imagetools create and the formula patch both re-apply cleanly)."
421
+
echo"::warning::failed to update ${tap_repo}/${formula_path} to ${VERSION}; docker retags (and oss-repo promotion, if configured) already succeeded. Re-run this action to retry the tap update - it is idempotent (crane tag and the formula patch both re-apply cleanly)."
0 commit comments