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

This commit is contained in:
Jordan Mecom
2026-07-20 15:12:14 -07:00
committed by GitHub
parent 9ed843a0ac
commit 0b297edbdf
5 changed files with 67 additions and 93 deletions

View File

@@ -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 # 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: # a chart feature PR can publish its own new version when merged:
# #
# version-bump/<v> → tag v<v> → dispatch release.yml (desktop app) # version-bump/<v> → tag v<v> → release.yml (desktop app)
# relay-release/<v> → tag relay-v<v> → dispatch docker.yml (relay image) # relay-release/<v> → tag relay-v<v> → docker.yml (relay image)
# chart-release/<v> → tag chart-v<v> → dispatch helm-chart.yml (main helm chart) # chart-release/<v> → tag chart-v<v> → helm-chart.yml (main helm chart)
# push-chart-release/<v> → tag push-chart-v<v> → dispatch push-gateway-helm-chart.yml # 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` # 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) # 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 # Release tags are created with a short-lived token from the dedicated
# relying on the consumer's `on.push.tags` trigger: auto-tag pushes the tag # buzz-release-bot GitHub App. GitHub attributes the ref creation to that
# with the default GITHUB_TOKEN, which GitHub's recursion guard blocks from # App, so the consumer's `on.push.tags` trigger runs normally. The workflow's
# firing any `on: push` trigger. So release.yml, docker.yml, and helm-chart.yml # default GITHUB_TOKEN remains read-only and is never used to create a tag.
# 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`).
# #
# The mobile lane is push-only by infosec necessity: OSS `block/buzz` CI must # 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 # not trigger CI in the private `buzz-releases` repo, so auto-dispatch across
@@ -33,8 +29,7 @@ on:
branches: [main] branches: [main]
permissions: permissions:
contents: write contents: read
actions: write
jobs: jobs:
auto-tag: auto-tag:
@@ -47,6 +42,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.merge_commit_sha }} ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 0 fetch-depth: 0
persist-credentials: false
- name: Resolve release lane and version - name: Resolve release lane and version
id: release id: release
@@ -59,24 +55,19 @@ jobs:
case "$BRANCH" in case "$BRANCH" in
version-bump/*) version-bump/*)
VERSION="${BRANCH#version-bump/}" VERSION="${BRANCH#version-bump/}"
TAG_PREFIX="v" TAG_PREFIX="v" ;;
DISPATCH="release" ;;
relay-release/*) relay-release/*)
VERSION="${BRANCH#relay-release/}" VERSION="${BRANCH#relay-release/}"
TAG_PREFIX="relay-v" TAG_PREFIX="relay-v" ;;
DISPATCH="docker" ;;
chart-release/*) chart-release/*)
VERSION="${BRANCH#chart-release/}" VERSION="${BRANCH#chart-release/}"
TAG_PREFIX="chart-v" TAG_PREFIX="chart-v" ;;
DISPATCH="helm-chart" ;;
push-chart-release/*) push-chart-release/*)
VERSION="${BRANCH#push-chart-release/}" VERSION="${BRANCH#push-chart-release/}"
TAG_PREFIX="push-chart-v" TAG_PREFIX="push-chart-v" ;;
DISPATCH="push-gateway-helm-chart" ;;
mobile-release/*) mobile-release/*)
VERSION="${BRANCH#mobile-release/}" VERSION="${BRANCH#mobile-release/}"
TAG_PREFIX="mobile-v" TAG_PREFIX="mobile-v" ;;
DISPATCH="" ;;
*) *)
parent_sha="$(git rev-parse HEAD^)" parent_sha="$(git rev-parse HEAD^)"
old_version="$(git show "${parent_sha}:deploy/charts/buzz/Chart.yaml" 2>/dev/null | awk '/^version:/ {print $2}')" 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" echo "enabled=false" >> "$GITHUB_OUTPUT"
exit 0 exit 0
fi fi
TAG_PREFIX="chart-v" TAG_PREFIX="chart-v" ;;
DISPATCH="helm-chart" ;;
esac esac
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
echo "::error::Invalid release version: '$VERSION'" echo "::error::Invalid release version: '$VERSION'"
@@ -95,19 +85,29 @@ jobs:
fi fi
{ {
echo "enabled=true" echo "enabled=true"
echo "version=$VERSION"
echo "tag=${TAG_PREFIX}${VERSION}" echo "tag=${TAG_PREFIX}${VERSION}"
echo "dispatch=$DISPATCH"
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
echo "Tagging ${TAG_PREFIX}${VERSION}" 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 - name: Create and push tag
if: steps.release.outputs.enabled == 'true' if: steps.release.outputs.enabled == 'true'
env: env:
GH_TOKEN: ${{ steps.release-tagger.outputs.token }}
TAG: ${{ steps.release.outputs.tag }} TAG: ${{ steps.release.outputs.tag }}
run: | run: |
EXISTING_SHA="$(git ls-remote --tags origin "refs/tags/$TAG" | awk '{print $1}')" set -euo pipefail
if [ -n "$EXISTING_SHA" ]; then 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 if [ "$EXISTING_SHA" = "$GITHUB_SHA" ]; then
echo "Tag $TAG already exists at $GITHUB_SHA — skipping tag creation" echo "Tag $TAG already exists at $GITHUB_SHA — skipping tag creation"
exit 0 exit 0
@@ -116,40 +116,7 @@ jobs:
exit 1 exit 1
fi fi
fi fi
git config user.email "41898282+github-actions[bot]@users.noreply.github.com" gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
git config user.name "github-actions[bot]" -f ref="refs/tags/$TAG" \
git tag "$TAG" -f sha="$GITHUB_SHA" \
git push origin "$TAG" --silent
- 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[@]}"

View File

@@ -18,15 +18,13 @@ name: Docker image
# - push tags relay-v*.*.* → :{version} + :{major}.{minor} + :{major} # - push tags relay-v*.*.* → :{version} + :{major}.{minor} + :{major}
# (+ :latest for stable, NOT for prereleases) # (+ :latest for stable, NOT for prereleases)
# - pull_request → build only (no push), cache stays warm # - 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: # Why workflow_dispatch carries a version input:
# auto-tag-on-release-pr-merge.yml pushes relay-v* with the default # Normal releases arrive through the push:tags trigger above. The input is
# GITHUB_TOKEN, which GitHub deliberately does NOT let fire on:push triggers # retained only for an operator to rerun publication manually at an immutable
# (recursion guard). So the push:tags trigger above never runs for releases. # relay tag. The workflow rejects a dispatch whose github.ref, checked-out
# auto-tag instead dispatches this workflow at the immutable tag and supplies # HEAD, and relay-v tag do not resolve to one commit.
# 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.
# On the rescue path inputs.version is already bare (e.g. 0.3.0), so the # 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 # 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 # 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 # own — it only strips refs/tags/, then runs the raw ref through
# semver.valid(), which rejects "relay-v0.3.0". The match capture # semver.valid(), which rejects "relay-v0.3.0". The match capture
# group feeds the bare version to the semver parser. value= supplies # 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, # 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= # 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 # is empty, so the ref drives it and match strips relay-v — push

View File

@@ -9,14 +9,11 @@ name: helm chart
# `chart-v*` tags publish — `main` pushes and PRs stay lint/render-only so we # `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`. # never overwrite a released chart version from an in-progress `main`.
# #
# Why workflow_dispatch carries version/ref inputs (same reason as docker.yml): # Why workflow_dispatch carries version/ref inputs:
# auto-tag-on-release-pr-merge.yml pushes `chart-v*` with the default # Normal releases arrive through the `push.tags` trigger below. The inputs are
# GITHUB_TOKEN, which GitHub's recursion guard blocks from firing any # retained only for an operator to rerun publication manually at an immutable
# `on: push` trigger. So the `push.tags` trigger below is dead for # chart tag; the publish job checks out inputs.ref and packages at
# auto-pushed tags — auto-tag instead dispatches this workflow with the bare # inputs.version.
# 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.
on: on:
workflow_dispatch: workflow_dispatch:
@@ -153,7 +150,7 @@ jobs:
publish: publish:
# Packages the chart and pushes it to GHCR as an OCI artifact. Fires only on # 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 # 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 # `main` can never overwrite a released chart version. Mirrors docker.yml's
# GHCR publish (login with GITHUB_TOKEN, packages: write, fork override var). # GHCR publish (login with GITHUB_TOKEN, packages: write, fork override var).

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 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. 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 ### Desktop
1. **`just release-desktop`** runs locally on `main` — computes the next 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. opens (or updates) a PR.
2. **Merge the PR** — the `auto-tag-on-release-pr-merge` workflow detects the 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. `relay-release/*` branch merge and pushes a `relay-v<version>` tag.
3. **Auto-tag dispatches `docker.yml`** — the same workflow then triggers 3. **Tag triggers `docker.yml`** — the `relay-v<version>` push triggers
`docker.yml` with the version and tag ref, which builds the multi-arch relay `docker.yml`, which builds the multi-arch relay
image and publishes `ghcr.io/block/buzz:<version>` (plus `:<major>.<minor>`, image and publishes `ghcr.io/block/buzz:<version>` (plus `:<major>.<minor>`,
`:<major>`, and `:latest` for stable releases). Prereleases `:<major>`, and `:latest` for stable releases). Prereleases
(`relay-v<version>-rc.1`) publish only the prerelease tag and do **not** (`relay-v<version>-rc.1`) publish only the prerelease tag and do **not**
move `:latest`. (The dispatch — rather than relying on `docker.yml`'s move `:latest`. GitHub runs the tag trigger because the tag is created by
`push: tags` trigger — is required because GitHub suppresses `on: push` for the dedicated GitHub App rather than the workflow's `GITHUB_TOKEN`.
tags pushed by the workflow's own `GITHUB_TOKEN`; the desktop lane dispatches
`release.yml` for the same reason.)
Every push to `main` continues to build and publish `:main` + `:sha-<7>` tags Every push to `main` continues to build and publish `:main` + `:sha-<7>` tags
(the rolling development image). The `:latest` tag tracks the latest **stable** (the rolling development image). The `:latest` tag tracks the latest **stable**

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/release.yml"
grep -q 'verify-release-ref\.sh' "$repo_root/.github/workflows/docker.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 -q 'test-release-ref-contract\.sh' "$repo_root/.github/workflows/ci.yml"
grep -Fq -- "--ref \"\$TAG\"" \ auto_tag="$repo_root/.github/workflows/auto-tag-on-release-pr-merge.yml"
"$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" echo "release ref contract passed"