From fdb6bfd4a5170d0950bb70d7257747996c36cdc1 Mon Sep 17 00:00:00 2001 From: unional Date: Sat, 10 Oct 2026 21:28:53 -0700 Subject: [PATCH 1/2] fix(release): start CI on the Version Packages PR without a stored secret (v2 line) Replace the optional GitHub App token from #61 with a secretless dispatch. GITHUB_TOKEN-caused events start no workflows, so the version PR got no checks and stayed blocked on the required `code / all-checks`. workflow_dispatch is the one event GITHUB_TOKEN may trigger, so after changesets/action opens or updates the PR, the release job runs `gh workflow run --ref changeset-release/`. - Remove RELEASE_APP_ID / RELEASE_APP_PRIVATE_KEY, HAS_RELEASE_APP, the create-github-app-token step and the token fallbacks. - Update the README caller example and adoption steps. - Add a `ci-workflow` input (default pull-request.yml) and document `actions: write`; callers must grant it and add `workflow_dispatch:` to their CI workflow. A failed dispatch warns, never fails the release. Co-Authored-By: Claude Opus 5.5 Signed-off-by: unional --- .../workflows/pnpm-release-changeset-oidc.yml | 73 ++++++++++++------- README.md | 35 ++++----- 2 files changed, 64 insertions(+), 44 deletions(-) diff --git a/.github/workflows/pnpm-release-changeset-oidc.yml b/.github/workflows/pnpm-release-changeset-oidc.yml index 059ef6d..e5354c1 100644 --- a/.github/workflows/pnpm-release-changeset-oidc.yml +++ b/.github/workflows/pnpm-release-changeset-oidc.yml @@ -2,15 +2,23 @@ name: pnpm-release-changeset-oidc # Secretless variant of pnpm-release-changeset.yml. # # Differences from that workflow: -# - No CI_GITHUB_TOKEN. The "version packages" PR is opened with a repobuddy GitHub -# App token when the caller passes RELEASE_APP_ID and RELEASE_APP_PRIVATE_KEY. A PR -# opened with GITHUB_TOKEN does not trigger `on: pull_request` workflows, so its -# required checks never report and it can only be merged by an admin; the app token -# does trigger them. An app key does not expire, so there is still nothing to rotate. -# Without those secrets the job falls back to the built-in GITHUB_TOKEN with elevated -# `permissions`, and the org must allow GitHub Actions to create pull requests. -# Consumers whose pull-request.yml carries `branches-ignore: ['changeset-release/*']` -# still skip those checks until they drop that filter. +# - No CI_GITHUB_TOKEN. Checkout and changesets/action both use the built-in +# GITHUB_TOKEN with elevated `permissions`. GitHub starts no workflows for events +# that token causes, so opening or pushing the "version packages" PR does not run +# the caller's `on: pull_request` CI, and a ruleset that requires its checks leaves +# the PR blocked. workflow_dispatch is the one event GITHUB_TOKEN may trigger, so +# after changesets/action opens or updates the PR, this job runs +# `gh workflow run --ref changeset-release/`. The check runs +# from that dispatch attach to the branch head under the GitHub Actions app and +# satisfy the required check by name. A GitHub App token would also work, but it +# means a long-lived private key in org secrets; this needs no stored secret. +# +# The caller must: +# - grant `actions: write` (on top of `contents: write`, `pull-requests: write` +# and `id-token: write`) to the job that calls this workflow, for the dispatch; +# - list `workflow_dispatch:` in the `on:` block of its CI workflow +# (`ci-workflow`, default pull-request.yml). Without it the dispatch fails; the +# release still succeeds, but with a warning and a PR that has no checks. # - No NPM_TOKEN. Publishes via npm trusted publishing (OIDC), which also emits # provenance attestations. Each package must have a trusted publisher registered at # npmjs.com/package//access naming the repo and the *caller* workflow's @@ -32,6 +40,13 @@ name: pnpm-release-changeset-oidc on: workflow_call: inputs: + ci-workflow: + description: > + The caller's CI workflow file, dispatched on the version branch after the + "version packages" PR is opened or updated. It must list `workflow_dispatch:`. + type: string + required: false + default: pull-request.yml pnpm-version: type: string required: false @@ -39,11 +54,6 @@ on: type: string required: false default: '24' - secrets: - RELEASE_APP_ID: - required: false - RELEASE_APP_PRIVATE_KEY: - required: false outputs: published: description: 'Whether the release was published' @@ -56,6 +66,8 @@ permissions: contents: write # Lets changesets/action open the "version packages" PR. pull-requests: write + # Lets the job dispatch the caller's CI on the version branch (see the header). + actions: write concurrency: ${{ github.workflow }}-${{ github.ref }} @@ -63,24 +75,13 @@ jobs: release: runs-on: ubuntu-latest env: - HAS_RELEASE_APP: ${{ secrets.RELEASE_APP_ID != '' && secrets.RELEASE_APP_PRIVATE_KEY != '' }} PLAYWRIGHT_PATH: ~/.cache/ms-playwright outputs: published: ${{ steps.changesets.outputs.published }} steps: - - uses: actions/create-github-app-token@v3 - if: env.HAS_RELEASE_APP == 'true' - id: app-token - with: - app-id: ${{ secrets.RELEASE_APP_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - - # The version branch is pushed with the checkout's credentials, so the app token - # goes here too; otherwise the push would not re-trigger the PR's checks. - uses: actions/checkout@v7 with: fetch-depth: 0 - token: ${{ steps.app-token.outputs.token || github.token }} - uses: pnpm/action-setup@v6 @@ -112,9 +113,27 @@ jobs: id: changesets uses: changesets/action@v2.0.0 with: - github-token: ${{ steps.app-token.outputs.token || github.token }} commit-message: 'chore: version packages' version-script: pnpm run version publish-script: pnpm run release env: - GITHUB_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # GITHUB_TOKEN-caused events start no workflows, so the version PR would never get + # its required checks. Dispatch the caller's CI on the version branch instead; see + # the header. A failed dispatch warns rather than failing an otherwise good release. + - name: Start CI on the version packages PR + if: steps.changesets.outputs.pr-number != '' && steps.changesets.outputs.published != 'true' + env: + GH_TOKEN: ${{ github.token }} + CI_WORKFLOW: ${{ inputs.ci-workflow }} + VERSION_BRANCH: changeset-release/${{ github.ref_name }} + run: | + if ! out=$(gh workflow run "$CI_WORKFLOW" --repo "$GITHUB_REPOSITORY" --ref "$VERSION_BRANCH" 2>&1); then + echo "$out" + if grep -qi 'workflow_dispatch' <<<"$out"; then + echo "::warning::$CI_WORKFLOW has no workflow_dispatch trigger, so the version packages PR has no checks. Add 'workflow_dispatch:' to its 'on:' block." + else + echo "::warning::Could not dispatch $CI_WORKFLOW on $VERSION_BRANCH, so the version packages PR has no checks. Check that the caller grants 'actions: write' and that ci-workflow names its CI workflow file." + fi + fi diff --git a/README.md b/README.md index 237a673..2fd2621 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ they authenticate. | Workflow | GitHub auth | npm auth | | --- | --- | --- | | `pnpm-release-changeset.yml` | `CI_GITHUB_TOKEN` | `NPM_TOKEN` | -| `pnpm-release-changeset-oidc.yml` | repobuddy App token, else built-in `GITHUB_TOKEN` | trusted publishing (OIDC) | +| `pnpm-release-changeset-oidc.yml` | built-in `GITHUB_TOKEN` | trusted publishing (OIDC) | **Prefer the OIDC variant for new repos, and migrate existing ones.** Both tokens are long-lived credentials that expire silently and are worth stealing; @@ -206,24 +206,28 @@ emits provenance attestations. id-token: write contents: write pull-requests: write - secrets: - RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} - RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + actions: write ``` - The two `secrets:` lines are optional. With them, the workflow opens the - "version packages" PR with a repobuddy GitHub App token. Without them, it - falls back to the built-in `GITHUB_TOKEN`. + `actions: write` lets the workflow start your CI on the "version packages" + PR (see below). If your CI workflow is not `pull-request.yml`, pass its file + name as `with: ci-workflow: `. -4. Once a release has published successfully, delete `NPM_TOKEN` and +4. Add `workflow_dispatch:` to the `on:` block of your CI workflow. Without it, + the release still succeeds, but it warns and the "version packages" PR gets + no checks. +5. Once a release has published successfully, delete `NPM_TOKEN` and `CI_GITHUB_TOKEN` from the repo's secrets. -Why the App token: a PR opened with `GITHUB_TOKEN` does not trigger -`on: pull_request` workflows, so the "version packages" PR gets no status -checks and can only be merged by an admin. A PR opened with the App token does -trigger them, and an App key does not expire. A consumer whose -`pull-request.yml` carries `branches-ignore: ['changeset-release/*']` still -skips those checks until it drops that filter. +Why the dispatch: GitHub starts no workflows for events that `GITHUB_TOKEN` +causes, so opening or pushing the "version packages" PR does not run +`on: pull_request` CI, and a ruleset that requires its checks leaves the PR +blocked. `workflow_dispatch` is the one event `GITHUB_TOKEN` may trigger, so +after changesets/action opens or updates the PR, the workflow runs +`gh workflow run --ref changeset-release/`. The check +runs attach to the branch head under the GitHub Actions app and satisfy the +required check by name. A GitHub App token would also work, but it needs a +long-lived private key in org secrets; the dispatch needs no stored secret. ### Will Renovate keep consumer pins current? @@ -341,9 +345,6 @@ publish-gate: release: uses: repobuddy/.github/.github/workflows/pnpm-release-changeset-oidc.yml@v2 needs: [code, publish-gate] - secrets: - RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} - RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} ``` It **blocks** on files that must never ship (tests, key material, env files, repo From e39adb232860d82d4d36e1012637e680b665bc3d Mon Sep 17 00:00:00 2001 From: unional Date: Sat, 10 Oct 2026 21:35:27 -0700 Subject: [PATCH 2/2] fix(release): do not declare actions: write in the reusable workflow A called workflow cannot request a permission its caller did not grant; the run fails at startup. Existing callers grant only id-token, contents and pull-requests, so declaring actions: write would break every release. The dispatch stays fail-soft: without the caller grant it 403s and warns, now with a message that names the missing actions: write grant. Co-Authored-By: Claude Opus 5.5 Signed-off-by: unional --- .github/workflows/pnpm-release-changeset-oidc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pnpm-release-changeset-oidc.yml b/.github/workflows/pnpm-release-changeset-oidc.yml index e5354c1..00695b3 100644 --- a/.github/workflows/pnpm-release-changeset-oidc.yml +++ b/.github/workflows/pnpm-release-changeset-oidc.yml @@ -66,8 +66,6 @@ permissions: contents: write # Lets changesets/action open the "version packages" PR. pull-requests: write - # Lets the job dispatch the caller's CI on the version branch (see the header). - actions: write concurrency: ${{ github.workflow }}-${{ github.ref }} @@ -133,6 +131,8 @@ jobs: echo "$out" if grep -qi 'workflow_dispatch' <<<"$out"; then echo "::warning::$CI_WORKFLOW has no workflow_dispatch trigger, so the version packages PR has no checks. Add 'workflow_dispatch:' to its 'on:' block." + elif grep -qiE '403|Resource not accessible' <<<"$out"; then + echo "::warning::GITHUB_TOKEN may not dispatch $CI_WORKFLOW, so the version packages PR has no checks. Grant 'actions: write' in the caller's 'permissions:' for the job that calls this workflow." else echo "::warning::Could not dispatch $CI_WORKFLOW on $VERSION_BRANCH, so the version packages PR has no checks. Check that the caller grants 'actions: write' and that ci-workflow names its CI workflow file." fi