Skip to content

Commit f7d1e57

Browse files
theoephraimclaude
andcommitted
Add npm auth setup for trusted publishing (OIDC) and token-based auth
Bumpy now automatically handles npm authentication before publishing: - Detects GitHub Actions OIDC and validates npm version (>= 11.5.1) - Auto-configures .npmrc when NPM_TOKEN/NODE_AUTH_TOKEN env vars are set - Warns with actionable suggestions when no auth is detected in CI Updated workflow examples to show both OIDC (primary) and token-based auth. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7e4e8f8 commit f7d1e57

4 files changed

Lines changed: 149 additions & 12 deletions

File tree

.github/workflows/release.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ jobs:
99
permissions:
1010
contents: write
1111
pull-requests: write
12-
id-token: write
12+
id-token: write # required for npm trusted publishing (OIDC)
1313
steps:
1414
- uses: actions/checkout@v4
1515
with:
1616
fetch-depth: 0
1717
- uses: oven-sh/setup-bun@v2
18+
- run: npm install -g npm@latest # npm >= 11.5.1 required for trusted publishing
1819
- run: bun install
1920
# Build first since we use the local workspace package instead of the published one
2021
- run: bun run --filter @varlock/bumpy build

README.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ jobs:
9595
GH_TOKEN: ${{ github.token }}
9696
```
9797
98-
**Release** — create a "Version Packages" PR on merge to main:
98+
**Release** — create a "Version Packages" PR on merge to main, publish when merged:
9999
100100
```yaml
101-
# .github/workflows/bumpy-release.yml
101+
# .github/workflows/bumpy-release.yml — trusted publishing (OIDC, no secret needed)
102102
name: Bumpy Release
103103
on:
104104
push:
@@ -110,18 +110,51 @@ jobs:
110110
permissions:
111111
contents: write
112112
pull-requests: write
113-
id-token: write
113+
id-token: write # required for npm trusted publishing (OIDC)
114114
steps:
115115
- uses: actions/checkout@v4
116116
with:
117117
fetch-depth: 0
118118
- uses: oven-sh/setup-bun@v2
119+
- run: npm install -g npm@latest # npm >= 11.5.1 required for trusted publishing
119120
- run: bun install
120121
- run: bunx @varlock/bumpy ci release
121122
env:
122123
GH_TOKEN: ${{ github.token }}
123124
```
124125
126+
> **Trusted publishing setup:** Configure each package on [npmjs.com](https://docs.npmjs.com/trusted-publishers/) → Package Settings → Trusted Publishers → GitHub Actions. Specify your org/user, repo, and the workflow filename (`bumpy-release.yml`). No `NPM_TOKEN` secret needed.
127+
128+
<details>
129+
<summary>Alternative: token-based auth (NPM_TOKEN secret)</summary>
130+
131+
```yaml
132+
# .github/workflows/bumpy-release.yml — token-based auth
133+
name: Bumpy Release
134+
on:
135+
push:
136+
branches: [main]
137+
138+
jobs:
139+
release:
140+
runs-on: ubuntu-latest
141+
permissions:
142+
contents: write
143+
pull-requests: write
144+
steps:
145+
- uses: actions/checkout@v4
146+
with:
147+
fetch-depth: 0
148+
- uses: oven-sh/setup-bun@v2
149+
- run: bun install
150+
- run: bunx @varlock/bumpy ci release
151+
env:
152+
GH_TOKEN: ${{ github.token }}
153+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
154+
```
155+
156+
</details>
157+
125158
Or use `bumpy ci release --auto-publish` to version + publish directly without a PR.
126159

127160
## AI Integration

llms.md

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ jobs:
586586
```
587587
588588
```yaml
589-
# .github/workflows/bumpy-release.yml
589+
# .github/workflows/bumpy-release.yml — trusted publishing (OIDC, no secret needed)
590590
name: Bumpy Release
591591
on:
592592
push:
@@ -598,22 +598,47 @@ jobs:
598598
permissions:
599599
contents: write
600600
pull-requests: write
601-
id-token: write # for npm provenance
601+
id-token: write # required for npm trusted publishing (OIDC)
602602
steps:
603603
- uses: actions/checkout@v4
604604
with:
605605
fetch-depth: 0
606606
- uses: oven-sh/setup-bun@v2
607+
- run: npm install -g npm@latest # npm >= 11.5.1 required for trusted publishing
607608
- run: bun install
608-
# Option A: Create a "Version Packages" PR
609609
- run: bunx @varlock/bumpy ci release
610610
env:
611611
GH_TOKEN: ${{ github.token }}
612-
# Option B: Auto-publish directly
613-
# - run: bunx @varlock/bumpy ci release --auto-publish
614-
# env:
615-
# GH_TOKEN: ${{ github.token }}
616-
# NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
612+
```
613+
614+
Trusted publishing setup: configure each package on npmjs.com → Package Settings → Trusted Publishers → GitHub Actions.
615+
Specify your org/user, repo, and the workflow filename. No NPM_TOKEN secret needed.
616+
617+
Alternative: token-based auth (uses `NPM_TOKEN` secret instead of OIDC):
618+
619+
```yaml
620+
# .github/workflows/bumpy-release.yml — token-based auth
621+
name: Bumpy Release
622+
on:
623+
push:
624+
branches: [main]
625+
626+
jobs:
627+
release:
628+
runs-on: ubuntu-latest
629+
permissions:
630+
contents: write
631+
pull-requests: write
632+
steps:
633+
- uses: actions/checkout@v4
634+
with:
635+
fetch-depth: 0
636+
- uses: oven-sh/setup-bun@v2
637+
- run: bun install
638+
- run: bunx @varlock/bumpy ci release
639+
env:
640+
GH_TOKEN: ${{ github.token }}
641+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
617642
```
618643

