Execute Release #314
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Execute Release | |
| on: | |
| workflow_run: | |
| workflows: ["Publish Bluefin dakota"] | |
| types: [completed] | |
| branches: [testing] | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: 'Dry run — verify gates but do not promote' | |
| type: boolean | |
| default: false | |
| permissions: | |
| actions: read | |
| contents: write | |
| id-token: write | |
| issues: write | |
| packages: write | |
| pull-requests: write | |
| statuses: write | |
| concurrency: | |
| group: dakota-execute-release | |
| cancel-in-progress: false | |
| jobs: | |
| freshness-check: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should-release: ${{ steps.compare.outputs.should-release }} | |
| build_sha: ${{ steps.compare.outputs.build_sha }} | |
| steps: | |
| - name: Check publish conclusion and compare digests | |
| id: compare | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| # Skip if triggered by a failed publish workflow | |
| if [ "${{ github.event_name }}" = "workflow_run" ] && \ | |
| [ "${{ github.event.workflow_run.conclusion }}" != "success" ]; then | |
| echo "Publish workflow did not succeed (conclusion: ${{ github.event.workflow_run.conclusion }}). Skipping." | |
| echo "should-release=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Resolve the SHA that was built and published | |
| if [ "${{ github.event_name }}" = "workflow_run" ]; then | |
| BUILD_SHA="${{ github.event.workflow_run.head_sha }}" | |
| else | |
| BUILD_SHA=$(gh api "repos/${{ github.repository }}/git/ref/heads/testing" --jq '.object.sha') | |
| fi | |
| # Verify the SHA-tagged image exists in GHCR (proves this SHA was published). | |
| # GHCR can briefly lag after publish.yml completes, so retry before | |
| # declaring the release broken. | |
| for attempt in $(seq 1 12); do | |
| if skopeo inspect --no-tags \ | |
| "docker://ghcr.io/projectbluefin/dakota:${BUILD_SHA}" > /dev/null 2>&1; then | |
| echo "Verified :${BUILD_SHA} is readable in GHCR (attempt ${attempt})" | |
| break | |
| fi | |
| if [ "$attempt" -eq 12 ]; then | |
| echo "ERROR: :${BUILD_SHA} is still unavailable in GHCR after publish" | |
| exit 1 | |
| fi | |
| echo "Waiting for GHCR to expose :${BUILD_SHA} (attempt ${attempt}/12)..." | |
| sleep 10 | |
| done | |
| # Guard: if testing advanced since the build, the reusable fast-forward would | |
| # point main to a newer commit than the image we're promoting. Skip promotion. | |
| if [ "${{ github.event_name }}" = "workflow_run" ]; then | |
| CURRENT_SHA=$(gh api "repos/${{ github.repository }}/git/ref/heads/testing" --jq '.object.sha') | |
| if [ "$CURRENT_SHA" != "$BUILD_SHA" ]; then | |
| echo "::warning::testing has advanced (build: ${BUILD_SHA}, current: ${CURRENT_SHA}). Skipping promotion to avoid SHA mismatch on main fast-forward. Will promote next build." | |
| echo "should-release=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| fi | |
| # Compare the SHA-tagged build digest against :stable. Using the SHA tag | |
| # (not floating :testing) guarantees the decision input matches the action | |
| # input — the same image we will promote is the one we evaluated. | |
| TESTING_DIGEST=$(skopeo inspect --no-tags "docker://ghcr.io/projectbluefin/dakota:${BUILD_SHA}" \ | |
| | jq -r '.Digest') | |
| STABLE_DIGEST="" | |
| if skopeo inspect --no-tags "docker://ghcr.io/projectbluefin/dakota:stable" > /dev/null 2>&1; then | |
| STABLE_DIGEST=$(skopeo inspect --no-tags "docker://ghcr.io/projectbluefin/dakota:stable" \ | |
| | jq -r '.Digest') | |
| fi | |
| if [ -n "$STABLE_DIGEST" ] && [ "$TESTING_DIGEST" = "$STABLE_DIGEST" ]; then | |
| echo "should-release=false" >> "$GITHUB_OUTPUT" | |
| echo ":${BUILD_SHA} and :stable are identical ($TESTING_DIGEST) — nothing to promote" | |
| else | |
| echo "should-release=true" >> "$GITHUB_OUTPUT" | |
| echo "build_sha=${BUILD_SHA}" >> "$GITHUB_OUTPUT" | |
| echo ":${BUILD_SHA} ($TESTING_DIGEST) differs from :stable (${STABLE_DIGEST:-not found}) — promotion needed" | |
| fi | |
| execute: | |
| needs: [freshness-check] | |
| if: needs.freshness-check.outputs.should-release == 'true' && inputs.dry_run != true | |
| permissions: | |
| actions: read | |
| contents: write | |
| id-token: write | |
| issues: write | |
| packages: write | |
| pull-requests: write | |
| statuses: write | |
| uses: projectbluefin/actions/.github/workflows/reusable-execute-release.yml@v1 | |
| with: | |
| registry: ghcr.io/projectbluefin | |
| variants: >- | |
| [ | |
| {"image":"dakota","source_tag":"${{ needs.freshness-check.outputs.build_sha }}","target_tag":"stable"}, | |
| {"image":"dakota-nvidia","source_tag":"${{ needs.freshness-check.outputs.build_sha }}","target_tag":"stable"} | |
| ] | |
| cosign_identity_regexp: ^https://github\.com/projectbluefin/dakota/\.github/workflows/publish\.yml@refs/heads/(testing|gh-readonly-queue/testing/.+)$ | |
| fast_forward_branch: main | |
| fast_forward_sha: ${{ needs.freshness-check.outputs.build_sha }} | |
| secrets: inherit | |
| release-notes: | |
| needs: [freshness-check, execute] | |
| if: always() && needs.execute.result == 'success' | |
| permissions: | |
| actions: read | |
| contents: write | |
| id-token: write | |
| packages: read | |
| uses: projectbluefin/actions/.github/workflows/reusable-release.yml@v1 | |
| with: | |
| stream_name: stable | |
| image: ghcr.io/projectbluefin/dakota | |
| # Use the BST-native SBOM from publish.yml's publish-sbom job. | |
| # Inline syft with rpm cataloger returns 0 packages on BST images. | |
| build_workflow: publish.yml | |
| build_branch: testing | |
| sbom_artifact: sbom-dakota | |
| checkout_ref: testing | |
| project_name: Dakota | |
| badge_label: Stable | |
| accent_color: "#0ea5e9" | |
| cert_identity_regexp: >- | |
| ^https://github\.com/projectbluefin/dakota/\.github/workflows/publish\.yml@refs/heads/(testing|gh-readonly-queue/testing/.+)$ | |
| notable_packages: >- | |
| [ | |
| {"sbom_name": "linux", "label": "Kernel"}, | |
| {"sbom_name": "gnome-shell", "label": "GNOME Shell"}, | |
| {"sbom_name": "flatpak", "label": "Flatpak"}, | |
| {"sbom_name": "bootc", "label": "bootc"} | |
| ] | |
| docs_url: https://docs.projectbluefin.io/changelogs | |
| secrets: inherit | |
| post-release-variants: | |
| needs: [execute, release-notes] | |
| if: always() && needs.release-notes.result == 'success' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| packages: read | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| - name: Resolve promoted digests | |
| id: digests | |
| run: | | |
| set -euo pipefail | |
| for image in dakota dakota-nvidia; do | |
| digest=$(skopeo inspect --no-tags "docker://ghcr.io/projectbluefin/${image}:stable" \ | |
| | jq -r '.Digest' | cut -c1-19) | |
| echo "${image}=${digest}" >> "$GITHUB_OUTPUT" | |
| done | |
| - name: Prepend variants table to release | |
| env: | |
| DAKOTA_DIGEST: ${{ steps.digests.outputs.dakota }} | |
| NVIDIA_DIGEST: ${{ steps.digests.outputs.dakota-nvidia }} | |
| run: | | |
| set -euo pipefail | |
| TAG=$(gh release list --repo "${{ github.repository }}" --limit 1 --json tagName --jq '.[0].tagName') | |
| CURRENT=$(gh release view "$TAG" --repo "${{ github.repository }}" --json body --jq '.body') | |
| VARIANTS_SECTION=$(printf '%s\n' \ | |
| '## Variants promoted' \ | |
| '' \ | |
| '| Variant | Tag | Digest |' \ | |
| '|---|---|---|' \ | |
| "| \`dakota\` | \`:stable\` | \`${DAKOTA_DIGEST}\` |" \ | |
| "| \`dakota-nvidia\` | \`:stable\` | \`${NVIDIA_DIGEST}\` |" \ | |
| '' \ | |
| '---' \ | |
| '') | |
| printf '%s\n%s' "$VARIANTS_SECTION" "$CURRENT" > /tmp/updated-body.md | |
| gh release edit "$TAG" --repo "${{ github.repository }}" --notes-file /tmp/updated-body.md | |
| # ── Multi-arch manifest (aarch64) ──────────────────────────────────────── | |
| # Non-gating: creates a multi-arch :stable manifest if a SHA-pinned aarch64 | |
| # image exists in GHCR for this build. Skips silently if aarch64 is absent — | |
| # x86_64 stable is already live. ARM never gates this release. | |
| # | |
| # Supply-chain note: both the x86_64 :stable image and the SHA-pinned | |
| # aarch64 image are cosign-verified against the anchored publish.yml | |
| # identity BEFORE the manifest is constructed. The aarch64 component is | |
| # pinned to the digest reported by skopeo for `:aarch64-${BUILD_SHA}` so | |
| # the manifest publishes exactly what was verified, not whatever the | |
| # floating `:aarch64` tag happens to point at. | |
| create-multiarch-stable: | |
| needs: [freshness-check, execute] | |
| if: always() && needs.execute.result == 'success' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| packages: write | |
| env: | |
| BUILD_SHA: ${{ needs.freshness-check.outputs.build_sha }} | |
| COSIGN_IDENTITY_REGEXP: ^https://github\.com/projectbluefin/dakota/\.github/workflows/publish\.yml@refs/heads/(testing|gh-readonly-queue/testing/.+)$ | |
| COSIGN_ISSUER: https://token.actions.githubusercontent.com | |
| steps: | |
| - name: Login to GHCR | |
| env: | |
| GH_ACTOR: ${{ github.actor }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: echo "$GH_TOKEN" | podman login ghcr.io --username "$GH_ACTOR" --password-stdin | |
| - name: Check whether SHA-pinned :aarch64 exists | |
| id: check-aarch64 | |
| run: | | |
| set -euo pipefail | |
| if skopeo inspect --no-tags "docker://ghcr.io/projectbluefin/dakota:aarch64-${BUILD_SHA}" \ | |
| > /dev/null 2>&1; then | |
| echo "present=true" >> "$GITHUB_OUTPUT" | |
| echo "aarch64-${BUILD_SHA} image found — will create multi-arch manifest" | |
| else | |
| echo "present=false" >> "$GITHUB_OUTPUT" | |
| echo "aarch64-${BUILD_SHA} image not found — skipping multi-arch manifest" | |
| fi | |
| - name: Install cosign | |
| if: steps.check-aarch64.outputs.present == 'true' | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 \ | |
| -o "$RUNNER_TEMP/cosign" | |
| sudo install -m 0755 "$RUNNER_TEMP/cosign" /usr/local/bin/cosign | |
| cosign version | |
| - name: Resolve SHA-pinned aarch64 digest | |
| if: steps.check-aarch64.outputs.present == 'true' | |
| id: arm-digest | |
| run: | | |
| set -euo pipefail | |
| AARCH64_DIGEST=$(skopeo inspect --no-tags \ | |
| "docker://ghcr.io/projectbluefin/dakota:aarch64-${BUILD_SHA}" \ | |
| | jq -r '.Digest') | |
| if [ -z "$AARCH64_DIGEST" ] || [ "$AARCH64_DIGEST" = "null" ]; then | |
| echo "::error::Failed to resolve digest for aarch64-${BUILD_SHA}" | |
| exit 1 | |
| fi | |
| echo "digest=${AARCH64_DIGEST}" >> "$GITHUB_OUTPUT" | |
| echo "Resolved aarch64-${BUILD_SHA} → ${AARCH64_DIGEST}" | |
| - name: Verify cosign signature on :stable (x86_64) | |
| if: steps.check-aarch64.outputs.present == 'true' | |
| run: | | |
| set -euo pipefail | |
| cosign verify \ | |
| --certificate-identity-regexp "$COSIGN_IDENTITY_REGEXP" \ | |
| --certificate-oidc-issuer "$COSIGN_ISSUER" \ | |
| "ghcr.io/projectbluefin/dakota:stable" | |
| - name: Verify cosign signature on SHA-pinned aarch64 by digest | |
| if: steps.check-aarch64.outputs.present == 'true' | |
| env: | |
| AARCH64_DIGEST: ${{ steps.arm-digest.outputs.digest }} | |
| run: | | |
| set -euo pipefail | |
| cosign verify \ | |
| --certificate-identity-regexp "$COSIGN_IDENTITY_REGEXP" \ | |
| --certificate-oidc-issuer "$COSIGN_ISSUER" \ | |
| "ghcr.io/projectbluefin/dakota@${AARCH64_DIGEST}" | |
| - name: Create multi-arch :stable manifest | |
| if: steps.check-aarch64.outputs.present == 'true' | |
| env: | |
| AARCH64_DIGEST: ${{ steps.arm-digest.outputs.digest }} | |
| run: | | |
| set -euo pipefail | |
| # Compose the manifest list from the just-verified images. The | |
| # aarch64 component is referenced by its content digest (not the | |
| # floating :aarch64 tag) so we publish exactly what cosign signed. | |
| podman manifest create \ | |
| "ghcr.io/projectbluefin/dakota:stable-multiarch" \ | |
| "ghcr.io/projectbluefin/dakota:stable" \ | |
| "ghcr.io/projectbluefin/dakota@${AARCH64_DIGEST}" | |
| podman manifest push --all \ | |
| "ghcr.io/projectbluefin/dakota:stable-multiarch" \ | |
| "docker://ghcr.io/projectbluefin/dakota:stable-multiarch" | |
| echo "Multi-arch stable manifest pushed as :stable-multiarch (aarch64=${AARCH64_DIGEST})" | |
| post-release-verify: | |
| name: Post-release verification + cleanup | |
| if: inputs.dry_run != true && needs.execute.result == 'success' && needs.create-multiarch-stable.result == 'success' | |
| needs: [freshness-check, execute, create-multiarch-stable] | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| BUILD_SHA: ${{ needs.freshness-check.outputs.build_sha }} | |
| IMAGE: ghcr.io/projectbluefin/dakota | |
| steps: | |
| - name: Verify main bookmark matches promoted SHA | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| main_sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/refs/heads/main" --jq .object.sha) | |
| if [ "$main_sha" != "$BUILD_SHA" ]; then | |
| echo "::error::main bookmark is at $main_sha but promoted SHA was $BUILD_SHA" | |
| exit 1 | |
| fi | |
| echo "main bookmark verified: $main_sha" | |
| - name: Verify :stable points to promoted digest | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update -qq && sudo apt-get install -y -qq skopeo | |
| promoted_digest=$(skopeo inspect --no-tags "docker://${IMAGE}:${BUILD_SHA}" | jq -r .Digest) | |
| stable_digest=$(skopeo inspect --no-tags "docker://${IMAGE}:stable" | jq -r .Digest) | |
| if [ "$stable_digest" != "$promoted_digest" ]; then | |
| echo "::error::stable digest=$stable_digest, expected $promoted_digest" | |
| exit 1 | |
| fi | |
| echo "stable digest verified: $stable_digest" | |
| - name: Verify :stable-multiarch exists | |
| run: | | |
| set -euo pipefail | |
| skopeo inspect --no-tags --raw "docker://${IMAGE}:stable-multiarch" > /dev/null \ | |
| || { echo "::error::stable-multiarch tag missing"; exit 1; } | |
| echo "stable-multiarch present" | |
| - name: Detect stale auto/* promotion branches | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| # OCI-native flow does not create auto/* promotion branches. Detect any leftover. | |
| stale=$(gh api "repos/${GITHUB_REPOSITORY}/branches" --paginate --jq '.[].name' \ | |
| | grep -E '^auto/promote-' || true) | |
| if [ -n "$stale" ]; then | |
| echo "::warning::stale promotion branches present (expected none under OCI-native flow):" | |
| echo "$stale" | |
| else | |
| echo "no stale promotion branches" | |
| fi | |
| - name: Prune untagged GHCR versions for dakota | |
| uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0 | |
| with: | |
| package-name: dakota | |
| package-type: container | |
| min-versions-to-keep: 100 | |
| delete-only-untagged-versions: 'true' | |
| continue-on-error: true | |
| - name: Prune untagged GHCR versions for dakota-nvidia | |
| uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0 | |
| with: | |
| package-name: dakota-nvidia | |
| package-type: container | |
| min-versions-to-keep: 100 | |
| delete-only-untagged-versions: 'true' | |
| continue-on-error: true | |
| - name: Summary — ready for next cycle | |
| run: | | |
| { | |
| echo "## Post-release verification" | |
| echo "" | |
| echo "- main bookmark: $BUILD_SHA" | |
| echo "- :stable digest: verified" | |
| echo "- :stable-multiarch: present" | |
| echo "- stale auto/* branches: clean" | |
| echo "- untagged GHCR versions: pruned" | |
| echo "" | |
| echo "Repo is ready for the next promotion cycle." | |
| } >> "$GITHUB_STEP_SUMMARY" |