Skip to content

Release

Release #612

Workflow file for this run

name: Release
# Cuts a release when a merge to main bumped the root package version — i.e.
# when the Version PR (maintained by version-pr.yml) has been merged. Triggered
# after CI/CD succeeds so the Docker images for the commit already exist.
#
# A release: tags vX.Y.Z at the version-bump commit, creates the GitHub Release
# from that version's CHANGELOG.md section (flagging schema migrations), retags
# the CI-built images (X.Y.Z, X.Y, latest), and starts the deploy chain
# (staging automatically, production after environment approval).
#
# Versioning contract: docs/admin/compatibility-policy.mdx
on:
workflow_run:
workflows: ["CI/CD"]
types: [completed]
branches: [main]
# Serialize promotion of the moving series and latest tags.
concurrency:
group: release
cancel-in-progress: false
permissions: {}
jobs:
release:
# Only real pushes to our own main — a fork PR's CI/CD run reports
# head_branch of the fork; without these guards a fork branch named "main"
# could drive the privileged release job.
if: >-
${{ github.event.workflow_run.conclusion == 'success'
&& github.event.workflow_run.event == 'push'
&& github.event.workflow_run.head_repository.full_name == github.repository }}
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
released: ${{ steps.cut.outputs.released }}
version: ${{ steps.cut.outputs.version }}
major: ${{ steps.cut.outputs.major }}
minor: ${{ steps.cut.outputs.minor }}
tag_name: ${{ steps.cut.outputs.tag_name }}
sha: ${{ steps.cut.outputs.sha }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Pin to the exact commit CI/CD built — never main's tip, which may
# already carry a newer version bump (that would skip this release).
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
- name: Cut release if this commit bumped the version
id: cut
# GITHUB_TOKEN suffices: the tag/Release it creates is not meant to
# trigger any workflow (deploys are wired via `needs`/dispatch below).
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
VERSION=$(git show "$SHA:package.json" | jq -r .version)
PARENT_VERSION=$(git show "${SHA}^:package.json" | jq -r .version)
TAG="v$VERSION"
# Only the Version PR merge changes the root version. Feature merges add
# changesets, not version bumps, so they no-op here.
if [ "$VERSION" = "$PARENT_VERSION" ]; then
echo "Version unchanged at $VERSION — no release to cut."
echo "released=false" >> "$GITHUB_OUTPUT"
exit 0
fi
RESUME=false
if existing=$(gh release view "$TAG" --repo "${{ github.repository }}" --json isDraft,targetCommitish 2>/dev/null); then
if [ "$(jq -r .isDraft <<< "$existing")" != true ]; then
echo "Release $TAG is already published."
echo "released=false" >> "$GITHUB_OUTPUT"
exit 0
fi
[ "$(jq -r .targetCommitish <<< "$existing")" = "$SHA" ] || {
echo "::error::Draft $TAG targets a different commit"; exit 1; }
RESUME=true
fi
echo "Cutting $TAG at $SHA (was $PARENT_VERSION)"
PREV_TAG=$(git tag --list 'v*' --sort=-v:refname | head -1)
# Release notes = this version's CHANGELOG.md section (the assembled
# changeset entries), with a first-class migration warning when the
# release touches the Liquibase changelog.
NOTES=$(mktemp)
if [ -n "$PREV_TAG" ] && ! git diff --quiet "$PREV_TAG" "$SHA" -- server/application/src/main/resources/db/changelog/; then
{
echo "> [!WARNING]"
echo "> This release contains **schema migrations**. They run automatically on startup — back up your database before upgrading. See the [migration guide](https://github.com/ls1intum/Hephaestus/blob/main/MIGRATION.md)."
echo ""
} >> "$NOTES"
fi
# Slice the `## <version>` section: stop at the next release header or
# the footer's `---` fence. Exact-string header compare is regex-safe
# against the dots in the version. (A literal `---` inside a changeset
# body would truncate notes — our entries are short prose, so this is
# accepted over more fragile parsing.)
git show "$SHA:CHANGELOG.md" \
| awk -v ver="## $VERSION" '
$0 == ver { on=1; next }
on && (/^## / || /^---$/) { exit }
on { print }
' >> "$NOTES"
echo "----- release notes -----"; cat "$NOTES"; echo "-------------------------"
if [ "$RESUME" = false ]; then
gh release create "$TAG" --draft \
--title "$TAG" \
--notes-file "$NOTES" \
--target "$SHA" \
--repo "${{ github.repository }}"
fi
{
echo "released=true"
echo "version=$VERSION"
echo "major=${VERSION%%.*}"
MINOR="${VERSION#*.}"; echo "minor=${MINOR%%.*}"
echo "tag_name=$TAG"
echo "sha=$SHA"
} >> "$GITHUB_OUTPUT"
- name: Summary
if: steps.cut.outputs.released == 'true'
run: |
echo "## Preparing ${{ steps.cut.outputs.tag_name }}" >> "$GITHUB_STEP_SUMMARY"
echo "Evidence verification is in progress." >> "$GITHUB_STEP_SUMMARY"
tag-images:
needs: release
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
permissions:
packages: write
id-token: write
attestations: write
contents: write # gh release upload (release image lock)
outputs:
agent-pi-digest: ${{ steps.retag.outputs.agent-pi-digest }}
application-server-digest: ${{ steps.retag.outputs.application-server-digest }}
steps:
- name: Check out the released tree
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.release.outputs.sha }}
fetch-depth: 1
- name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Capture release image digests
id: retag
env:
SHA: ${{ needs.release.outputs.sha }}
run: |
set -euo pipefail
# Authoritative registry-side digest of the manifest list.
manifest_digest() {
docker buildx imagetools inspect "$1" --format '{{json .Manifest}}' | jq -r '.digest'
}
mapfile -t IMAGES < <(jq -er '.images[]' security/release-images.json)
[ "${#IMAGES[@]}" -gt 0 ] || { echo "::error::Release image inventory is empty"; exit 1; }
: > release-images.tsv
# Probe all images first; refuse to partial-publish if any is missing.
for img in "${IMAGES[@]}"; do
FULL_IMAGE="ghcr.io/ls1intum/hephaestus/$img"
for i in $(seq 1 24); do
if docker buildx imagetools inspect "$FULL_IMAGE:$SHA" > /dev/null 2>&1; then break; fi
if [ "$i" -ge 24 ]; then
echo "::error::Image $FULL_IMAGE:$SHA not found after 120 s"
exit 1
fi
sleep 5
done
done
for img in "${IMAGES[@]}"; do
FULL_IMAGE="ghcr.io/ls1intum/hephaestus/$img"
echo "::group::$img"
SRC_DIGEST=$(manifest_digest "$FULL_IMAGE:$SHA")
[[ "$SRC_DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]] || {
echo "::error::$img source digest is malformed: ${SRC_DIGEST:-<empty>}"; exit 1; }
echo "$img source digest: $SRC_DIGEST"
printf '%s\t%s\n' "$img" "$SRC_DIGEST" >> release-images.tsv
case "$img" in
agent-pi) echo "agent-pi-digest=$SRC_DIGEST" >> "$GITHUB_OUTPUT" ;;
application-server) echo "application-server-digest=$SRC_DIGEST" >> "$GITHUB_OUTPUT" ;;
esac
echo "::endgroup::"
done
# The pin asset below tells every production deploy to run THIS agent image, so this is the
# last point at which an image the released server cannot drive is still catchable. The
# sandbox is Bun-only (ADR 0030), so `node` must be absent, not present.
- name: Smoke-test agent-pi
env:
IMAGE: ghcr.io/ls1intum/hephaestus/agent-pi@${{ steps.retag.outputs.agent-pi-digest }}
LAYOUT: server/application/src/main/java/de/tum/cit/aet/hephaestus/agent/runtime/SandboxLayout.java
run: |
set -euo pipefail
docker pull "$IMAGE"
docker run --rm --entrypoint /bin/sh "$IMAGE" -c \
'bun --version && ! command -v node >/dev/null 2>&1'
expected=$(grep -oE 'RUNTIME_CONTRACT_VERSION = [0-9]+' "$LAYOUT" | grep -oE '[0-9]+$')
[ -n "$expected" ] || { echo "::error::Could not read RUNTIME_CONTRACT_VERSION from $LAYOUT"; exit 1; }
declared=$(docker inspect --format '{{index .Config.Labels "hephaestus.agent.runtime-contract"}}' "$IMAGE")
if [ "$declared" != "$expected" ]; then
echo "::error::agent-pi implements runtime contract '${declared:-<none>}' but the released server stages for v${expected} — refusing to publish a pin for an unmatched pair."
exit 1
fi
echo "agent-pi implements runtime contract v${expected}"
- uses: ./.github/actions/setup-release-security-tools
- uses: ./.github/actions/setup-bun
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Generate and enforce release evidence
env:
TRIVY_USERNAME: ${{ github.actor }}
TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
mkdir -p evidence
: > release-platforms.tsv
started=$SECONDS
trivy image --download-db-only
db_metadata="${TRIVY_CACHE_DIR:-$HOME/.cache/trivy}/db/metadata.json"
jq -e '(.UpdatedAt | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) as $updated |
((now - $updated) >= 0) and ((now - $updated) <= 86400)' \
"$db_metadata" >/dev/null
cp "$db_metadata" evidence/trivy-db.json
jq -n --arg syft "$(syft version -o json | jq -r .version)" \
--arg trivy "$(trivy version --format json | jq -r .Version)" \
--arg cosign "$(cosign version --json | jq -r .gitVersion)" \
'{syft: $syft, trivy: $trivy, cosign: $cosign}' > evidence/tool-versions.json
generate_evidence() {
local image=$1 repository=$2 digest=$3 provenance=$4
local ref="$repository@$digest"
for platform in linux/amd64 linux/arm64; do
suffix=${platform//\//-}
architecture=${platform#*/}
platform_digest=$(docker buildx imagetools inspect "$ref" --raw | jq -er \
--arg architecture "$architecture" \
'.manifests[] | select(.platform.os == "linux" and .platform.architecture == $architecture) | .digest')
[[ "$platform_digest" =~ ^sha256:[a-f0-9]{64}$ ]] || { echo "::error::$image $platform digest is malformed"; exit 1; }
platform_ref="$repository@$platform_digest"
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$image" "$platform" "$digest" "$platform_digest" "$repository" "$provenance" >> release-platforms.tsv
syft "$platform_ref" --scope squashed \
-o "syft-json=evidence/$image-$suffix.syft.json" \
-o "spdx-json=evidence/$image-$suffix.spdx.json" \
-o "cyclonedx-json=evidence/$image-$suffix.cdx.json"
bun scripts/check-release-sbom.ts \
"evidence/$image-$suffix.syft.json" \
"evidence/$image-$suffix.spdx.json" \
"evidence/$image-$suffix.cdx.json" \
"$platform_digest" "$platform" "evidence/$image-$suffix.sbom-summary.json"
trivy image --scanners vuln --format json --output "evidence/$image-$suffix.trivy.json" "$platform_ref"
bun scripts/check-release-vulnerabilities.ts "$image" "evidence/$image-$suffix.trivy.json" security/vulnerability-policy.json "evidence/$image-$suffix.policy.json"
if [ "$provenance" = first-party ]; then
cosign attest --yes --type spdxjson --predicate "evidence/$image-$suffix.spdx.json" "$platform_ref"
fi
done
}
while IFS=$'\t' read -r image digest; do
generate_evidence "$image" "ghcr.io/ls1intum/hephaestus/$image" "$digest" first-party
done < release-images.tsv
jq -r '.upstream[] | [.name, .repository, .digest] | @tsv' security/release-images.json |
while IFS=$'\t' read -r image repository digest; do
generate_evidence "$image" "$repository" "$digest" upstream
done
duration=$((SECONDS - started))
jq -n \
--arg schemaVersion "1" \
--arg generatedAt "$(date -u +%FT%TZ)" \
--arg release "${{ needs.release.outputs.tag_name }}" \
--arg commit "${{ needs.release.outputs.sha }}" \
--argjson durationSeconds "$duration" \
--rawfile subjects release-platforms.tsv \
'{schemaVersion: ($schemaVersion | tonumber), release: $release, commit: $commit,
generatedAt: $generatedAt, durationSeconds: $durationSeconds,
subjects: ($subjects | split("\n") | map(select(length > 0) | split("\t") |
{image: .[0], platform: .[1], indexDigest: .[2], digest: .[3], repository: .[4], provenance: .[5]}))}' \
> evidence/manifest.json
cp security/vulnerability-policy.json evidence/vulnerability-policy.json
cp security/release-images.json evidence/release-images.json
(cd evidence && sha256sum -- * > SHA256SUMS)
- name: Verify evidence from registry subjects
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
while IFS=$'\t' read -r image platform _ digest repository provenance; do
[ "$provenance" = first-party ] || continue
ref="$repository@$digest"
suffix=${platform//\//-}
cosign verify-attestation \
--type spdxjson \
--certificate-identity 'https://github.com/${{ github.repository }}/.github/workflows/release.yml@refs/heads/main' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"$ref" > verified-attestations.json
jq -e --slurpfile sbom "evidence/$image-$suffix.spdx.json" \
'any(.[]; (.payload | @base64d | fromjson | .predicate) == $sbom[0])' \
verified-attestations.json >/dev/null
done < release-platforms.tsv
while IFS=$'\t' read -r image digest; do
ref="ghcr.io/ls1intum/hephaestus/$image@$digest"
cosign verify "$ref" \
--certificate-identity 'https://github.com/${{ github.repository }}/.github/workflows/reusable-docker-build.yml@refs/heads/main' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
--certificate-github-workflow-repository '${{ github.repository }}' >/dev/null
gh attestation verify "oci://$ref" --owner "${{ github.repository_owner }}" \
--signer-workflow '${{ github.repository }}/.github/workflows/reusable-docker-build.yml' >/dev/null
done < release-images.tsv
- name: Write release image lock
id: pin
env:
VERSION: ${{ needs.release.outputs.version }}
run: |
set -euo pipefail
ASSET="release-v${VERSION}.json"
jq '{schemaVersion, release, commit,
images: [.subjects[]]
| group_by(.image)
| map({image: .[0].image, repository: .[0].repository, provenance: .[0].provenance,
indexDigest: .[0].indexDigest,
platforms: (map({key: .platform, value: .digest}) | from_entries)})}' \
evidence/manifest.json > "$ASSET"
bun scripts/release-image-lock.ts "$ASSET" evidence/manifest.json \
"v${VERSION}" /tmp/release-lock.env
echo "asset-path=$ASSET" >> "$GITHUB_OUTPUT"
jq . "$ASSET"
- name: Generate subject checksums
id: subjects
env:
ASSET: ${{ steps.pin.outputs.asset-path }}
run: |
set -euo pipefail
while IFS=$'\t' read -r image digest; do
printf '%s %s\n' "${digest#sha256:}" "$image"
done < release-images.tsv > subjects.sha256
sha256sum "$ASSET" >> subjects.sha256
cat subjects.sha256
- name: Attest release image lock
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-checksums: subjects.sha256
predicate-type: https://in-toto.io/attestation/release/v0.1
predicate: |
{ "purl": "pkg:github/ls1intum/hephaestus@${{ needs.release.outputs.tag_name }}" }
- name: Sign release image lock
env:
ASSET: ${{ steps.pin.outputs.asset-path }}
run: |
set -euo pipefail
cosign sign-blob --yes --bundle "${ASSET}.sigstore.json" "$ASSET"
- name: Verify signature with the deploy-side identity (fail fast)
env:
ASSET: ${{ steps.pin.outputs.asset-path }}
run: |
set -euo pipefail
cosign verify-blob \
--bundle "${ASSET}.sigstore.json" \
--certificate-identity 'https://github.com/ls1intum/Hephaestus/.github/workflows/release.yml@refs/heads/main' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"$ASSET"
- name: Upload release image lock to GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ needs.release.outputs.tag_name }}
ASSET: ${{ steps.pin.outputs.asset-path }}
run: |
set -euo pipefail
gh release upload "$TAG_NAME" \
"$ASSET" "${ASSET}.sigstore.json" evidence/* \
--repo "${{ github.repository }}"
publish-release:
needs: [release, tag-images]
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
attestations: read
steps:
- name: Checkout release verifier
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.release.outputs.sha }}
fetch-depth: 1
persist-credentials: false
- uses: ./.github/actions/setup-bun
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Verify durable assets from a clean environment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ needs.release.outputs.tag_name }}
run: |
set -euo pipefail
mkdir evidence
gh release download "$TAG_NAME" --repo "${{ github.repository }}" --dir evidence
(cd evidence && sha256sum -c SHA256SUMS)
for file in manifest.json release-images.json tool-versions.json trivy-db.json vulnerability-policy.json; do
test -s "evidence/$file"
done
lock="release-${TAG_NAME}.json"
cosign verify-blob \
--bundle "evidence/$lock.sigstore.json" \
--certificate-identity 'https://github.com/ls1intum/Hephaestus/.github/workflows/release.yml@refs/heads/main' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"evidence/$lock"
bun scripts/release-image-lock.ts "evidence/$lock" evidence/manifest.json \
"$TAG_NAME" /tmp/release-lock.env
jq -e --slurpfile inventory evidence/release-images.json \
'.schemaVersion == 1 and
([.subjects[].image] | unique == (($inventory[0].images + [$inventory[0].upstream[].name]) | sort)) and
(group_by(.image) | all(.[]; [.[].platform] == ["linux/amd64", "linux/arm64"])) and
all(.subjects[]; (.digest | test("^sha256:[a-f0-9]{64}$")) and (.indexDigest | test("^sha256:[a-f0-9]{64}$")))' \
evidence/manifest.json >/dev/null
jq -er '.subjects[] | [.image, .platform, .digest, .repository, .provenance] | @tsv' evidence/manifest.json |
while IFS=$'\t' read -r image platform digest repository provenance; do
suffix=${platform//\//-}
bun scripts/check-release-sbom.ts \
"evidence/$image-$suffix.syft.json" \
"evidence/$image-$suffix.spdx.json" \
"evidence/$image-$suffix.cdx.json" \
"$digest" "$platform" /tmp/sbom-summary.json
cmp "evidence/$image-$suffix.sbom-summary.json" /tmp/sbom-summary.json
for kind in trivy policy; do
test -s "evidence/$image-$suffix.$kind.json"
done
if [ "$provenance" = first-party ]; then
cosign verify-attestation --type spdxjson \
--certificate-identity 'https://github.com/${{ github.repository }}/.github/workflows/release.yml@refs/heads/main' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"$repository@$digest" > verified-attestations.json
jq -e --slurpfile sbom "evidence/$image-$suffix.spdx.json" \
'any(.[]; (.payload | @base64d | fromjson | .predicate) == $sbom[0])' \
verified-attestations.json >/dev/null
fi
index_digest=$(jq -er --arg image "$image" --arg platform "$platform" \
'.subjects[] | select(.image == $image and .platform == $platform) | .indexDigest' evidence/manifest.json)
architecture=${platform#*/}
docker buildx imagetools inspect "$repository@$index_digest" --raw |
jq -e --arg architecture "$architecture" --arg digest "$digest" \
'any(.manifests[]; .platform.os == "linux" and .platform.architecture == $architecture and .digest == $digest)' >/dev/null
done
jq -er '[.subjects[] | select(.provenance == "first-party") | [.repository, .indexDigest]] | unique[] | @tsv' evidence/manifest.json |
while IFS=$'\t' read -r repository digest; do
ref="$repository@$digest"
cosign verify "$ref" \
--certificate-identity 'https://github.com/${{ github.repository }}/.github/workflows/reusable-docker-build.yml@refs/heads/main' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
--certificate-github-workflow-repository '${{ github.repository }}' >/dev/null
gh attestation verify "oci://$ref" --owner "${{ github.repository_owner }}" \
--signer-workflow '${{ github.repository }}/.github/workflows/reusable-docker-build.yml' >/dev/null
done
asset="evidence/release-${TAG_NAME}.json"
cosign verify-blob --bundle "${asset}.sigstore.json" \
--certificate-identity 'https://github.com/ls1intum/Hephaestus/.github/workflows/release.yml@refs/heads/main' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' "$asset" >/dev/null
- name: Promote verified image digests
env:
VERSION: ${{ needs.release.outputs.version }}
SERIES: ${{ needs.release.outputs.major }}.${{ needs.release.outputs.minor }}
run: |
set -euo pipefail
jq -er '[.subjects[] | select(.provenance == "first-party") | [.repository, .indexDigest]] | unique[] | @tsv' evidence/manifest.json |
while IFS=$'\t' read -r ref digest; do
docker buildx imagetools create -t "$ref:$VERSION" "$ref@$digest"
promoted=$(docker buildx imagetools inspect "$ref:$VERSION" --format '{{json .Manifest}}' | jq -r .digest)
[ "$promoted" = "$digest" ] || { echo "::error::Promotion changed $ref:$VERSION digest"; exit 1; }
done
jq -er '[.subjects[] | select(.provenance == "first-party") | [.repository, .indexDigest]] | unique[] | @tsv' evidence/manifest.json |
while IFS=$'\t' read -r ref digest; do
docker buildx imagetools create -t "$ref:$SERIES" -t "$ref:latest" "$ref@$digest"
for tag in "$SERIES" latest; do
promoted=$(docker buildx imagetools inspect "$ref:$tag" --format '{{json .Manifest}}' | jq -r .digest)
[ "$promoted" = "$digest" ] || { echo "::error::Promotion changed $ref:$tag digest"; exit 1; }
done
done
- name: Publish release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ needs.release.outputs.tag_name }}
run: |
set -euo pipefail
gh release edit "$TAG_NAME" --repo "${{ github.repository }}" --draft=false
[ "$(gh release view "$TAG_NAME" --repo "${{ github.repository }}" --json isImmutable --jq .isImmutable)" = true ] || {
echo "::error::Repository immutable releases must be enabled before publishing"; exit 1; }
deploy-staging:
needs: [release, tag-images, publish-release]
if: needs.release.outputs.released == 'true'
uses: ./.github/workflows/deploy-staging.yml
permissions:
contents: read
packages: read
with:
image-tag: ${{ needs.release.outputs.tag_name }}
secrets: inherit
deploy-production:
needs: [release, deploy-staging]
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
steps:
- name: Trigger Production Deploy
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GH_PAT }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'deploy-prod.yml',
ref: 'main',
inputs: {
'image-tag': '${{ needs.release.outputs.tag_name }}'
}
});
console.log('✅ Production deployment triggered for ${{ needs.release.outputs.tag_name }}');
- name: Summary
run: |
echo "## 🚀 Production Deployment Triggered" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Version **${{ needs.release.outputs.tag_name }}** deployment to production has been triggered." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "⏳ Waiting for approval in the [Deploy to Production](https://github.com/${{ github.repository }}/actions/workflows/deploy-prod.yml) workflow." >> $GITHUB_STEP_SUMMARY