[codex] create release tags with dedicated App (#2186)

This commit is contained in:
Jordan Mecom
2026-07-20 22:12:14 +00:00
committed by GitHub
parent 9ed843a0ac
commit 0b297edbdf
5 changed files with 67 additions and 93 deletions
@@ -4,22 +4,18 @@ name: Auto-tag on Release PR Merge
# prefix; the main chart lane also auto-detects a Chart.yaml version bump so
# a chart feature PR can publish its own new version when merged:
#
# version-bump/<v> → tag v<v> → dispatch release.yml (desktop app)
# relay-release/<v> → tag relay-v<v> → dispatch docker.yml (relay image)
# chart-release/<v> → tag chart-v<v> → dispatch helm-chart.yml (main helm chart)
# push-chart-release/<v> → tag push-chart-v<v> → dispatch push-gateway-helm-chart.yml
# version-bump/<v> → tag v<v> → release.yml (desktop app)
# relay-release/<v> → tag relay-v<v> → docker.yml (relay image)
# chart-release/<v> → tag chart-v<v> → helm-chart.yml (main helm chart)
# push-chart-release/<v> → tag push-chart-v<v> → push-gateway-helm-chart.yml
# any internal PR that bumps deploy/charts/buzz/Chart.yaml `version`
# → tag chart-v<v> → dispatch helm-chart.yml (helm chart)
# → tag chart-v<v> → helm-chart.yml (helm chart)
# mobile-release/<v> → tag mobile-v<v> → (manual sprout_ref for buzz-releases build — see below)
#
# The desktop, relay, and chart lanes dispatch their build workflow rather than
# relying on the consumer's `on.push.tags` trigger: auto-tag pushes the tag
# with the default GITHUB_TOKEN, which GitHub's recursion guard blocks from
# firing any `on: push` trigger. So release.yml, docker.yml, and helm-chart.yml
# all have a `push.tags` trigger that is dead for auto-pushed tags — the
# dispatch is the real path. Each dispatch passes the bare version and the tag
# ref so the consumer builds the tagged commit (github.ref on a dispatch is
# `main`).
# Release tags are created with a short-lived token from the dedicated
# buzz-release-bot GitHub App. GitHub attributes the ref creation to that
# App, so the consumer's `on.push.tags` trigger runs normally. The workflow's
# default GITHUB_TOKEN remains read-only and is never used to create a tag.
#
# The mobile lane is push-only by infosec necessity: OSS `block/buzz` CI must
# not trigger CI in the private `buzz-releases` repo, so auto-dispatch across
@@ -33,8 +29,7 @@ on:
branches: [main]
permissions:
contents: write
actions: write
contents: read
jobs:
auto-tag:
@@ -47,6 +42,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 0
persist-credentials: false
- name: Resolve release lane and version
id: release
@@ -59,24 +55,19 @@ jobs:
case "$BRANCH" in
version-bump/*)
VERSION="${BRANCH#version-bump/}"
TAG_PREFIX="v"
DISPATCH="release" ;;
TAG_PREFIX="v" ;;
relay-release/*)
VERSION="${BRANCH#relay-release/}"
TAG_PREFIX="relay-v"
DISPATCH="docker" ;;
TAG_PREFIX="relay-v" ;;
chart-release/*)
VERSION="${BRANCH#chart-release/}"
TAG_PREFIX="chart-v"
DISPATCH="helm-chart" ;;
TAG_PREFIX="chart-v" ;;
push-chart-release/*)
VERSION="${BRANCH#push-chart-release/}"
TAG_PREFIX="push-chart-v"
DISPATCH="push-gateway-helm-chart" ;;
TAG_PREFIX="push-chart-v" ;;
mobile-release/*)
VERSION="${BRANCH#mobile-release/}"
TAG_PREFIX="mobile-v"
DISPATCH="" ;;
TAG_PREFIX="mobile-v" ;;
*)
parent_sha="$(git rev-parse HEAD^)"
old_version="$(git show "${parent_sha}:deploy/charts/buzz/Chart.yaml" 2>/dev/null | awk '/^version:/ {print $2}')"
@@ -86,8 +77,7 @@ jobs:
echo "enabled=false" >> "$GITHUB_OUTPUT"
exit 0
fi
TAG_PREFIX="chart-v"
DISPATCH="helm-chart" ;;
TAG_PREFIX="chart-v" ;;
esac
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
echo "::error::Invalid release version: '$VERSION'"
@@ -95,19 +85,29 @@ jobs:
fi
{
echo "enabled=true"
echo "version=$VERSION"
echo "tag=${TAG_PREFIX}${VERSION}"
echo "dispatch=$DISPATCH"
} >> "$GITHUB_OUTPUT"
echo "Tagging ${TAG_PREFIX}${VERSION}"
- name: Create release tagger token
if: steps.release.outputs.enabled == 'true'
id: release-tagger
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.BUZZ_RELEASE_TAGGER_CLIENT_ID }}
private-key: ${{ secrets.BUZZ_RELEASE_TAGGER_PRIVATE_KEY }}
permission-contents: write
- name: Create and push tag
if: steps.release.outputs.enabled == 'true'
env:
GH_TOKEN: ${{ steps.release-tagger.outputs.token }}
TAG: ${{ steps.release.outputs.tag }}
run: |
EXISTING_SHA="$(git ls-remote --tags origin "refs/tags/$TAG" | awk '{print $1}')"
if [ -n "$EXISTING_SHA" ]; then
set -euo pipefail
EXISTING_REF="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" --jq .ref 2>/dev/null || true)"
if [ -n "$EXISTING_REF" ]; then
EXISTING_SHA="$(gh api "repos/$GITHUB_REPOSITORY/commits/$TAG" --jq .sha)"
if [ "$EXISTING_SHA" = "$GITHUB_SHA" ]; then
echo "Tag $TAG already exists at $GITHUB_SHA — skipping tag creation"
exit 0
@@ -116,40 +116,7 @@ jobs:
exit 1
fi
fi
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
git tag "$TAG"
git push origin "$TAG"
- name: Trigger release build
# The desktop, relay, and chart lanes dispatch their build workflow
# because the consumer's on:push:tags trigger is dead for tags pushed
# by GITHUB_TOKEN (recursion guard — see header comment). Mobile has no
# consumer yet, so dispatch="" skips this step entirely.
if: steps.release.outputs.enabled == 'true' && steps.release.outputs.dispatch != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DISPATCH: ${{ steps.release.outputs.dispatch }}
VERSION: ${{ steps.release.outputs.version }}
TAG: ${{ steps.release.outputs.tag }}
run: |
# Run the workflow itself at the immutable tag so github.sha, OIDC,
# provenance, and the checked-out source all name the release commit.
extra_args=()
case "$DISPATCH" in
release) WORKFLOW="release.yml" ;;
docker) WORKFLOW="docker.yml" ;;
helm-chart)
WORKFLOW="helm-chart.yml"
extra_args=(-f "ref=$TAG") ;;
push-gateway-helm-chart)
WORKFLOW="push-gateway-helm-chart.yml"
extra_args=(-f "ref=$TAG") ;;
*)
echo "::error::Unhandled dispatch target: '$DISPATCH'"
exit 1 ;;
esac
gh workflow run "$WORKFLOW" \
--ref "$TAG" \
-f version="$VERSION" \
"${extra_args[@]}"
gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
-f ref="refs/tags/$TAG" \
-f sha="$GITHUB_SHA" \
--silent
+6 -8
View File
@@ -18,15 +18,13 @@ name: Docker image
# - push tags relay-v*.*.* → :{version} + :{major}.{minor} + :{major}
# (+ :latest for stable, NOT for prereleases)
# - pull_request → build only (no push), cache stays warm
# - workflow_dispatch → relay-tag rescue, dispatched at the tag itself
# - workflow_dispatch → manual relay-tag rescue at the tag itself
#
# Why workflow_dispatch carries a version input:
# auto-tag-on-release-pr-merge.yml pushes relay-v* with the default
# GITHUB_TOKEN, which GitHub deliberately does NOT let fire on:push triggers
# (recursion guard). So the push:tags trigger above never runs for releases.
# auto-tag instead dispatches this workflow at the immutable tag and supplies
# the bare version for metadata-action. The workflow rejects a dispatch whose
# github.ref, checked-out HEAD, and relay-v tag do not resolve to one commit.
# Normal releases arrive through the push:tags trigger above. The input is
# retained only for an operator to rerun publication manually at an immutable
# relay tag. The workflow rejects a dispatch whose github.ref, checked-out
# HEAD, and relay-v tag do not resolve to one commit.
# On the rescue path inputs.version is already bare (e.g. 0.3.0), so the
# match=^relay-v(.*)$ regex simply no-ops (it warns, leaving the value
# intact) and the bare version flows straight to the semver parser. On a
@@ -146,7 +144,7 @@ jobs:
# own — it only strips refs/tags/, then runs the raw ref through
# semver.valid(), which rejects "relay-v0.3.0". The match capture
# group feeds the bare version to the semver parser. value= supplies
# the version on the auto-tag rescue dispatch (github.ref is `main`
# the version on a manual rescue dispatch (github.ref is `main`
# there, not the tag): it is already bare, so match no-ops (warns,
# value intact) and the bare version validates as-is. On push value=
# is empty, so the ref drives it and match strips relay-v — push
+6 -9
View File
@@ -9,14 +9,11 @@ name: helm chart
# `chart-v*` tags publish — `main` pushes and PRs stay lint/render-only so we
# never overwrite a released chart version from an in-progress `main`.
#
# Why workflow_dispatch carries version/ref inputs (same reason as docker.yml):
# auto-tag-on-release-pr-merge.yml pushes `chart-v*` with the default
# GITHUB_TOKEN, which GitHub's recursion guard blocks from firing any
# `on: push` trigger. So the `push.tags` trigger below is dead for
# auto-pushed tags — auto-tag instead dispatches this workflow with the bare
# version + tag ref, and the publish job checks out inputs.ref and packages at
# inputs.version. On a real manual `git push` of a `chart-v*` tag the
# push.tags trigger fires and inputs are empty.
# Why workflow_dispatch carries version/ref inputs:
# Normal releases arrive through the `push.tags` trigger below. The inputs are
# retained only for an operator to rerun publication manually at an immutable
# chart tag; the publish job checks out inputs.ref and packages at
# inputs.version.
on:
workflow_dispatch:
@@ -153,7 +150,7 @@ jobs:
publish:
# Packages the chart and pushes it to GHCR as an OCI artifact. Fires only on
# a `chart-v*` tag push (manual) or the auto-tag rescue dispatch (which
# a `chart-v*` tag push or a manual rescue dispatch (which
# carries inputs.version) — never on `main` pushes or PRs, so an in-progress
# `main` can never overwrite a released chart version. Mirrors docker.yml's
# GHCR publish (login with GITHUB_TOKEN, packages: write, fork override var).
+9 -6
View File
@@ -59,6 +59,11 @@ with any new commits, and updates the PR in place.
All three lanes share one engine; they differ only in which version manifest
they bump, which branch prefix they use, and what the merge triggers.
The merge workflow creates tags with a short-lived installation token from the
dedicated `buzz-release-bot` GitHub App. Release-tag rules allow that App to
create matching tags and prevent other actors from creating, moving, or
deleting them. The workflow's default `GITHUB_TOKEN` is read-only.
### Desktop
1. **`just release-desktop`** runs locally on `main` — computes the next
@@ -79,15 +84,13 @@ they bump, which branch prefix they use, and what the merge triggers.
opens (or updates) a PR.
2. **Merge the PR** — the `auto-tag-on-release-pr-merge` workflow detects the
`relay-release/*` branch merge and pushes a `relay-v<version>` tag.
3. **Auto-tag dispatches `docker.yml`** — the same workflow then triggers
`docker.yml` with the version and tag ref, which builds the multi-arch relay
3. **Tag triggers `docker.yml`** — the `relay-v<version>` push triggers
`docker.yml`, which builds the multi-arch relay
image and publishes `ghcr.io/block/buzz:<version>` (plus `:<major>.<minor>`,
`:<major>`, and `:latest` for stable releases). Prereleases
(`relay-v<version>-rc.1`) publish only the prerelease tag and do **not**
move `:latest`. (The dispatch — rather than relying on `docker.yml`'s
`push: tags` trigger — is required because GitHub suppresses `on: push` for
tags pushed by the workflow's own `GITHUB_TOKEN`; the desktop lane dispatches
`release.yml` for the same reason.)
move `:latest`. GitHub runs the tag trigger because the tag is created by
the dedicated GitHub App rather than the workflow's `GITHUB_TOKEN`.
Every push to `main` continues to build and publish `:main` + `:sha-<7>` tags
(the rolling development image). The `:latest` tag tracks the latest **stable**
+11 -2
View File
@@ -53,7 +53,16 @@ fi
grep -q 'verify-release-ref\.sh' "$repo_root/.github/workflows/release.yml"
grep -q 'verify-release-ref\.sh' "$repo_root/.github/workflows/docker.yml"
grep -q 'test-release-ref-contract\.sh' "$repo_root/.github/workflows/ci.yml"
grep -Fq -- "--ref \"\$TAG\"" \
"$repo_root/.github/workflows/auto-tag-on-release-pr-merge.yml"
auto_tag="$repo_root/.github/workflows/auto-tag-on-release-pr-merge.yml"
grep -q 'actions/create-github-app-token@' "$auto_tag"
grep -q 'client-id:.*vars\.BUZZ_RELEASE_TAGGER_CLIENT_ID' "$auto_tag"
grep -q 'private-key:.*secrets\.BUZZ_RELEASE_TAGGER_PRIVATE_KEY' "$auto_tag"
grep -q 'permission-contents: write' "$auto_tag"
grep -q 'GH_TOKEN:.*steps\.release-tagger\.outputs\.token' "$auto_tag"
grep -Fq 'git/refs' "$auto_tag"
if grep -q 'gh workflow run' "$auto_tag"; then
echo "auto-tag still dispatches a publisher instead of using the tag push" >&2
exit 1
fi
echo "release ref contract passed"