Skip to content

Commit 009313c

Browse files
committed
oicd
1 parent b9d8388 commit 009313c

File tree

4 files changed

+147
-9
lines changed

4 files changed

+147
-9
lines changed

.github/workflows/test.yml

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
id: publish
3131
run: npx . publish "./test" --webhook "${{ secrets.DISCORD_WEBHOOK_TEST_RELEASE }}" --access-token "${{ secrets.NPM_TOKEN }}" --version+hash --tag github --version+tag --create-tag "test/" --llm-api-key "${{ secrets.LLM_API_KEY }}"
3232

33-
- name: Print output
33+
- name: Print output
3434
run: |
3535
echo "Package version: ${{ steps.publish.outputs.package-version }}"
3636
@@ -42,3 +42,49 @@ jobs:
4242

4343
- name: Just add a tag
4444
run: npx . publish "./test" --webhook "${{ secrets.DISCORD_WEBHOOK_TEST_RELEASE }}" --access-token "${{ secrets.NPM_TOKEN }}" --tag github-2
45+
46+
# OIDC-based publishing (Trusted Publishing)
47+
# NOTE: First publish of a package MUST use --access-token. OIDC only works for existing packages.
48+
#
49+
# To enable OIDC for this package:
50+
# 1. Ensure the package exists on npmjs.com (publish once with --access-token)
51+
# 2. Go to https://www.npmjs.com/package/publish-helper-test-package/access
52+
# 3. Click "Settings" → "Trusted Publisher" → "GitHub Actions"
53+
# 4. Configure: owner (needle-tools), repository (npm-publish-helper), workflow (test.yml)
54+
publish-oidc:
55+
runs-on: ubuntu-latest
56+
timeout-minutes: 5
57+
permissions:
58+
contents: read
59+
id-token: write # Required for OIDC authentication
60+
defaults:
61+
run:
62+
working-directory: ./
63+
64+
steps:
65+
- uses: actions/checkout@v4
66+
67+
- name: Setup Node.js
68+
uses: actions/setup-node@v4
69+
with:
70+
node-version: '22' # npm >= 11.5 required for OIDC
71+
# Note: Do NOT set registry-url here for OIDC - it creates .npmrc expecting NODE_AUTH_TOKEN
72+
# which conflicts with OIDC. Let npm use its default registry.
73+
74+
- name: Check npm version
75+
run: |
76+
echo "npm version: $(npm --version)"
77+
echo "node version: $(node --version)"
78+
echo "npmrc contents (if any):"
79+
cat ~/.npmrc 2>/dev/null || echo "(no .npmrc)"
80+
81+
- name: Install dependencies
82+
run: npm install
83+
84+
- name: Run publish with OIDC
85+
id: publish-oidc
86+
run: npx . publish "./test" --webhook "${{ secrets.DISCORD_WEBHOOK_TEST_RELEASE }}" --oidc --version+hash --tag oidc --version+tag
87+
88+
- name: Print output
89+
run: |
90+
echo "Package version: ${{ steps.publish-oidc.outputs.package-version }}"

bin/cli.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ program.command('publish', 'Publish npm package')
6868
.option("--create-tag", "Create a git tag. Default: null. Can be set to e.g. '--create-tag release/'", { required: false, validator: program.STRING })
6969
.option("--webhook <webhook>", "Webhook URL to send notifications", { required: false, validator: program.STRING })
7070
.option("--access-token <access-token>", "NPM access token", { required: false, validator: program.STRING })
71+
.option("--oidc", "Use OIDC (OpenID Connect) for authentication instead of access tokens. Requires npm >= 11.5 and a trusted publisher configured on npmjs.com. Works with GitHub Actions and GitLab CI/CD.", { required: false, validator: program.BOOLEAN, default: false })
7172
.option("--dry-run", "Dry run mode, do not publish", { required: false, validator: program.BOOLEAN, default: false })
7273
.option("--override-name <name>", "Override package name", { required: false, validator: program.STRING })
7374
.option("--override-version <version>", "Override package version", { required: false, validator: program.STRING })
@@ -93,6 +94,7 @@ program.command('publish', 'Publish npm package')
9394

9495
registry: registry,
9596
accessToken: options.accessToken?.toString() || null,
97+
useOidc: options.oidc === true,
9698
useHashInVersion: options.versionHash === true, // default to false
9799
useTagInVersion: options.versionTag === true, // default to false
98100
createGitTag: options.createTag !== undefined, // default to false

