From 2d94bcb34a9b2cd2e2debba6881444df51612074 Mon Sep 17 00:00:00 2001 From: Tomas Aprile Date: Tue, 28 Apr 2026 14:51:19 -0300 Subject: [PATCH] feat(release): support GitHub App token for branch-protection bypass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds optional RELEASE_APP_ID / RELEASE_APP_PRIVATE_KEY secrets to the release reusable. When present, the job mints a short-lived installation token via actions/create-github-app-token and uses it as GITHUB_TOKEN for semantic-release. Falls back to the built-in GITHUB_TOKEN when absent, so existing consumers without branch protection are unaffected. Unblocks @semantic-release/git pushes on repos whose `main` is protected by a "PRs required" ruleset (GH013) without reintroducing a long-lived PAT — the App token only exists during the release job, which runs on push:main and is never reached by Dependabot PRs. --- .github/workflows/release.yml | 24 +++++++++++++++++++++++- docs/secrets.md | 29 +++++++++++++++++++++++++++++ docs/troubleshooting.md | 18 ++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0212ef6..b45f008 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,20 @@ on: required: false type: boolean default: false + secrets: + RELEASE_APP_ID: + description: | + GitHub App ID used to mint a short-lived token that can bypass branch + protection on `main`. Required when `@semantic-release/git` is in the + plugins list and the default branch has a "PRs required" ruleset. + Optional; if unset, the job falls back to the built-in GITHUB_TOKEN + (which can't push to protected branches). + required: false + RELEASE_APP_PRIVATE_KEY: + description: | + PEM private key for the GitHub App referenced by RELEASE_APP_ID. + Required if RELEASE_APP_ID is set. + required: false jobs: publish: @@ -49,6 +63,14 @@ jobs: issues: write pull-requests: write steps: + - name: Mint release token (GitHub App) + id: app-token + if: ${{ secrets.RELEASE_APP_ID != '' }} + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + - name: Checkout caller repo uses: actions/checkout@v4 with: @@ -85,4 +107,4 @@ jobs: extra_plugins: ${{ inputs.extra-plugins }} env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} diff --git a/docs/secrets.md b/docs/secrets.md index f9dd498..bf738b9 100644 --- a/docs/secrets.md +++ b/docs/secrets.md @@ -10,6 +10,8 @@ What secrets the reusables need, where they should live, and how to configure th | `VERCEL_TOKEN` | `deploy.yml` | **org** | Vercel personal or org-scoped token | | `VERCEL_ORG_ID` | `deploy.yml` | **org** | Same across all Refokus Vercel projects | | `VERCEL_PROJECT_ID` | `deploy.yml` | **repo** | Unique per Vercel project | +| `RELEASE_APP_ID` | `release.yml` | **org or repo** | Optional. GitHub App ID for branch-protection bypass on `main`. Required when using `@semantic-release/git` against a branch with a "PRs required" ruleset. | +| `RELEASE_APP_PRIVATE_KEY` | `release.yml` | **org or repo** | Optional. PEM private key paired with `RELEASE_APP_ID`. | `GITHUB_TOKEN` covers what we used to need a PAT for: cloning the public `refokus-agency/platform` reusables (no auth needed for public repos), authenticating `.npmrc` for `@refokus-agency/*` packages on GitHub Packages, and tagging/publishing in `release.yml`. The caller declares the scopes via `permissions:` (`contents`, `packages`). @@ -92,6 +94,33 @@ Unique per Vercel project. Configured **per repo**. You can also find the project ID in the Vercel dashboard: Project → Settings → General → "Project ID". +## Release bypass with a GitHub App + +`release.yml` runs `semantic-release`, which by default uses `@semantic-release/git` to push a commit (CHANGELOG, version bump) back to `main`. The built-in `GITHUB_TOKEN` cannot push to `main` if the branch is protected by a ruleset that requires PRs — pushes are rejected with `GH013: Repository rule violations found`. + +The fix is to mint a short-lived token from a GitHub App that is on the ruleset's bypass list, and use that token for `semantic-release`. This is scoped to the release job only — Dependabot PR runs use `pull_request` workflows that don't touch this token, so this setup doesn't reintroduce the secret-exposure pattern that pushed the project off PATs. + +### Setup + +1. **Create a GitHub App** at the org level: `https://github.com/organizations/refokus-agency/settings/apps/new`. + - Repository permissions: `contents: read and write`, `issues: read and write`, `pull-requests: read and write`, `metadata: read`. + - Webhook: disabled. + - Where can be installed: **Only this account** (`refokus-agency`). +2. **Generate a private key** (`.pem`) on the app's settings page and download it. +3. **Install the app** on the repos that need release bypass. +4. **Add the app to the bypass list** of the org-level ruleset on `main`: `https://github.com/organizations/refokus-agency/settings/rules` → ruleset → Bypass list → Add bypass → select the app → mode "Always". Requires org-owner permissions. +5. **Set the secrets** on the repo (or org level if you want to share the app across many repos): + ```bash + gh secret set RELEASE_APP_ID --repo / --body "" + gh secret set RELEASE_APP_PRIVATE_KEY --repo / < path/to/key.pem + ``` + On Windows / PowerShell: + ```powershell + Get-Content path/to/key.pem -Raw | gh secret set RELEASE_APP_PRIVATE_KEY --repo / + ``` + +If both secrets are present when `release.yml` runs, the job mints an installation token and uses it as `GITHUB_TOKEN` for `semantic-release`. If either is missing, the job falls back to the built-in `GITHUB_TOKEN` — fine for repos without branch protection or that don't use `@semantic-release/git`. + ## Rotating secrets When a Vercel token expires or is compromised: diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e6afaf7..c8277d1 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -102,6 +102,24 @@ Common failure modes when using the centralized workflows, and how to fix them. - Verify `.releaserc` (or `release` key in `package.json`) exists and configures the GitHub Packages plugin. - Check the logs — semantic-release is verbose about why it didn't release. +## Semantic-release fails with "GH013: Repository rule violations found" + +**Symptoms:** the release job fails at the `prepare` step of `@semantic-release/git` with: + +``` +remote: error: GH013: Repository rule violations found for refs/heads/main. +- Changes must be made through a pull request. +``` + +**Cause:** `@semantic-release/git` tries to push the version-bump + CHANGELOG commit directly to `main`, but the branch is protected by a ruleset that requires PRs. The built-in `GITHUB_TOKEN` cannot bypass that ruleset. + +**Fix:** configure a GitHub App with branch-protection bypass and pass its credentials to the release job. See [secrets.md → Release bypass with a GitHub App](secrets.md#release-bypass-with-a-github-app) for the full setup. + +If you don't want to set up a GitHub App, two alternatives: + +- Remove `@semantic-release/git` from the plugins list in `.releaserc.json`. The release still creates a tag, GitHub Release, and publishes the package — only the version-bump commit and CHANGELOG.md aren't pushed back to the repo. +- Switch to `release-please` (PR-based, no direct push to `main`). `platform` itself uses release-please for the same reason. + ## Caching seems ineffective **Symptoms:** every run re-downloads all dependencies.