From 16e1d7c363297c628b4acf3c1f512c42df2d21e6 Mon Sep 17 00:00:00 2001 From: helly25 <6420169+helly25@users.noreply.github.com> Date: Sun, 28 Jun 2026 14:32:57 +0100 Subject: [PATCH] ci(release): CI-native verified release (API bump commit + bot approval) Rework the reusable workflow to cut the release itself instead of running the caller's local trigger_release.sh (which is Keychain/admin-local and produced an Unverified bump commit in CI). Now: push the signed tag with the PAT (admin-bypasses the tag ruleset, triggers release.yml); create the version-bump commit via the GitHub Contents API so GitHub signs it (Verified -> passes the required-signatures branch ruleset); open the bump PR; approve it as github-actions[bot] (distinct identity, counts as the required review); enable squash auto-merge. Needs the caller to grant pull-requests: write. --- .github/workflows/trigger_release.yaml | 126 ++++++++++++++++--------- 1 file changed, 79 insertions(+), 47 deletions(-) diff --git a/.github/workflows/trigger_release.yaml b/.github/workflows/trigger_release.yaml index a18e90a..0fe9a4e 100644 --- a/.github/workflows/trigger_release.yaml +++ b/.github/workflows/trigger_release.yaml @@ -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 `` 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 @@ -38,15 +37,17 @@ 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 @@ -54,49 +55,80 @@ jobs: 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}"