Skip to content

Commit 322451a

Browse files
authored
ci: publish releases with npm trusted publishing (#785)
* ci: publish releases with npm trusted publishing Move releases off the local machine and into GitHub Actions. The new Release workflow is dispatched manually with a bump type and dist-tag, then bumps the version, tags it, builds, publishes to npm over OIDC and creates the GitHub release. No npm token is needed anywhere, and packages get provenance for free. The local `release` scripts are removed so nobody publishes from a laptop by accident, along with the dead semantic-release config block that no longer matched how releases were cut. * ci: push the version commit with a GitHub App token `main` is protected and the repository is user-owned, so the GitHub Actions app cannot be added as a ruleset bypass actor. Mint a short-lived token from a dedicated GitHub App instead, and use it for the checkout so bumpp can push the version commit and tag. GITHUB_TOKEN drops to contents: read, since everything that writes now goes through the app token.
1 parent 1b3fe06 commit 322451a

4 files changed

Lines changed: 113 additions & 23 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: "Version bump"
8+
type: choice
9+
default: patch
10+
options:
11+
- patch
12+
- minor
13+
- major
14+
- prepatch
15+
- preminor
16+
- premajor
17+
- prerelease
18+
tag:
19+
description: "npm dist-tag"
20+
type: string
21+
default: latest
22+
23+
concurrency:
24+
group: release
25+
cancel-in-progress: false
26+
27+
permissions: {}
28+
29+
jobs:
30+
release:
31+
runs-on: ubuntu-latest
32+
permissions:
33+
contents: read # Everything that writes uses the GitHub App token below
34+
id-token: write # Mint the OIDC token npm trusted publishing requires
35+
36+
defaults:
37+
run:
38+
working-directory: packages/react-intersection-observer
39+
40+
steps:
41+
- name: Mint GitHub App token
42+
# `main` is protected, and the GitHub Actions app cannot be a ruleset
43+
# bypass actor on a user-owned repository. This app can, so the version
44+
# commit is pushed with a short-lived token minted here and revoked
45+
# when the job ends.
46+
id: app-token
47+
uses: actions/create-github-app-token@v2
48+
with:
49+
app-id: ${{ vars.RELEASE_APP_ID }}
50+
private-key: ${{ secrets.RELEASE_APP_KEY }}
51+
- uses: actions/checkout@v7
52+
with:
53+
fetch-depth: 0
54+
token: ${{ steps.app-token.outputs.token }}
55+
- run: corepack enable
56+
working-directory: .
57+
- name: Setup Node.js
58+
uses: actions/setup-node@v6
59+
with:
60+
node-version: 24
61+
cache: "pnpm"
62+
registry-url: "https://registry.npmjs.org"
63+
- name: Update npm
64+
# Trusted publishing requires npm 11.5.1 or newer
65+
run: npm install -g npm@latest
66+
- name: Install dependencies
67+
run: pnpm install --frozen-lockfile
68+
working-directory: .
69+
- name: Configure git
70+
run: |
71+
git config user.name "github-actions[bot]"
72+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
73+
- name: Bump version, commit and tag
74+
run: pnpm exec bumpp ${{ inputs.version }} --yes
75+
- name: Read version
76+
id: version
77+
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
78+
- name: Build
79+
run: pnpm build
80+
working-directory: .
81+
- name: Publish to npm
82+
# No NODE_AUTH_TOKEN: npm authenticates through the OIDC token, and
83+
# provenance is attested automatically by trusted publishing.
84+
run: npm publish --tag ${{ inputs.tag }}
85+
- name: Create GitHub release
86+
run: gh release create "v${{ steps.version.outputs.version }}" --generate-notes ${{ startsWith(inputs.version, 'pre') && '--prerelease' || '' }}
87+
working-directory: .
88+
env:
89+
GH_TOKEN: ${{ steps.app-token.outputs.token }}

‎CONTRIBUTING.md‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,10 @@ Fork the repository and create a branch for your feature/bug fix.
5050

5151
### Commit Message Conventions
5252

53-
- We use
54-
[semantic-release](https://github.com/semantic-release/semantic-release) to
55-
manage releases automatically. To ensure that releases are automatically
56-
versioned correctly, we follow the
53+
- We follow the
5754
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
58-
Conventions. This means that your commit messages should have the following
59-
format:
55+
Conventions, so the generated release notes stay readable. This means that
56+
your commit messages should have the following format:
6057

6158
`<type>: <subject>`
6259

@@ -92,3 +89,24 @@ Build every published and documentation surface with:
9289
```shell
9390
pnpm build:all
9491
```
92+
93+
## Releasing
94+
95+
Releases are published from CI with
96+
[npm trusted publishing](https://docs.npmjs.com/trusted-publishers), so no npm
97+
token exists anywhere and packages are published with provenance. There is no
98+
local publish step, and `npm publish` from a laptop will be rejected.
99+
100+
To cut a release, a maintainer opens the **Actions** tab, selects the
101+
**Release** workflow and runs it from the branch to release:
102+
103+
- `version` picks the bump (`patch`, `minor`, `major` or a `pre*` variant).
104+
- `tag` picks the npm dist-tag, `latest` by default. Use `beta` for prereleases.
105+
106+
The workflow bumps the version, commits and tags it, builds the package,
107+
publishes it to npm, and creates a GitHub release with generated notes.
108+
109+
`main` is protected, so the version commit is pushed with a short-lived token
110+
minted from a GitHub App that is listed as a bypass actor on the branch
111+
ruleset. The app's id lives in the `RELEASE_APP_ID` variable and its private key
112+
in the `RELEASE_APP_KEY` secret.

‎package.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
"dev:storybook": "pnpm --filter storybook dev",
1414
"docs:build": "pnpm --filter docs build",
1515
"lint": "pnpm --filter react-intersection-observer lint && pnpm --filter storybook lint && pnpm --filter docs lint",
16-
"release": "pnpm --filter react-intersection-observer release",
1716
"test": "pnpm --filter react-intersection-observer test",
1817
"typecheck": "pnpm build && pnpm --filter react-intersection-observer typecheck && pnpm --filter storybook typecheck && pnpm --filter docs typecheck",
1918
"turbo:build": "turbo run build",

‎packages/react-intersection-observer/package.json‎

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
"postbuild": "npm_config_cache=.npm-cache attw --pack && publint && size-limit",
4949
"dev": "run-p dev:*",
5050
"dev:package": "tsup src/index.tsx --watch",
51-
"release": "bumpp && npm publish",
5251
"preview": "pnpx pkg-pr-new publish --no-template",
5352
"lint": "biome check .",
5453
"version": "pnpm build",
@@ -69,21 +68,6 @@
6968
"useInView",
7069
"useIntersectionObserver"
7170
],
72-
"release": {
73-
"branches": [
74-
"main",
75-
{
76-
"name": "beta",
77-
"prerelease": true
78-
}
79-
],
80-
"plugins": [
81-
"@semantic-release/commit-analyzer",
82-
"@semantic-release/release-notes-generator",
83-
"@semantic-release/npm",
84-
"@semantic-release/github"
85-
]
86-
},
8771
"size-limit": [
8872
{
8973
"path": "dist/index.mjs",

0 commit comments

Comments
 (0)