From 4c6f62b84363e06f412fed2eee4a2b54d008e514 Mon Sep 17 00:00:00 2001 From: Alexey Shalaev <75322386+AlexeyShalaev@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:50:47 +0400 Subject: [PATCH] fix: CI runs on release PRs without a close and reopen Release Please opened and updated its PR with GITHUB_TOKEN. GitHub holds the pull_request run such an event creates until it expires, so the required All checks passed never reported, and every release needed someone to close and reopen the PR after its last update. The workflow now mints a token from an org-owned GitHub App (actions/create-github-app-token, scoped to contents and pull-requests write, the same permissions the job's GITHUB_TOKEN has) and hands it to Release Please, so its PR events start CI like anyone else's. The App is configured once per org: RELEASE_PLEASE_APP_CLIENT_ID as a variable, RELEASE_PLEASE_APP_PRIVATE_KEY as a secret. Until then the workflow falls back to GITHUB_TOKEN and warns, so a new library still releases the way it does today. setup_repo.py warns when a repository cannot see the two names. Closes #12 --- README.md | 19 +++++++++++++++ scripts/setup_repo.py | 18 ++++++++++++++ template/.github/workflows/release-please.yml | 24 +++++++++++++++++++ template/NEW_LIBRARY_CHECKLIST.md.jinja | 2 ++ 4 files changed, 63 insertions(+) diff --git a/README.md b/README.md index f42f401..af20222 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,25 @@ A library rendered before the answers file existed needs one written by hand fir answers it was rendered with, `_src_path: gh:bedrock-python/python-library-template`, and as `_commit` the template commit it was rendered from, or the closest one. +## The release App + +Release Please opens and updates its release PR with a token of an org-owned GitHub App. +With the workflow's own `GITHUB_TOKEN`, the PR's CI run is held and never starts, the +required "All checks passed" never reports, and the PR can only be merged after someone +closes and reopens it. One App serves every library and is set up once: + +1. Create a GitHub App owned by the org (org Settings → Developer settings → GitHub Apps): + webhook off, repository permissions **Contents: Read and write** and **Pull requests: + Read and write**, installable on this account only. +2. Install it on the org, for all repositories. +3. In org Settings → Secrets and variables → Actions, visible to all repositories: + variable `RELEASE_PLEASE_APP_CLIENT_ID` = the App's Client ID, secret + `RELEASE_PLEASE_APP_PRIVATE_KEY` = a private key generated for it. + +Release PRs, tags and GitHub Releases are then authored by the App's bot account. While +the variable is unset, the workflow falls back to `GITHUB_TOKEN` and its run carries a +warning, and `setup_repo.py` warns for any repository that cannot see the two names. + ## For AI Agents See [`.claude/LIBRARY_CREATION.md`](.claude/LIBRARY_CREATION.md) for important rules: diff --git a/scripts/setup_repo.py b/scripts/setup_repo.py index 6f3be87..f5944ca 100644 --- a/scripts/setup_repo.py +++ b/scripts/setup_repo.py @@ -24,6 +24,8 @@ RULESET_NAME = "master-rules" REQUIRED_CHECK = "All checks passed" +RELEASE_APP_CLIENT_ID = "RELEASE_PLEASE_APP_CLIENT_ID" # organization variable +RELEASE_APP_PRIVATE_KEY = "RELEASE_PLEASE_APP_PRIVATE_KEY" # organization secret MAX_TOPICS = 20 @@ -111,6 +113,22 @@ def has_workflow(name: str) -> bool: gh(f"repos/{repo}/pages", "-X", "POST", payload={"build_type": "workflow"}, ok=(409,)) pages = gh(f"repos/{repo}/pages", ok=(404,)) + if has_workflow("release-please.yml"): + # Without the App the release PR is opened with GITHUB_TOKEN and CI never + # runs on it; the workflow falls back and warns, this says so up front. + step("Release Please: the release App's client ID and private key reach the repository") + def visible(kind: str) -> set[str]: + names: set[str] = set() + for scope in (f"organization-{kind}", kind): + listed = gh(f"repos/{repo}/actions/{scope}?per_page=100", ok=(403, 404)) or {} + names.update(item["name"] for item in listed.get(kind, [])) + return names + + for kind, name in (("variables", RELEASE_APP_CLIENT_ID), ("secrets", RELEASE_APP_PRIVATE_KEY)): + if name not in visible(kind): + print(f" [WARN] Actions {kind[:-1]} {name} is not visible to {repo}: CI will not run on" + " release PRs until it is (template README, 'The release App')") + step("Actions: workflows get a read-only token unless they ask for more; Release Please may open PRs") gh(f"repos/{repo}/actions/permissions/workflow", "-X", "PUT", payload={"default_workflow_permissions": "read", "can_approve_pull_request_reviews": True}) diff --git a/template/.github/workflows/release-please.yml b/template/.github/workflows/release-please.yml index 8947d93..5b29311 100644 --- a/template/.github/workflows/release-please.yml +++ b/template/.github/workflows/release-please.yml @@ -12,7 +12,31 @@ jobs: release-please: runs-on: ubuntu-latest steps: + # A pull request opened or updated with GITHUB_TOKEN does not get a CI run + # that ever starts, so the release PR could not pass its required check + # until a person closed and reopened it. A token of the org's release App + # starts CI like anyone else's push. Until the App is set up, GITHUB_TOKEN + # is used and the run says so. Setup, once per org: + # https://github.com/bedrock-python/python-library-template#the-release-app + - name: Token from the release App + id: app + if: vars.RELEASE_PLEASE_APP_CLIENT_ID != '' + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.RELEASE_PLEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + + - name: No release App + if: steps.app.outcome == 'skipped' + run: >- + echo "::warning::RELEASE_PLEASE_APP_CLIENT_ID is not set, so the release PR + is opened with GITHUB_TOKEN and CI will not run on it: close and reopen it + right before merging." + - uses: googleapis/release-please-action@v5 with: + token: ${{ steps.app.outputs.token || github.token }} config-file: release-please-config.json manifest-file: .release-please-manifest.json diff --git a/template/NEW_LIBRARY_CHECKLIST.md.jinja b/template/NEW_LIBRARY_CHECKLIST.md.jinja index 1389f7f..a51af5b 100644 --- a/template/NEW_LIBRARY_CHECKLIST.md.jinja +++ b/template/NEW_LIBRARY_CHECKLIST.md.jinja @@ -51,6 +51,8 @@ This configures (idempotent, re-run any time): private vulnerability reporting - A `master` ruleset: pull requests only, no force-push or deletion, `All checks passed` required (left out with a warning if CI has not reported yet — re-run after it has) +- A warning if the repository cannot see the release App's credentials: release PRs + would then get no CI (template README, "The release App") ## Step 4 — PyPI Trusted Publisher (manual)