Skip to content

Commit 03a3e7b

Browse files
authored
Merge pull request #883 from durable-workflow/rust-325-native-rc-ghcr
Rust #325: publish native release candidates to GHCR without Docker Hub
2 parents 5b58282 + 04c43f2 commit 03a3e7b

3 files changed

Lines changed: 65 additions & 19 deletions

File tree

‎.github/workflows/native-release.yml‎

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ name: Native release candidate
1111
# its build provenance. The PHP image and its Release workflow are unchanged.
1212
#
1313
# Its credentials are its own: the dockerhub-native Environment and the
14-
# NATIVE_DOCKERHUB_* secret names. While they are absent the workflow fails
15-
# closed: each job signs in to Docker Hub before it builds anything, so a
16-
# dispatch stops there and pushes nothing. It can never resolve the PHP
17-
# release's Environment or repository credentials.
14+
# NATIVE_DOCKERHUB_* secret names. It can never resolve the PHP release's
15+
# Environment or repository credentials.
16+
#
17+
# A release candidate (a version with a -rc.N suffix) publishes to GHCR only,
18+
# so candidates can be qualified before Docker Hub credentials exist
19+
# (operator decision, October 30). Any other version publishes to both
20+
# registries: each job then signs in to Docker Hub before it builds anything,
21+
# so while the credentials are absent a dispatch stops there and pushes
22+
# nothing.
1823

1924
run-name: Native ${{ inputs.tag }} at ${{ inputs.release_commit }}
2025

@@ -40,8 +45,27 @@ env:
4045
GHCR_IMAGE: ghcr.io/durable-workflow/server-native
4146

