Skip to content
Merged
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
126 changes: 79 additions & 47 deletions .github/workflows/trigger_release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,31 @@
#
# jobs:
# trigger-release:
# permissions:
# contents: read
# pull-requests: write # so github-actions[bot] can approve the bump PR
# uses: helly25/bzl/.github/workflows/trigger_release.yaml@main
# secrets: inherit
# with:
# version: ${{ inputs.version }}
#
# It checks out the CALLER repo's `main`, verifies MODULE.bazel and CHANGELOG.md
# agree on the version and that the version is neither tagged nor released, then
# runs the caller's tools/trigger_release.sh, which pushes the signed version tag
# (firing the caller's release.yml -> GitHub release + BCR) and opens the
# next-version bump PR.
# It operates on the CALLER repo's `main` and releases whatever version is in
# MODULE.bazel (which must equal the CHANGELOG.md top header). It:
# 1. pushes a signed tag `<version>` with the PAT (which, as a repo admin, the
# tag ruleset's bypass allows) -- this triggers the caller's release.yml;
# 2. bumps MODULE.bazel + CHANGELOG.md to the next patch version and creates the
# bump commit through the GitHub Contents API so GitHub signs it ("Verified"),
# satisfying the branch ruleset's required-signatures with no bypass;
# 3. opens the bump PR, approves it as github-actions[bot] (a different identity
# than the PAT author, so it counts as the required review), and enables
# squash auto-merge (branch protection completes it once CI is green).
#
# Required (org-level) secrets, passed via `secrets: inherit`:
# RELEASE_TOKEN PAT (contents + pull-requests + workflow write), used
# as the checkout/push token so the pushed tag triggers
# release.yml (the default GITHUB_TOKEN would not).
# RELEASE_GPG_PRIVATE_KEY Dedicated release signing key (ASCII-armored).
# RELEASE_GPG_PASSPHRASE Passphrase for the signing key.
# Required secrets (via `secrets: inherit`):
# RELEASE_TOKEN PAT (Contents + Pull requests: write) of a repo admin.
# RELEASE_GPG_PRIVATE_KEY Signing key for the annotated release tag.
# RELEASE_GPG_PASSPHRASE Passphrase for that key.
name: Trigger Release

on:
workflow_call:
inputs:
version:
description: "Release version x.y.z (blank = use MODULE.bazel)."
required: false
type: string
secrets:
RELEASE_TOKEN:
required: true
Expand All @@ -38,65 +37,98 @@ on:
RELEASE_GPG_PASSPHRASE:
required: true

# All writes go through RELEASE_TOKEN; the default GITHUB_TOKEN only needs read.
permissions:
contents: read

jobs:
trigger-release:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout caller main
- name: Checkout main
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
fetch-tags: true
token: ${{ secrets.RELEASE_TOKEN }}

- name: Import release signing key
- name: Import signing key (for the release tag)
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
git_user_signingkey: true
git_commit_gpgsign: true
git_tag_gpgsign: true

- name: Resolve and validate version
id: ver
- name: Cut release and open the bump PR
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
INPUT_VERSION: ${{ inputs.version }}
BOT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# 1) MODULE.bazel and CHANGELOG.md must agree on the version.
REPO="${GITHUB_REPOSITORY}"

