Execute Release #366
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 | |
| skip_release_gate: | |
| description: 'Emergency recovery only — skip testsuite release gates' | |
| type: boolean | |
| default: false | |
| promote_sha: | |
| description: 'Recovery only — promote a specific SHA instead of the current testing HEAD (bypasses SHA mismatch guard)' | |
| required: false | |
| type: string | |
| default: '' | |
| 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 || steps.gate.outputs.should-release }} | |
| build_sha: ${{ steps.gate.outputs.build_sha }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Resolve build SHA and release gate | |
| id: gate | |
| 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. | |
| # promote_sha overrides SHA resolution (recovery: promotes a specific build | |
| # even when testing has advanced past it due to workflow-only commits). | |
| if [[ -n "${{ inputs.promote_sha }}" ]]; then | |
| BUILD_SHA="${{ inputs.promote_sha }}" | |
| echo "Recovery mode: promoting specified SHA ${BUILD_SHA}" | |
| elif [ "${{ 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 | |
| # 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. | |
| # Bypassed when promote_sha is specified (recovery: explicit SHA override). | |
| if [ "${{ github.event_name }}" = "workflow_run" ] && [[ -z "${{ inputs.promote_sha }}" ]]; 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 | |
| echo "build_sha=${BUILD_SHA}" >> "$GITHUB_OUTPUT" | |
| echo "proceed=true" >> "$GITHUB_OUTPUT" | |
| - name: Resolve published digest | |
| if: steps.gate.outputs.proceed == 'true' | |
| id: digest | |
| uses: ./.github/actions/resolve-image-digest | |
| with: | |
| artifact-name: digest-default | |
| image: ghcr.io/projectbluefin/dakota | |
| build-sha: ${{ steps.gate.outputs.build_sha }} | |
| registry-creds: ${{ github.actor }}:${{ secrets.GITHUB_TOKEN }} | |
| run-id: ${{ github.event.workflow_run.id || '' }} | |
| verify: 'true' | |
| - name: Compare against :stable | |
| if: steps.gate.outputs.proceed == 'true' | |
| id: compare | |
| env: | |
| GH_ACTOR: ${{ github.actor }} | |
| GH_TOKEN: ${{ github.token }} | |
| BUILD_SHA: ${{ steps.gate.outputs.build_sha }} | |
| TESTING_DIGEST: ${{ steps.digest.outputs.digest }} | |
| run: | | |
| set -euo pipefail | |
| IMAGE="ghcr.io/projectbluefin/dakota" | |
| STABLE_DIGEST="" | |
| if skopeo inspect --no-tags --creds "${GH_ACTOR}:${GH_TOKEN}" \ | |
| "docker://${IMAGE}:stable" > /dev/null 2>&1; then | |
| STABLE_DIGEST=$(skopeo inspect --no-tags --creds "${GH_ACTOR}:${GH_TOKEN}" \ | |
| "docker://${IMAGE}: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} ($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"}, | |
| {"image":"dakota-gaming","source_tag":"${{ needs.freshness-check.outputs.build_sha }}","target_tag":"stable"}, | |
| {"image":"dakota-nvidia-gaming","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 omitted: handled by update-main-bookmark job below, | |
| # which force-updates main to the promoted SHA. The reusable action's | |
| # force=false fast-forward fails when main diverges from testing (e.g. | |
| # when commits land on main via direct merge, bypassing the testing flow). | |
| fast_forward_sha: ${{ needs.freshness-check.outputs.build_sha }} | |
| run_release_gate: ${{ inputs.skip_release_gate != true }} | |
| secrets: inherit | |
| # Update the stable bookmark (main) to point to the promoted SHA. | |
| # Runs as a separate job with force=true to handle the case where main has | |
| # diverged from testing (e.g. commits landed on main directly, bypassing | |
| # the testing→stable promotion flow). The reusable action's force=false | |
| # fast-forward would reject this; we handle it correctly here instead. | |
| update-main-bookmark: | |
| needs: [freshness-check, execute] | |
| if: always() && needs.execute.result == 'success' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| - name: Force-update main to promoted SHA | |
| env: | |
| TARGET_SHA: ${{ needs.freshness-check.outputs.build_sha }} | |
| run: | | |
| set -euo pipefail | |
| compare=$(gh api \ | |
| "repos/${{ github.repository }}/compare/${TARGET_SHA}...main" \ | |
| --jq '.status' 2>/dev/null || echo "unknown") | |
| echo "main vs ${TARGET_SHA}: ${compare}" | |
| if [ "$compare" = 'identical' ]; then | |
| echo "main is already at ${TARGET_SHA}. Nothing to do." | |
| exit 0 | |
| fi | |
| gh api "repos/${{ github.repository }}/git/refs/heads/main" \ | |
| --method PATCH \ | |
| --field sha="$TARGET_SHA" \ | |
| --field force=true | |
| echo "Updated main → ${TARGET_SHA}" | |
| 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 dakota-gaming dakota-nvidia-gaming; 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 }} | |
| GAMING_DIGEST: ${{ steps.digests.outputs.dakota-gaming }} | |
| NVIDIA_GAMING_DIGEST: ${{ steps.digests.outputs.dakota-nvidia-gaming }} | |
| 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}\` |" \ | |
| "| \`dakota-gaming\` | \`:stable\` | \`${GAMING_DIGEST}\` |" \ | |
| "| \`dakota-nvidia-gaming\` | \`:stable\` | \`${NVIDIA_GAMING_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 | |
| for image in dakota dakota-nvidia dakota-gaming dakota-nvidia-gaming; do | |
| promoted_digest=$(skopeo inspect --no-tags \ | |
| "docker://ghcr.io/projectbluefin/${image}:${BUILD_SHA}" | jq -r .Digest) | |
| stable_digest=$(skopeo inspect --no-tags \ | |
| "docker://ghcr.io/projectbluefin/${image}:stable" | jq -r .Digest) | |
| if [ "$stable_digest" != "$promoted_digest" ]; then | |
| echo "::error::${image}:stable digest=$stable_digest, expected $promoted_digest" | |
| exit 1 | |
| fi | |
| echo "${image}:stable digest verified: $stable_digest" | |
| done | |
| - 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: Prune untagged GHCR versions for dakota-gaming | |
| uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0 | |
| with: | |
| package-name: dakota-gaming | |
| 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-gaming | |
| uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0 | |
| with: | |
| package-name: dakota-nvidia-gaming | |
| 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" |