Skip to content

Commit a8c0edf

Browse files
committed
oicd
1 parent b9d8388 commit a8c0edf

File tree

4 files changed

+198
-9
lines changed

4 files changed

+198
-9
lines changed

.github/workflows/test.yml

Lines changed: 55 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,57 @@ 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 environment for OIDC
75+
run: |
76+
echo "=== Node/npm versions ==="
77+
echo "npm version: $(npm --version)"
78+
echo "node version: $(node --version)"
79+
echo ""
80+
echo "=== OIDC Environment Variables ==="
81+
echo "GITHUB_ACTIONS: $GITHUB_ACTIONS"
82+
echo "ACTIONS_ID_TOKEN_REQUEST_URL: ${ACTIONS_ID_TOKEN_REQUEST_URL:-(not set)}"
83+
echo "ACTIONS_ID_TOKEN_REQUEST_TOKEN: ${ACTIONS_ID_TOKEN_REQUEST_TOKEN:+****(set)}"
84+
echo ""
85+
echo "=== npmrc contents (if any) ==="
86+
cat ~/.npmrc 2>/dev/null || echo "(no ~/.npmrc)"
87+
cat .npmrc 2>/dev/null || echo "(no ./.npmrc)"
88+
89+
- name: Install dependencies
90+
run: npm install
91+
92+
- name: Run publish with OIDC
93+
id: publish-oidc
94+
run: npx . publish "./test" --webhook "${{ secrets.DISCORD_WEBHOOK_TEST_RELEASE }}" --oidc --version+hash --tag oidc --version+tag
95+
96+
- name: Print output
97+
run: |
98+
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: 134 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,31 @@ 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.`);
76+
// Check npm version for OIDC support
77+
const npmVersionResult = tryExecSync('npm --version', { cwd: packageDirectory });
78+
if (npmVersionResult.success) {
79+
const npmVersion = npmVersionResult.output.trim();
80+
const [major, minor] = npmVersion.split('.').map(Number);
81+
logger.info(`npm version: ${npmVersion}`);
82+
if (major < 11 || (major === 11 && minor < 5)) {
83+
logger.warn(`⚠ npm version ${npmVersion} may not support OIDC. Version 11.5+ is recommended.`);
84+
}
85+
}
6986
}
7087

7188
// Remove slahes from the end of the tag (this may happen if the tag is provided by github ref_name
@@ -126,7 +143,7 @@ export async function publish(args) {
126143
msg += `Commit URL: ${repoUrl}/commit/${shortSha}\n`;
127144
msg += `Build time: ${buildTime}\n`;
128145
msg += `Registry: ${args.registry}\n`;
129-
msg += `Token: ${obfuscateToken(args.accessToken)}\n`;
146+
msg += `Auth: ${args.useOidc ? 'OIDC (Trusted Publishing)' : obfuscateToken(args.accessToken)}\n`;
130147
msg += `Tag: ${args.tag || '-'}${args.useTagInVersion ? ' (version+tag)' : ''}${args.createGitTag ? ' (creating git tag)' : ''}\n`;
131148
msg += "```";
132149
await sendMessageToWebhook(webhook, msg, { logger });
@@ -197,13 +214,13 @@ export async function publish(args) {
197214
// Default env
198215
const env = {
199216
...process.env,
200-
NPM_TOKEN: args.accessToken || undefined,
217+
NPM_TOKEN: args.useOidc ? undefined : (args.accessToken || undefined),
201218
NPM_CONFIG_REGISTRY: (args.registry || 'https://registry.npmjs.org/'),
202219
}
203220

204221

205222
// set config
206-
{
223+
if (!args.useOidc) {
207224
let registryUrlWithoutScheme = (args.registry || 'https://registry.npmjs.org/').replace(/https?:\/\//, '');
208225
if (!registryUrlWithoutScheme.endsWith('/')) registryUrlWithoutScheme += '/';
209226

@@ -223,6 +240,34 @@ export async function publish(args) {
223240
// env
224241
// });
225242
// }
243+
} else {
244+
logger.info(`Skipping npm config auth token setup - using OIDC authentication`);
245+
246+
// Check for OIDC environment variables (GitHub Actions sets these)
247+
const hasOidcEnv = !!(process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN);
248+
if (hasOidcEnv) {
249+
logger.info(`✓ GitHub Actions OIDC environment detected`);
250+
logger.info(` ACTIONS_ID_TOKEN_REQUEST_URL: ${process.env.ACTIONS_ID_TOKEN_REQUEST_URL ? '(set)' : '(not set)'}`);
251+
logger.info(` ACTIONS_ID_TOKEN_REQUEST_TOKEN: ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ? '(set)' : '(not set)'}`);
252+
} else {
253+
logger.warn(`⚠ GitHub Actions OIDC environment variables not detected!`);
254+
logger.warn(` This may indicate:`);
255+
logger.warn(` - The workflow doesn't have 'id-token: write' permission`);
256+
logger.warn(` - Not running in GitHub Actions`);
257+
logger.warn(` - Running on a self-hosted runner without OIDC support`);
258+
}
259+
260+
// Clear any existing auth tokens that might interfere with OIDC
261+
// npm OIDC works best when there's no conflicting token configuration
262+
let registryUrlWithoutScheme = (args.registry || 'https://registry.npmjs.org/').replace(/https?:\/\//, '');
263+
if (!registryUrlWithoutScheme.endsWith('/')) registryUrlWithoutScheme += '/';
264+
try {
265+
const clearCmd = `npm config delete //${registryUrlWithoutScheme}:_authToken`;
266+
logger.info(`Clearing any existing auth token for OIDC: ${clearCmd}`);
267+
tryExecSync(clearCmd, { cwd: packageDirectory, env });
268+
} catch {
269+
// Ignore errors - token might not exist
270+
}
226271
}
227272

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

259304

260-
let cmd = `npm publish --access public`
305+
const registryUrl = args.registry || 'https://registry.npmjs.org/';
306+
let cmd = `npm publish --access public --registry ${registryUrl}`
307+
if (args.useOidc) {
308+
cmd += ' --provenance';
309+
logger.info(`OIDC authentication enabled, adding --provenance flag for trusted publishing.`);
310+
}
261311
if (dryRun) {
262312
cmd += ' --dry-run';
263313
logger.info(`Dry run mode enabled, not actually publishing package.`);
@@ -294,8 +344,84 @@ export async function publish(args) {
294344
}
295345
else {
296346
logger.error(`❌ Failed to publish package ${packageJson.name}@${packageJson.version}\n${res.error}`);
347+
348+
// Provide helpful OIDC troubleshooting information if OIDC was enabled
349+
if (args.useOidc) {
350+
const errorStr = res.error?.toString() || res.output?.toString() || '';
351+
const is404Error = errorStr.includes('404') || errorStr.includes('Not found') || errorStr.includes('Not Found');
352+
const is401Error = errorStr.includes('401') || errorStr.includes('Unauthorized');
353+
const is403Error = errorStr.includes('403') || errorStr.includes('Forbidden');
354+
const isNeedAuthError = errorStr.includes('ENEEDAUTH') || errorStr.includes('need auth') || errorStr.includes('You need to authorize');
355+
356+
logger.error(`\n${'='.repeat(80)}`);
357+
logger.error(`OIDC TROUBLESHOOTING GUIDE`);
358+
logger.error(`${'='.repeat(80)}`);
359+
360+
if (isNeedAuthError) {
361+
logger.error(`\n⚠️ ERROR: ENEEDAUTH - Authentication Required`);
362+
logger.error(` npm is not detecting the OIDC environment. This usually means:\n`);
363+
logger.error(` a) MISSING PERMISSIONS: Workflow needs 'id-token: write' permission`);
364+
logger.error(` Add to your workflow:`);
365+
logger.error(` permissions:`);
366+
logger.error(` contents: read`);
367+
logger.error(` id-token: write\n`);
368+
logger.error(` b) NPM VERSION TOO OLD: OIDC requires npm >= 11.5`);
369+
logger.error(` Update Node.js to v22+ or run: npm install -g npm@latest\n`);
370+
logger.error(` c) NOT IN GITHUB ACTIONS: OIDC only works in supported CI environments`);
371+
logger.error(` Currently supported: GitHub Actions, GitLab CI/CD\n`);
372+
logger.error(` d) SELF-HOSTED RUNNER: OIDC requires cloud-hosted runners\n`);
373+
logger.error(` Environment check:`);
374+
logger.error(` ACTIONS_ID_TOKEN_REQUEST_URL: ${process.env.ACTIONS_ID_TOKEN_REQUEST_URL ? '✓ set' : '✗ NOT SET'}`);
375+
logger.error(` ACTIONS_ID_TOKEN_REQUEST_TOKEN: ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ? '✓ set' : '✗ NOT SET'}`);
376+
logger.error(` GITHUB_ACTIONS: ${process.env.GITHUB_ACTIONS ? '✓ set' : '✗ NOT SET'}\n`);
377+
} else if (is404Error) {
378+
logger.error(`\n⚠️ ERROR: 404 Not Found`);
379+
logger.error(` This usually means one of the following:\n`);
380+
logger.error(` a) FIRST-TIME PUBLISH: The package doesn't exist on npmjs.com yet.`);
381+
logger.error(` → First publish MUST be done with a token: --access-token <token>`);
382+
logger.error(` → After first publish, configure trusted publisher, then use --oidc\n`);
383+
logger.error(` b) TRUSTED PUBLISHER NOT CONFIGURED for this package.`);
384+
logger.error(` → Go to: https://www.npmjs.com/package/${packageJson.name}/access`);
385+
logger.error(` → Click "Settings" → "Trusted Publisher" → "GitHub Actions"`);
386+
logger.error(` → Configure: organization/user, repository, workflow filename\n`);
387+
logger.error(` c) WORKFLOW FILENAME MISMATCH`);
388+
logger.error(` → The workflow filename on npmjs.com must EXACTLY match your .yml file`);
389+
logger.error(` → Check for typos, case sensitivity, and path differences\n`);
390+
} else if (is401Error) {
391+
logger.error(`\n⚠️ ERROR: 401 Unauthorized`);
392+
logger.error(` The OIDC token was not accepted. Check:\n`);
393+
logger.error(` a) GitHub Actions must have 'id-token: write' permission`);
394+
logger.error(` b) Trusted publisher must be configured on npmjs.com`);
395+
logger.error(` c) npm version must be >= 11.5\n`);
396+
} else if (is403Error) {
397+
logger.error(`\n⚠️ ERROR: 403 Forbidden`);
398+
logger.error(` Access denied. Check:\n`);
399+
logger.error(` a) You have publish permissions for this package`);
400+
logger.error(` b) Trusted publisher configuration matches your workflow exactly`);
401+
logger.error(` c) Repository owner/name matches the trusted publisher config\n`);
402+
} else {
403+
logger.error(`\nOIDC authentication failed. Please check the following:\n`);
404+
}
405+
406+
logger.error(`CHECKLIST:`);
407+
logger.error(` □ Package exists on npmjs.com (first publish requires --access-token)`);
408+
logger.error(` □ Trusted publisher configured: https://www.npmjs.com/package/${packageJson.name}/access`);
409+
logger.error(` □ Workflow has 'id-token: write' permission`);
410+
logger.error(` □ npm version >= 11.5 (current: run 'npm --version' to check)`);
411+
logger.error(` □ Using cloud-hosted runner (not self-hosted)`);
412+
logger.error(` □ Workflow filename matches exactly (case-sensitive)\n`);
413+
logger.error(`DOCUMENTATION:`);
414+
logger.error(` → npm Trusted Publishing: https://docs.npmjs.com/trusted-publishers/`);
415+
logger.error(` → npm Provenance: https://docs.npmjs.com/generating-provenance-statements/`);
416+
logger.error(`${'='.repeat(80)}\n`);
417+
}
418+
297419
if (webhook) {
298-
await sendMessageToWebhookWithCodeblock(webhook, `❌ **Failed to publish package** \`${packageJson.name}@${packageJson.version}\`:`, res.error, { logger });
420+
let errorMsg = `❌ **Failed to publish package** \`${packageJson.name}@${packageJson.version}\`:`;
421+
if (args.useOidc) {
422+
errorMsg += `\n⚠️ OIDC was enabled. See logs for troubleshooting guide or visit: https://docs.npmjs.com/trusted-publishers/`;
423+
}
424+
await sendMessageToWebhookWithCodeblock(webhook, errorMsg, res.error, { logger });
299425
}
300426
throw new Error(`Failed to publish package ${packageJson.name}@${packageJson.version}: ${res.error}`);
301427
}

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)