diff --git a/.github/workflows/pnpm-release-changeset-oidc.yml b/.github/workflows/pnpm-release-changeset-oidc.yml index 3b210f1..1e16bc9 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' @@ -63,24 +73,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 @@ -116,4 +115,25 @@ jobs: version: pnpm run version publish: 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.pullRequestNumber != '' && 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." + 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 + fi diff --git a/README.md b/README.md index 74a7f44..0deb578 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,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; @@ -213,24 +213,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?