Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 46 additions & 27 deletions .github/workflows/pnpm-release-changeset-oidc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ci-workflow> --ref changeset-release/<branch>`. 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/<name>/access naming the repo and the *caller* workflow's
Expand All @@ -32,18 +40,20 @@ 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
node-version:
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'
Expand All @@ -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

Expand Down Expand Up @@ -112,9 +111,29 @@ 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."
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
35 changes: 18 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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: <file>`.

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 <ci-workflow> --ref changeset-release/<branch>`. 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?

Expand Down Expand Up @@ -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
Expand Down