4247
jobs:
48+
plan:
49+
name: Choose the registries for this tag
50+
if: github.ref == 'refs/heads/main'
51+
runs-on: ubuntu-latest
52+
timeout-minutes: 5
53+
outputs:
54+
dockerhub: ${{ steps.plan.outputs.dockerhub }}
55+
steps:
56+
- name: Publish a release candidate to GHCR only
57+
id: plan
58+
env:
59+
RELEASE_TAG: ${{ inputs.tag }}
60+
run: |
61+
set -euo pipefail
62+
[[ "$RELEASE_TAG" =~ ^native-[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]] \
63+
|| { echo 'tag must be native-X.Y.Z or native-X.Y.Z-rc.N' >&2; exit 1; }
64+
if [[ -n "${BASH_REMATCH[1]}" ]]; then echo dockerhub=false; else echo dockerhub=true; fi >> "$GITHUB_OUTPUT"
65+
4366
build-amd64:
4467
name: Build and smoke linux/amd64
68+
needs: plan
4569
if: github.ref == 'refs/heads/main'
4670
runs-on: ubuntu-latest
4771
timeout-minutes: 75
@@ -81,6 +105,7 @@ jobs:
81105
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
82106

83107
- name: Log in to Docker Hub
108+
if: needs.plan.outputs.dockerhub == 'true'
84109
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
85110
with:
86111
username: ${{ secrets.NATIVE_DOCKERHUB_USERNAME }}
@@ -107,7 +132,7 @@ jobs:
107132
org.opencontainers.image.revision=${{ inputs.release_commit }}
108133
dev.durable-workflow.release.tag=${{ inputs.tag }}
109134
dev.durable-workflow.release.run-id=${{ github.run_id }}
110-
outputs: type=image,"name=${{ env.GHCR_IMAGE }},${{ env.DOCKERHUB_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
135+
outputs: type=image,"name=${{ env.GHCR_IMAGE }}${{ needs.plan.outputs.dockerhub == 'true' && format(',{0}', env.DOCKERHUB_IMAGE) || '' }}",push-by-digest=true,name-canonical=true,push=true
111136

112137
- name: Smoke-test the pushed linux/amd64 digest on its own architecture
113138
env:
@@ -131,6 +156,7 @@ jobs:
131156

132157
build-arm64:
133158
name: Build and smoke linux/arm64
159+
needs: plan
134160
if: github.ref == 'refs/heads/main'
135161
runs-on: ubuntu-24.04-arm
136162
timeout-minutes: 75
@@ -170,6 +196,7 @@ jobs:
170196
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
171197

172198
- name: Log in to Docker Hub
199+
if: needs.plan.outputs.dockerhub == 'true'
173200
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
174201
with:
175202
username: ${{ secrets.NATIVE_DOCKERHUB_USERNAME }}
@@ -196,7 +223,7 @@ jobs:
196223
org.opencontainers.image.revision=${{ inputs.release_commit }}
197224
dev.durable-workflow.release.tag=${{ inputs.tag }}
198225
dev.durable-workflow.release.run-id=${{ github.run_id }}
199-
outputs: type=image,"name=${{ env.GHCR_IMAGE }},${{ env.DOCKERHUB_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
226+
outputs: type=image,"name=${{ env.GHCR_IMAGE }}${{ needs.plan.outputs.dockerhub == 'true' && format(',{0}', env.DOCKERHUB_IMAGE) || '' }}",push-by-digest=true,name-canonical=true,push=true
200227

201228
- name: Smoke-test the pushed linux/arm64 digest on its own architecture
202229
env:
@@ -220,7 +247,7 @@ jobs:
220247

221248
publish:
222249
name: Tag, verify and attest the native index
223-
needs: [build-amd64, build-arm64]
250+
needs: [plan, build-amd64, build-arm64]
224251
if: github.ref == 'refs/heads/main'
225252
runs-on: ubuntu-latest
226253
timeout-minutes: 20
@@ -240,6 +267,7 @@ jobs:
240267
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
241268

242269
- name: Log in to Docker Hub
270+
if: needs.plan.outputs.dockerhub == 'true'
243271
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
244272
with:
245273
username: ${{ secrets.NATIVE_DOCKERHUB_USERNAME }}
@@ -252,12 +280,13 @@ jobs:
252280
username: ${{ github.actor }}
253281
password: ${{ secrets.GITHUB_TOKEN }}
254282

255-
- name: Tag and verify the index in both registries
283+
- name: Tag and verify the index in each registry
256284
id: index
257285
env:
258286
RELEASE_TAG: ${{ inputs.tag }}
259287
DIGEST_AMD64: ${{ needs.build-amd64.outputs.digest }}
260288
DIGEST_ARM64: ${{ needs.build-arm64.outputs.digest }}
289+
PUBLISH_DOCKERHUB: ${{ needs.plan.outputs.dockerhub }}
261290
run: bash scripts/ci/native-release-publish.sh
262291

263292
- name: Attest the index's build provenance in GHCR
@@ -268,6 +297,7 @@ jobs:
268297
push-to-registry: true
269298

270299
- name: Attest the index's build provenance in Docker Hub
300+
if: needs.plan.outputs.dockerhub == 'true'
271301
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
272302
with:
273303
subject-name: index.docker.io/${{ env.DOCKERHUB_IMAGE }}

‎scripts/ci/check-native-release-tags.sh‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
# - the PHP Release workflow's push-tag filter matches no native- tag, so a
55
# native tag starts no PHP release, and still matches plain versions;
66
# - the tag verifier accepts native- only when asked for it;
7-
# - the native publisher accepts only native- tags.
7+
# - the native publisher accepts only native- tags, and lets only a release
8+
# candidate (-rc.N) skip Docker Hub.
89
# Each script is stopped at its input checks, before any API or registry call.
910
set -euo pipefail
1011
fail() { printf 'Native release tag check failed: %s\n' "$1" >&2; exit 1; }
@@ -55,7 +56,12 @@ expect "may only be empty or native-" RELEASE_TAG=x-2.12.0 RELEASE_TAG_PREFIX=x-
5556

5657
publish=scripts/ci/native-release-publish.sh
5758
images=(GHCR_IMAGE=ghcr.io/example/native DOCKERHUB_IMAGE=example/native)
58-
expect "must be native-<exact release version>" RELEASE_TAG=2.12.0-rc.1 "${images[@]}" bash "$publish"
59-
# No digests stops the publisher right after the native tag is accepted.
60-
expect "exact smoked digest" RELEASE_TAG=native-2.12.0-rc.1 "${images[@]}" bash "$publish"
59+
expect "must be native-<exact release version>" RELEASE_TAG=2.12.0-rc.1 "${images[@]}" PUBLISH_DOCKERHUB=true bash "$publish"
60+
# No digests stops the publisher right after the tag and registries are accepted.
61+
expect "exact smoked digest" RELEASE_TAG=native-2.12.0-rc.1 "${images[@]}" PUBLISH_DOCKERHUB=true bash "$publish"
62+
expect "exact smoked digest" RELEASE_TAG=native-2.12.0-rc.1 "${images[@]}" PUBLISH_DOCKERHUB=false bash "$publish"
63+
expect "exact smoked digest" RELEASE_TAG=native-3.0.0 "${images[@]}" PUBLISH_DOCKERHUB=true bash "$publish"
64+
# Only a release candidate may skip Docker Hub, and the choice must be explicit.
65+
expect "only a release candidate (-rc.N) may skip Docker Hub" RELEASE_TAG=native-3.0.0 "${images[@]}" PUBLISH_DOCKERHUB=false bash "$publish"
66+
expect "PUBLISH_DOCKERHUB must be true or false" RELEASE_TAG=native-2.12.0-rc.1 "${images[@]}" bash "$publish"
6167
printf 'Native release tags: %d release.yml pattern(s) checked; verifier and publisher accept native- only as intended.\n' "${#patterns[@]}"

‎scripts/ci/native-release-publish.sh‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#!/usr/bin/env bash
2-
# Give the smoked native platform digests (Server #325) one release tag in
3-
# both registries, as one index, and verify it.
2+
# Give the smoked native platform digests (Server #325) one release tag, as
3+
# one index, in each registry, and verify it. A release candidate (-rc.N)
4+
# publishes to GHCR only; any other version to GHCR and Docker Hub.
45
# RELEASE_TAG is the native Git tag, native-<release version>; the image tag
56
# is that version without the native- prefix.
6-
# Env: RELEASE_TAG, GHCR_IMAGE, DOCKERHUB_IMAGE, and DIGEST_AMD64 and
7+
# Env: RELEASE_TAG, GHCR_IMAGE, DOCKERHUB_IMAGE, PUBLISH_DOCKERHUB (true or
8+
# false, from the plan job), and DIGEST_AMD64 and
79
# DIGEST_ARM64 from the build jobs, which push by digest and fail unless that
810
# exact digest passed its smoke test. Writes digest= to $GITHUB_OUTPUT.
911
set -euo pipefail
@@ -12,6 +14,14 @@ fail() { printf 'Native release publication failed: %s\n' "$1" >&2; exit 1; }
1214
|| fail "RELEASE_TAG must be native-<exact release version>."
1315
image_tag=${BASH_REMATCH[1]}
1416
: "${GHCR_IMAGE:?}" "${DOCKERHUB_IMAGE:?}"
17+
# Only a release candidate may skip Docker Hub.
18+
case "${PUBLISH_DOCKERHUB:-}" in
19+
true) registries=("$GHCR_IMAGE" "$DOCKERHUB_IMAGE") ;;
20+
false)
21+
[[ "$image_tag" =~ -rc\.[0-9]+$ ]] || fail "only a release candidate (-rc.N) may skip Docker Hub."
22+
registries=("$GHCR_IMAGE") ;;
23+
*) fail "PUBLISH_DOCKERHUB must be true or false." ;;
24+
esac
1525