# --- validate (read-only) -------------------------------------------
bash .pre-commit/check_version.sh
MODULE_VERSION="$(sed -rne 's,.*version = "([0-9]+([.][0-9]+)+.*)".*,\1,p' MODULE.bazel | head -n1)"
VERSION="${INPUT_VERSION:-${MODULE_VERSION}}"
# 2) Numeric release version, and must match MODULE.bazel when provided.
VERSION="$(sed -rne 's,.*version = "([0-9]+([.][0-9]+)+.*)".*,\1,p' MODULE.bazel | head -n1)"
if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Version '${VERSION}' is not numeric x.y.z."
exit 1
fi
if [[ -n "${INPUT_VERSION}" && "${INPUT_VERSION}" != "${MODULE_VERSION}" ]]; then
echo "::error::Input version (${INPUT_VERSION}) != MODULE.bazel (${MODULE_VERSION})."
exit 1
echo "::error::MODULE.bazel version '${VERSION}' is not numeric x.y.z."; exit 1
fi
# 3) Must not already be tagged.
NEXT_VERSION="$(awk -F. '{print $1"."$2"."($3+1)}' <<<"${VERSION}")"
if [[ -n "$(git tag -l "${VERSION}")" ]]; then
echo "::error::Tag ${VERSION} already exists."
exit 1
echo "::error::tag ${VERSION} already exists."; exit 1
fi
# 4) Must not already be released (also catches a draft release).
if gh release view "${VERSION}" >/dev/null 2>&1; then
echo "::error::Release ${VERSION} already exists."
exit 1
echo "::error::release ${VERSION} already exists."; exit 1
fi
echo "version=${VERSION}" >>"${GITHUB_OUTPUT}"
echo "Releasing ${VERSION}."
echo "Releasing ${VERSION}; next dev version ${NEXT_VERSION}."

- name: Run tools/trigger_release.sh
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: tools/trigger_release.sh "${{ steps.ver.outputs.version }}"
# --- 1. signed tag -> triggers release.yml (PAT admin-bypasses the
# tag ruleset; default GITHUB_TOKEN would not trigger workflows) -
git tag -s -a "${VERSION}" \
-m "New release tag version: '${VERSION}'." \
-m "$(awk '/^#/{if(NR>1)exit}/^[^#]/{print}' CHANGELOG.md)"
git push origin "refs/tags/${VERSION}"

# --- 2. bump the files in the worktree ------------------------------
sed "1,/version = \"${VERSION}\"/ s/version = \"${VERSION}\"/version = \"${NEXT_VERSION}\"/" MODULE.bazel >MODULE.bazel.tmp
mv MODULE.bazel.tmp MODULE.bazel
{ printf '# %s\n\n' "${NEXT_VERSION}"; cat CHANGELOG.md; } >CHANGELOG.md.tmp
mv CHANGELOG.md.tmp CHANGELOG.md

# --- 3. create the bump commit via the Contents API so GitHub signs
# it ("Verified"); a git-pushed commit would be signed by the
# runner's key, which is Unverified and rejected by the ruleset -
BASE_SHA="$(git rev-parse HEAD)"
NEXT_BRANCH="chore/bump_version_to_${NEXT_VERSION}"
gh api -X POST "repos/${REPO}/git/refs" \
-f "ref=refs/heads/${NEXT_BRANCH}" -f "sha=${BASE_SHA}" >/dev/null
for f in MODULE.bazel CHANGELOG.md; do
gh api -X PUT "repos/${REPO}/contents/${f}" \
-f "message=Bump version to ${NEXT_VERSION}" \
-f "branch=${NEXT_BRANCH}" \
-f "sha=$(git rev-parse "HEAD:${f}")" \
-f "content=$(base64 <"${f}" | tr -d '\n')" >/dev/null
done

# --- 4. open the bump PR (author = PAT user) ------------------------
PRURL="$(gh pr create --base main --head "${NEXT_BRANCH}" \
--title "Bump version from ${VERSION} to ${NEXT_VERSION}" \
--body "Automated dev-version bump opened by the release workflow.")"
PRNUM="$(sed -rne 's,.*/pull/([0-9]+)$,\1,p' <<<"${PRURL}")"

# --- 5. approve as github-actions[bot] (a different identity than the
# PAT author, so it counts as the required review) -------------
GH_TOKEN="${BOT_TOKEN}" gh pr review "${PRNUM}" --approve \
--body "Automated approval of the version bump."

# --- 6. squash auto-merge; branch protection completes it once the
# approval and required status checks are satisfied ------------
gh pr merge "${PRNUM}" --auto --squash --delete-branch \
--subject "Bump version from ${VERSION} to ${NEXT_VERSION}" \
--body "Automated version bump."

echo "Released ${VERSION}. Bump PR #${PRNUM} approved; auto-merge queued: ${PRURL}"
Loading