Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions .github/workflows/native-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ name: Native release candidate
# its build provenance. The PHP image and its Release workflow are unchanged.
#
# Its credentials are its own: the dockerhub-native Environment and the
# NATIVE_DOCKERHUB_* secret names. While they are absent the workflow fails
# closed: each job signs in to Docker Hub before it builds anything, so a
# dispatch stops there and pushes nothing. It can never resolve the PHP
# release's Environment or repository credentials.
# NATIVE_DOCKERHUB_* secret names. It can never resolve the PHP release's
# Environment or repository credentials.
#
# A release candidate (a version with a -rc.N suffix) publishes to GHCR only,
# so candidates can be qualified before Docker Hub credentials exist
# (operator decision, October 30). Any other version publishes to both
# registries: each job then signs in to Docker Hub before it builds anything,
# so while the credentials are absent a dispatch stops there and pushes
# nothing.

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

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

jobs:
plan:
name: Choose the registries for this tag
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
dockerhub: ${{ steps.plan.outputs.dockerhub }}
steps:
- name: Publish a release candidate to GHCR only
id: plan
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
[[ "$RELEASE_TAG" =~ ^native-[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]] \
|| { echo 'tag must be native-X.Y.Z or native-X.Y.Z-rc.N' >&2; exit 1; }
if [[ -n "${BASH_REMATCH[1]}" ]]; then echo dockerhub=false; else echo dockerhub=true; fi >> "$GITHUB_OUTPUT"

build-amd64:
name: Build and smoke linux/amd64
needs: plan
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 75
Expand Down Expand Up @@ -81,6 +105,7 @@ jobs:
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4

- name: Log in to Docker Hub
if: needs.plan.outputs.dockerhub == 'true'
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
username: ${{ secrets.NATIVE_DOCKERHUB_USERNAME }}
Expand All @@ -107,7 +132,7 @@ jobs:
org.opencontainers.image.revision=${{ inputs.release_commit }}
dev.durable-workflow.release.tag=${{ inputs.tag }}
dev.durable-workflow.release.run-id=${{ github.run_id }}
outputs: type=image,"name=${{ env.GHCR_IMAGE }},${{ env.DOCKERHUB_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
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

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

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

- name: Log in to Docker Hub
if: needs.plan.outputs.dockerhub == 'true'
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
username: ${{ secrets.NATIVE_DOCKERHUB_USERNAME }}
Expand All @@ -196,7 +223,7 @@ jobs:
org.opencontainers.image.revision=${{ inputs.release_commit }}
dev.durable-workflow.release.tag=${{ inputs.tag }}
dev.durable-workflow.release.run-id=${{ github.run_id }}
outputs: type=image,"name=${{ env.GHCR_IMAGE }},${{ env.DOCKERHUB_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
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

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

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

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

- name: Tag and verify the index in both registries
- name: Tag and verify the index in each registry
id: index
env:
RELEASE_TAG: ${{ inputs.tag }}
DIGEST_AMD64: ${{ needs.build-amd64.outputs.digest }}
DIGEST_ARM64: ${{ needs.build-arm64.outputs.digest }}
PUBLISH_DOCKERHUB: ${{ needs.plan.outputs.dockerhub }}
run: bash scripts/ci/native-release-publish.sh

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

- name: Attest the index's build provenance in Docker Hub
if: needs.plan.outputs.dockerhub == 'true'
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-name: index.docker.io/${{ env.DOCKERHUB_IMAGE }}
Expand Down
14 changes: 10 additions & 4 deletions scripts/ci/check-native-release-tags.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
# - the PHP Release workflow's push-tag filter matches no native- tag, so a
# native tag starts no PHP release, and still matches plain versions;
# - the tag verifier accepts native- only when asked for it;
# - the native publisher accepts only native- tags.
# - the native publisher accepts only native- tags, and lets only a release
# candidate (-rc.N) skip Docker Hub.
# Each script is stopped at its input checks, before any API or registry call.
set -euo pipefail
fail() { printf 'Native release tag check failed: %s\n' "$1" >&2; exit 1; }
Expand Down Expand Up @@ -55,7 +56,12 @@ expect "may only be empty or native-" RELEASE_TAG=x-2.12.0 RELEASE_TAG_PREFIX=x-

publish=scripts/ci/native-release-publish.sh
images=(GHCR_IMAGE=ghcr.io/example/native DOCKERHUB_IMAGE=example/native)
expect "must be native-<exact release version>" RELEASE_TAG=2.12.0-rc.1 "${images[@]}" bash "$publish"
# No digests stops the publisher right after the native tag is accepted.
expect "exact smoked digest" RELEASE_TAG=native-2.12.0-rc.1 "${images[@]}" bash "$publish"
expect "must be native-<exact release version>" RELEASE_TAG=2.12.0-rc.1 "${images[@]}" PUBLISH_DOCKERHUB=true bash "$publish"
# No digests stops the publisher right after the tag and registries are accepted.
expect "exact smoked digest" RELEASE_TAG=native-2.12.0-rc.1 "${images[@]}" PUBLISH_DOCKERHUB=true bash "$publish"
expect "exact smoked digest" RELEASE_TAG=native-2.12.0-rc.1 "${images[@]}" PUBLISH_DOCKERHUB=false bash "$publish"
expect "exact smoked digest" RELEASE_TAG=native-3.0.0 "${images[@]}" PUBLISH_DOCKERHUB=true bash "$publish"
# Only a release candidate may skip Docker Hub, and the choice must be explicit.
expect "only a release candidate (-rc.N) may skip Docker Hub" RELEASE_TAG=native-3.0.0 "${images[@]}" PUBLISH_DOCKERHUB=false bash "$publish"
expect "PUBLISH_DOCKERHUB must be true or false" RELEASE_TAG=native-2.12.0-rc.1 "${images[@]}" bash "$publish"
printf 'Native release tags: %d release.yml pattern(s) checked; verifier and publisher accept native- only as intended.\n' "${#patterns[@]}"
24 changes: 17 additions & 7 deletions scripts/ci/native-release-publish.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#!/usr/bin/env bash
# Give the smoked native platform digests (Server #325) one release tag in
# both registries, as one index, and verify it.
# Give the smoked native platform digests (Server #325) one release tag, as
# one index, in each registry, and verify it. A release candidate (-rc.N)
# publishes to GHCR only; any other version to GHCR and Docker Hub.
# RELEASE_TAG is the native Git tag, native-<release version>; the image tag
# is that version without the native- prefix.
# Env: RELEASE_TAG, GHCR_IMAGE, DOCKERHUB_IMAGE, and DIGEST_AMD64 and
# Env: RELEASE_TAG, GHCR_IMAGE, DOCKERHUB_IMAGE, PUBLISH_DOCKERHUB (true or
# false, from the plan job), and DIGEST_AMD64 and
# DIGEST_ARM64 from the build jobs, which push by digest and fail unless that
# exact digest passed its smoke test. Writes digest= to $GITHUB_OUTPUT.
set -euo pipefail
Expand All @@ -12,6 +14,14 @@ fail() { printf 'Native release publication failed: %s\n' "$1" >&2; exit 1; }
|| fail "RELEASE_TAG must be native-<exact release version>."
image_tag=${BASH_REMATCH[1]}
: "${GHCR_IMAGE:?}" "${DOCKERHUB_IMAGE:?}"
# Only a release candidate may skip Docker Hub.
case "${PUBLISH_DOCKERHUB:-}" in
true) registries=("$GHCR_IMAGE" "$DOCKERHUB_IMAGE") ;;
false)
[[ "$image_tag" =~ -rc\.[0-9]+$ ]] || fail "only a release candidate (-rc.N) may skip Docker Hub."
registries=("$GHCR_IMAGE") ;;
*) fail "PUBLISH_DOCKERHUB must be true or false." ;;
esac

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

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

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