619644
### Non-interactive changeset creation (AI/CI)

packages/bumpy/src/core/publish-pipeline.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { resolve } from 'node:path';
2+
import { existsSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs';
23
import { unlink } from 'node:fs/promises';
34
import { readJson, writeJson } from '../utils/fs.ts';
45
import { runAsync } from '../utils/shell.ts';
6+
import { tryRun } from '../utils/shell.ts';
57
import { log, colorize } from '../utils/logger.ts';
68
import { createTag, tagExists } from './git.ts';
79
import { DependencyGraph } from './dep-graph.ts';
@@ -20,6 +22,79 @@ export interface PublishResult {
2022
failed: { name: string; error: string }[];
2123
}
2224

25+
/** Check if GitHub Actions OIDC is available (id-token: write permission granted) */
26+
function isOidcAvailable(): boolean {
27+
return !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
28+
}
29+
30+
/**
31+
* Set up npm authentication for publishing.
32+
*
33+
* Handles three scenarios:
34+
* 1. **Trusted publishing (OIDC)** — GitHub Actions with `id-token: write`.
35+
* npm >= 11.5.1 authenticates automatically via OIDC token exchange.
36+
* No secret needed, but we check the npm version and warn if too old.
37+
* 2. **Token-based auth** — `NPM_TOKEN` or `NODE_AUTH_TOKEN` env var.
38+
* Writes a project-level `.npmrc` so npm can authenticate.
39+
* 3. **Pre-configured** — user already has `.npmrc` with auth (e.g. via `actions/setup-node`).
40+
*/
41+
function setupNpmAuth(rootDir: string, publishManager: string): void {
42+
// Only relevant when publishing via npm CLI
43+
if (publishManager !== 'npm') return;
44+
45+
const npmrcPath = resolve(rootDir, '.npmrc');
46+
const existingNpmrc = existsSync(npmrcPath) ? readFileSync(npmrcPath, 'utf-8') : '';
47+
const hasAuthConfigured = existingNpmrc.includes(':_authToken=');
48+
49+
// If auth is already configured (e.g. via actions/setup-node), nothing to do
50+
if (hasAuthConfigured) {
51+
log.dim(' Using existing .npmrc auth configuration');
52+
return;
53+
}
54+
55+
// Scenario 1: OIDC trusted publishing
56+
if (isOidcAvailable()) {
57+
const npmVersion = tryRun('npm --version');
58+
if (npmVersion) {
59+
const [major, minor, patch] = npmVersion.split('.').map(Number);
60+
const meetsMinVersion = major! > 11 || (major === 11 && (minor! > 5 || (minor === 5 && patch! >= 1)));
61+
if (!meetsMinVersion) {
62+
log.warn(` npm ${npmVersion} detected — trusted publishing (OIDC) requires npm >= 11.5.1`);
63+
log.warn(' Add "npm install -g npm@latest" to your workflow before publishing');
64+
} else {
65+
log.dim(` OIDC detected — npm ${npmVersion} will authenticate via trusted publishing`);
66+
}
67+
}
68+
return;
69+
}
70+
71+
// Scenario 2: Token-based auth via environment variable
72+
// Support NPM_TOKEN (common convention) by mapping to NODE_AUTH_TOKEN (what npm reads from .npmrc)
73+
const token = process.env.NODE_AUTH_TOKEN || process.env.NPM_TOKEN;
74+
if (token) {
75+
if (process.env.NPM_TOKEN && !process.env.NODE_AUTH_TOKEN) {
76+
process.env.NODE_AUTH_TOKEN = token;
77+
}
78+
const authLine = '//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}';
79+
if (existingNpmrc) {
80+
appendFileSync(npmrcPath, `\n${authLine}\n`);
81+
} else {
82+
writeFileSync(npmrcPath, `${authLine}\n`);
83+
}
84+
log.dim(' Configured .npmrc with auth token');
85+
return;
86+
}
87+
88+
// No auth detected — warn
89+
if (process.env.CI) {
90+
log.warn(' No npm authentication detected. Publishing will likely fail.');
91+
log.warn(' Options:');
92+
log.warn(' • Trusted publishing (OIDC): add `id-token: write` permission + npm >= 11.5.1');
93+
log.warn(' • Token auth: set NPM_TOKEN or NODE_AUTH_TOKEN environment variable');
94+
log.warn(' • Manual: add `actions/setup-node` with `registry-url` to your workflow');
95+
}
96+
}
97+
2398
/**
2499
* Publish all packages in the release plan.
25100
* Order: topological (dependencies published before dependents).
@@ -37,6 +112,9 @@ export async function publishPackages(
37112
const result: PublishResult = { published: [], skipped: [], failed: [] };
38113
const publishConfig = config.publish;
39114

115+
// Set up npm authentication before publishing
116+
setupNpmAuth(rootDir, publishConfig.publishManager);
117+
40118
// Resolve "auto" pack manager to detected PM
41119
const packManager = publishConfig.packManager === 'auto' ? detectedPm : publishConfig.packManager;
42120

0 commit comments

Comments
 (0)