Skip to content

Commit 10366ad

Browse files
authored
feat(ci): publish to npm via OIDC trusted publishing (#124)
1 parent 4268705 commit 10366ad

4 files changed

Lines changed: 54 additions & 49 deletions

File tree

.github/workflows/draft.yml

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,19 @@ jobs:
3737
env:
3838
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3939
PublishBetaPackages:
40-
uses: ./.github/workflows/publish.yml
40+
runs-on: ubuntu-latest
41+
timeout-minutes: 5
4142
if: github.event_name == 'push'
4243
permissions:
43-
contents: write
44-
with:
45-
tag_name: ${{ needs.PublishBetaRelease.outputs.tag_name }}
46-
target_commitish: ${{ github.sha }}
47-
secrets:
48-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
44+
actions: write
45+
steps:
46+
- name: Dispatch publish workflow
47+
run: |-
48+
gh workflow run publish.yml --repo ${{ github.repository }} \
49+
-f tag_name="${{ needs.PublishBetaRelease.outputs.tag_name }}" \
50+
-f target_commitish="${{ github.sha }}"
51+
env:
52+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4953
needs:
5054
- PublishBetaRelease
5155
UpdateReleaseDraft:

.github/workflows/publish.yml

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,21 @@ name: Publish Release
88
release:
99
types:
1010
- published
11-
workflow_call:
11+
workflow_dispatch:
1212
inputs:
1313
tag_name:
1414
required: true
1515
type: string
1616
target_commitish:
1717
required: true
1818
type: string
19-
secrets:
20-
NPM_TOKEN:
21-
required: true
2219
jobs:
2320
PublishPackages:
2421
runs-on: ubuntu-latest
2522
timeout-minutes: 20
2623
permissions:
2724
contents: write
25+
id-token: write
2826
steps:
2927
- name: Checkout
3028
uses: actions/checkout@v4
@@ -33,11 +31,11 @@ jobs:
3331
- name: Install Node
3432
uses: actions/setup-node@v4
3533
with:
36-
node-version: 20
34+
node-version: 24
3735
- name: Install pnpm
3836
uses: pnpm/action-setup@v4
3937
with:
40-
version: 9
38+
version: 10
4139
- name: Install Dependencies
4240
run: pnpm install --no-frozen-lockfile
4341
- name: Run Build
@@ -50,10 +48,6 @@ jobs:
5048
(cd packages/lib && npm version --no-git-tag-version ${{ inputs.tag_name || github.event.release.tag_name }})
5149
(cd packages/cli && npm version --no-git-tag-version ${{ inputs.tag_name || github.event.release.tag_name }})
5250
(cd packages/actions && npm version --no-git-tag-version ${{ inputs.tag_name || github.event.release.tag_name }})
53-
- name: Setup npm auth
54-
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
55-
env:
56-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
5751
- name: Publish packages
5852
run: |-
5953
TAG_NAME="${{ inputs.tag_name || github.event.release.tag_name }}"
@@ -67,8 +61,6 @@ jobs:
6761
echo "Publishing with latest tag"
6862
pnpm -r publish --access public --no-git-checks
6963
fi
70-
env:
71-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
7264
CommitVersionBump:
7365
runs-on: ubuntu-latest
7466
timeout-minutes: 20

workflows/draft.wac.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {
22
Workflow,
33
NormalJob,
4-
ReusableWorkflowCallJob,
54
Step,
65
expressions as ex,
6+
dedentString,
77
} from '../packages/lib/src/index.js'
88

99
const betaReleaseStep = new Step({
@@ -34,20 +34,33 @@ const betaReleaseJob = new NormalJob('PublishBetaRelease', {
3434
},
3535
}).addStep(betaReleaseStep)
3636

37-
const publishBetaJob = new ReusableWorkflowCallJob('PublishBetaPackages', {
38-
uses: './.github/workflows/publish.yml',
37+
// Dispatches publish.yml instead of calling it as a reusable workflow: npm
38+
// trusted publishing (OIDC) validates the top-level workflow's filename
39+
// against the package's single trusted publisher, which is publish.yml.
40+
// GITHUB_TOKEN-triggered events don't start workflows, but workflow_dispatch
41+
// is exempt from that rule.
42+
const publishBetaJob = new NormalJob('PublishBetaPackages', {
43+
'runs-on': 'ubuntu-latest',
44+
'timeout-minutes': 5,
3945
if: "github.event_name == 'push'",
4046
permissions: {
41-
contents: 'write',
42-
},
43-
with: {
44-
tag_name: ex.expn(`needs.${betaReleaseJob.name}.outputs.tag_name`),
45-
target_commitish: ex.expn('github.sha'),
46-
},
47-
secrets: {
48-
NPM_TOKEN: ex.secret('NPM_TOKEN'),
47+
actions: 'write',
4948
},
50-
}).needs([betaReleaseJob])
49+
})
50+
.addStep(
51+
new Step({
52+
name: 'Dispatch publish workflow',
53+
run: dedentString(`
54+
gh workflow run publish.yml --repo ${ex.expn('github.repository')} \\
55+
-f tag_name="${ex.expn(`needs.${betaReleaseJob.name}.outputs.tag_name`)}" \\
56+
-f target_commitish="${ex.expn('github.sha')}"
57+
`),
58+
env: {
59+
GH_TOKEN: ex.secret('GITHUB_TOKEN'),
60+
},
61+
}),
62+
)
63+
.needs([betaReleaseJob])
5164

5265
const draftStep = new Step({
5366
name: 'Draft next release',

workflows/publish.wac.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ const checkout = new Step({
2222
const installNode = new Step({
2323
name: 'Install Node',
2424
uses: 'actions/setup-node@v4',
25-
with: { 'node-version': 20 },
25+
// npm trusted publishing (OIDC) requires Node >= 22.14
26+
with: { 'node-version': 24 },
2627
})
2728

2829
const installPnpm = new Step({
2930
name: 'Install pnpm',
3031
uses: 'pnpm/action-setup@v4',
31-
with: { version: 9 },
32+
// pnpm 10 supports OIDC trusted publishing; reads the same v9 lockfile
33+
with: { version: 10 },
3234
})
3335

3436
const installDependencies = new Step({
@@ -54,14 +56,10 @@ const bumpVersions = new Step({
5456
`),
5557
})
5658

57-
const setupNpmAuth = new Step({
58-
name: 'Setup npm auth',
59-
run: 'echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc',
60-
env: {
61-
NPM_TOKEN: ex.secret('NPM_TOKEN'),
62-
},
63-
})
64-
59+
// Auth is handled by npm trusted publishing (OIDC): the job's id-token
60+
// permission lets pnpm mint a short-lived token from the runner's OIDC
61+
// identity. The trusted publisher configured on npmjs.com must reference
62+
// this workflow file (publish.yml).
6563
const publishPackages = new Step({
6664
name: 'Publish packages',
6765
run: dedentString(`
@@ -77,16 +75,14 @@ const publishPackages = new Step({
7775
pnpm -r publish --access public --no-git-checks
7876
fi
7977
`),
80-
env: {
81-
NPM_TOKEN: ex.secret('NPM_TOKEN'),
82-
},
8378
})
8479

8580
const publishJob = new NormalJob('PublishPackages', {
8681
'runs-on': 'ubuntu-latest',
8782
'timeout-minutes': 20,
8883
permissions: {
8984
contents: 'write',
85+
'id-token': 'write',
9086
},
9187
}).addSteps([
9288
checkout,
@@ -95,7 +91,6 @@ const publishJob = new NormalJob('PublishPackages', {
9591
installDependencies,
9692
build,
9793
bumpVersions,
98-
setupNpmAuth,
9994
publishPackages,
10095
])
10196

@@ -139,14 +134,15 @@ export const publishWorkflow = new Workflow('publish', {
139134
release: {
140135
types: ['published'],
141136
},
142-
workflow_call: {
137+
// Dispatched (not workflow_call) by draft.yml for beta releases: npm
138+
// trusted publishing validates the top-level workflow's filename, and a
139+
// package can only have one trusted publisher — so every publish must
140+
// run with publish.yml as the top-level workflow.
141+
workflow_dispatch: {
143142
inputs: {
144143
tag_name: { required: true, type: 'string' },
145144
target_commitish: { required: true, type: 'string' },
146145
},
147-
secrets: {
148-
NPM_TOKEN: { required: true },
149-
},
150146
},
151147
},
152148
}).addJobs([publishJob, commitVersionBumpJob])

0 commit comments

Comments
 (0)