Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 35 additions & 68 deletions .github/workflows/auto-tag-on-release-pr-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,8 +29,7 @@ on:
branches: [main]

permissions:
contents: write
actions: write
contents: read

jobs:
auto-tag:
Expand All @@ -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
Expand All @@ -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}')"
Expand All @@ -86,28 +77,37 @@ 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'"
exit 1
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
Expand All @@ -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
14 changes: 6 additions & 8 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 6 additions & 9 deletions .github/workflows/helm-chart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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).
Expand Down
15 changes: 9 additions & 6 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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**
Expand Down
13 changes: 11 additions & 2 deletions scripts/test-release-ref-contract.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading