Skip to content

Execute Release

Execute Release #366

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"