1626
digests=("${DIGEST_AMD64:-}" "${DIGEST_ARM64:-}")
1727
for digest in "${digests[@]}"; do
@@ -21,22 +31,22 @@ done
2131
# A release tag is immutable: refuse one that either registry already has.
2232
# Only a registry's own "not found" counts as absent; any other failure to
2333
# look (authentication, network) stops here rather than risk an overwrite.
24-
for image in "$GHCR_IMAGE" "$DOCKERHUB_IMAGE"; do
34+
for image in "${registries[@]}"; do
2535
if lookup=$(docker buildx imagetools inspect "$image:$image_tag" 2>&1); then
2636
fail "$image:$image_tag already exists."
2737
fi
2838
grep -qiE 'not found|manifest unknown|name unknown' <<<"$lookup" \
2939
|| fail "could not confirm that $image:$image_tag is absent: $(head -c 300 <<<"$lookup")"
3040
done
3141

32-
for image in "$GHCR_IMAGE" "$DOCKERHUB_IMAGE"; do
42+
for image in "${registries[@]}"; do
3343
docker buildx imagetools create --tag "$image:$image_tag" \
3444
"$image@${digests[0]}" "$image@${digests[1]}"
3545
done
3646

3747
# Both registries hold the same index, naming exactly the smoked digests.
3848
index=""
39-
for image in "$GHCR_IMAGE" "$DOCKERHUB_IMAGE"; do
49+
for image in "${registries[@]}"; do
4050
raw=$(docker buildx imagetools inspect --raw "$image:$image_tag")
4151
digest=$(docker buildx imagetools inspect --format '{{.Manifest.Digest}}' "$image:$image_tag")
4252
if test -z "$index"; then index=$digest; fi
@@ -50,7 +60,7 @@ done
5060
printf 'digest=%s\n' "$index" >> "${GITHUB_OUTPUT:-/dev/null}"
5161
{
5262
printf '### Native %s\n\n' "$RELEASE_TAG"
53-
printf -- '- index `%s`, in `%s` and `%s`\n' "$index" "$GHCR_IMAGE:$image_tag" "$DOCKERHUB_IMAGE:$image_tag"
63+
printf -- '- index `%s`, in %s\n' "$index" "$(printf '`%s` ' "${registries[@]/%/:$image_tag}")"
5464
printf -- '- linux/amd64 `%s`, linux/arm64 `%s`, each smoke-tested on its own architecture\n' "${digests[0]}" "${digests[1]}"
5565
} >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
5666
printf '%s\n' "$index"

0 commit comments

Comments
 (0)