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)