src/publish.js

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,21 @@ export async function publish(args) {
5858
logger.info(`Package: ${packageJson.name}@${packageJson.version}`);
5959
logger.info(`Build time: ${buildTime}`);
6060
logger.info(`Short SHA: ${shortSha}`);
61-
logger.info(`Token: '${obfuscateToken(args.accessToken)}'`);
61+
if (args.useOidc) {
62+
logger.info(`Authentication: OIDC (Trusted Publishing)`);
63+
} else {
64+
logger.info(`Token: '${obfuscateToken(args.accessToken)}'`);
65+
}
6266
logger.info(`Repository: ${repoUrl}`);
6367
logger.info(`Last commit message: ${commitMessage}`);
6468
logger.info(`Last commit author: ${commitAuthorWithEmail}`);
6569
logger.info(`Registry: ${args.registry || 'https://registry.npmjs.org/'}`);
6670

67-
if (!args.accessToken?.length) {
68-
logger.warn(`No access token provided. Publishing to registry ${args.registry} may fail.`);
71+
if (!args.useOidc && !args.accessToken?.length) {
72+
logger.warn(`No access token provided and OIDC not enabled. Publishing to registry ${args.registry} may fail.`);
73+
}
74+
if (args.useOidc) {
75+
logger.info(`OIDC authentication enabled. Ensure your CI/CD has id-token: write permissions and a trusted publisher is configured on npmjs.com.`);
6976
}
7077

7178
// Remove slahes from the end of the tag (this may happen if the tag is provided by github ref_name
@@ -126,7 +133,7 @@ export async function publish(args) {
126133
msg += `Commit URL: ${repoUrl}/commit/${shortSha}\n`;
127134
msg += `Build time: ${buildTime}\n`;
128135
msg += `Registry: ${args.registry}\n`;
129-
msg += `Token: ${obfuscateToken(args.accessToken)}\n`;
136+
msg += `Auth: ${args.useOidc ? 'OIDC (Trusted Publishing)' : obfuscateToken(args.accessToken)}\n`;
130137
msg += `Tag: ${args.tag || '-'}${args.useTagInVersion ? ' (version+tag)' : ''}${args.createGitTag ? ' (creating git tag)' : ''}\n`;
131138
msg += "```";
132139
await sendMessageToWebhook(webhook, msg, { logger });
@@ -197,13 +204,13 @@ export async function publish(args) {
197204
// Default env
198205
const env = {
199206
...process.env,
200-
NPM_TOKEN: args.accessToken || undefined,
207+
NPM_TOKEN: args.useOidc ? undefined : (args.accessToken || undefined),
201208
NPM_CONFIG_REGISTRY: (args.registry || 'https://registry.npmjs.org/'),
202209
}
203210

204211

205212
// set config
206-
{
213+
if (!args.useOidc) {
207214
let registryUrlWithoutScheme = (args.registry || 'https://registry.npmjs.org/').replace(/https?:\/\//, '');
208215
if (!registryUrlWithoutScheme.endsWith('/')) registryUrlWithoutScheme += '/';
209216

@@ -223,6 +230,19 @@ export async function publish(args) {
223230
// env
224231
// });
225232
// }
233+
} else {
234+
logger.info(`Skipping npm config auth token setup - using OIDC authentication`);
235+
// Clear any existing auth tokens that might interfere with OIDC
236+
// npm OIDC works best when there's no conflicting token configuration
237+
let registryUrlWithoutScheme = (args.registry || 'https://registry.npmjs.org/').replace(/https?:\/\//, '');
238+
if (!registryUrlWithoutScheme.endsWith('/')) registryUrlWithoutScheme += '/';
239+
try {
240+
const clearCmd = `npm config delete //${registryUrlWithoutScheme}:_authToken`;
241+
logger.info(`Clearing any existing auth token for OIDC: ${clearCmd}`);
242+
tryExecSync(clearCmd, { cwd: packageDirectory, env });
243+
} catch {
244+
// Ignore errors - token might not exist
245+
}
226246
}
227247

228248
const htmlUrl = args.registry?.includes("npmjs") ? `https://www.npmjs.com/package/${packageJson.name}/v/${packageJson.version}` : (args.registry + `/${packageJson.name}`);
@@ -257,7 +277,12 @@ export async function publish(args) {
257277
logger.info(`Package view result ${packageVersionPublished}`);
258278

259279

260-
let cmd = `npm publish --access public`
280+
const registryUrl = args.registry || 'https://registry.npmjs.org/';
281+
let cmd = `npm publish --access public --registry ${registryUrl}`
282+
if (args.useOidc) {
283+
cmd += ' --provenance';
284+
logger.info(`OIDC authentication enabled, adding --provenance flag for trusted publishing.`);
285+
}
261286
if (dryRun) {
262287
cmd += ' --dry-run';
263288
logger.info(`Dry run mode enabled, not actually publishing package.`);
@@ -294,8 +319,66 @@ export async function publish(args) {
294319
}
295320
else {
296321
logger.error(`❌ Failed to publish package ${packageJson.name}@${packageJson.version}\n${res.error}`);
322+
323+
// Provide helpful OIDC troubleshooting information if OIDC was enabled
324+
if (args.useOidc) {
325+
const errorStr = res.error?.toString() || res.output?.toString() || '';
326+
const is404Error = errorStr.includes('404') || errorStr.includes('Not found') || errorStr.includes('Not Found');
327+
const is401Error = errorStr.includes('401') || errorStr.includes('Unauthorized');
328+
const is403Error = errorStr.includes('403') || errorStr.includes('Forbidden');
329+
330+
logger.error(`\n${'='.repeat(80)}`);
331+
logger.error(`OIDC TROUBLESHOOTING GUIDE`);
332+
logger.error(`${'='.repeat(80)}`);
333+
334+
if (is404Error) {
335+
logger.error(`\n⚠️ ERROR: 404 Not Found`);
336+
logger.error(` This usually means one of the following:\n`);
337+
logger.error(` a) FIRST-TIME PUBLISH: The package doesn't exist on npmjs.com yet.`);
338+
logger.error(` → First publish MUST be done with a token: --access-token <token>`);
339+
logger.error(` → After first publish, configure trusted publisher, then use --oidc\n`);
340+
logger.error(` b) TRUSTED PUBLISHER NOT CONFIGURED for this package.`);
341+
logger.error(` → Go to: https://www.npmjs.com/package/${packageJson.name}/access`);
342+
logger.error(` → Click "Settings" → "Trusted Publisher" → "GitHub Actions"`);
343+
logger.error(` → Configure: organization/user, repository, workflow filename\n`);
344+
logger.error(` c) WORKFLOW FILENAME MISMATCH`);
345+
logger.error(` → The workflow filename on npmjs.com must EXACTLY match your .yml file`);
346+
logger.error(` → Check for typos, case sensitivity, and path differences\n`);
347+
} else if (is401Error) {
348+
logger.error(`\n⚠️ ERROR: 401 Unauthorized`);
349+
logger.error(` The OIDC token was not accepted. Check:\n`);
350+
logger.error(` a) GitHub Actions must have 'id-token: write' permission`);
351+
logger.error(` b) Trusted publisher must be configured on npmjs.com`);
352+
logger.error(` c) npm version must be >= 11.5\n`);
353+
} else if (is403Error) {
354+
logger.error(`\n⚠️ ERROR: 403 Forbidden`);
355+
logger.error(` Access denied. Check:\n`);
356+
logger.error(` a) You have publish permissions for this package`);
357+
logger.error(` b) Trusted publisher configuration matches your workflow exactly`);
358+
logger.error(` c) Repository owner/name matches the trusted publisher config\n`);
359+
} else {
360+
logger.error(`\nOIDC authentication failed. Please check the following:\n`);
361+
}
362+
363+
logger.error(`CHECKLIST:`);
364+
logger.error(` □ Package exists on npmjs.com (first publish requires --access-token)`);
365+
logger.error(` □ Trusted publisher configured: https://www.npmjs.com/package/${packageJson.name}/access`);
366+
logger.error(` □ Workflow has 'id-token: write' permission`);
367+
logger.error(` □ npm version >= 11.5 (current: run 'npm --version' to check)`);
368+
logger.error(` □ Using cloud-hosted runner (not self-hosted)`);
369+
logger.error(` □ Workflow filename matches exactly (case-sensitive)\n`);
370+
logger.error(`DOCUMENTATION:`);
371+
logger.error(` → npm Trusted Publishing: https://docs.npmjs.com/trusted-publishers/`);
372+
logger.error(` → npm Provenance: https://docs.npmjs.com/generating-provenance-statements/`);
373+
logger.error(`${'='.repeat(80)}\n`);
374+
}
375+
297376
if (webhook) {
298-
await sendMessageToWebhookWithCodeblock(webhook, `❌ **Failed to publish package** \`${packageJson.name}@${packageJson.version}\`:`, res.error, { logger });
377+
let errorMsg = `❌ **Failed to publish package** \`${packageJson.name}@${packageJson.version}\`:`;
378+
if (args.useOidc) {
379+
errorMsg += `\n⚠️ OIDC was enabled. See logs for troubleshooting guide or visit: https://docs.npmjs.com/trusted-publishers/`;
380+
}
381+
await sendMessageToWebhookWithCodeblock(webhook, errorMsg, res.error, { logger });
299382
}
300383
throw new Error(`Failed to publish package ${packageJson.name}@${packageJson.version}: ${res.error}`);
301384
}

types/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ export type PublishOptions = {
2929

3030
registry: string;
3131
accessToken: string | null | undefined;
32+
/**
33+
* Use OIDC (OpenID Connect) for authentication instead of access tokens.
34+
* Requires npm >= 11.5 and a trusted publisher configured on npmjs.com.
35+
* When enabled, the npm CLI will use the CI/CD provider's identity token.
36+
* Currently supported in GitHub Actions and GitLab CI/CD.
37+
*/
38+
useOidc: boolean;
3239
tag: string | null | undefined;
3340
setLatestTag: boolean | undefined;
3441
useHashInVersion: boolean;

0 commit comments

Comments
 (0)