Skip to content

Commit 660fe93

Browse files
committed
oicd
1 parent b9d8388 commit 660fe93

File tree

4 files changed

+240
-9
lines changed

4 files changed

+240
-9
lines changed

.github/workflows/test.yml

Lines changed: 76 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,78 @@ 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'
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: Update npm to latest (OIDC requires npm >= 11.5)
75+
run: |
76+
echo "Current npm version: $(npm --version)"
77+
npm install -g npm@latest
78+
echo "Updated npm version: $(npm --version)"
79+
80+
- name: Check environment for OIDC
81+
run: |
82+
echo "=== Node/npm versions ==="
83+
echo "npm version: $(npm --version)"
84+
echo "node version: $(node --version)"
85+
echo ""
86+
echo "=== OIDC Environment Variables ==="
87+
echo "GITHUB_ACTIONS: $GITHUB_ACTIONS"
88+
echo "ACTIONS_ID_TOKEN_REQUEST_URL: ${ACTIONS_ID_TOKEN_REQUEST_URL:-(not set)}"
89+
echo "ACTIONS_ID_TOKEN_REQUEST_TOKEN: ${ACTIONS_ID_TOKEN_REQUEST_TOKEN:+****(set)}"
90+
echo ""
91+
echo "=== Token Environment Variables (should be unset for OIDC) ==="
92+
echo "NPM_TOKEN: ${NPM_TOKEN:-(not set)}"
93+
echo "NODE_AUTH_TOKEN: ${NODE_AUTH_TOKEN:-(not set)}"
94+
echo ""
95+
echo "=== npmrc contents (if any) ==="
96+
cat ~/.npmrc 2>/dev/null || echo "(no ~/.npmrc)"
97+
cat .npmrc 2>/dev/null || echo "(no ./.npmrc)"
98+
echo ""
99+
echo "=== npm config list ==="
100+
npm config list
101+
102+
- name: Install dependencies
103+
run: npm install
104+
105+
- name: Test direct npm publish with OIDC (debug)
106+
working-directory: ./test
107+
continue-on-error: true
108+
run: |
109+
echo "Testing direct npm publish with --provenance..."
110+
npm version 2.0.0-oidc-direct-test.$(git rev-parse --short HEAD) --no-git-tag-version
111+
npm publish --access public --provenance --tag oidc-direct --dry-run || echo "Direct test failed"
112+
113+
- name: Run publish with OIDC
114+
id: publish-oidc
115+
run: npx . publish "./test" --webhook "${{ secrets.DISCORD_WEBHOOK_TEST_RELEASE }}" --oidc --version+hash --tag oidc --version+tag
116+
117+
- name: Print output
118+
run: |
119+
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: 155 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,23 @@ export async function publish(args) {
197214
// Default env
198215
const env = {
199216
...process.env,
200-
NPM_TOKEN: args.accessToken || undefined,
201217
NPM_CONFIG_REGISTRY: (args.registry || 'https://registry.npmjs.org/'),
202218
}
203219

220+
// For OIDC: completely unset token variables so npm falls back to OIDC
221+
// An empty string is still a value - npm will try to use it instead of OIDC
222+
if (args.useOidc) {
223+
delete env.NPM_TOKEN;
224+
delete env.NODE_AUTH_TOKEN;
225+
delete env.NPM_CONFIG__AUTH;
226+
delete env.NPM_CONFIG_TOKEN;
227+
} else {
228+
env.NPM_TOKEN = args.accessToken || undefined;
229+
}
230+
204231

205232
// set config
206-
{
233+
if (!args.useOidc) {
207234
let registryUrlWithoutScheme = (args.registry || 'https://registry.npmjs.org/').replace(/https?:\/\//, '');
208235
if (!registryUrlWithoutScheme.endsWith('/')) registryUrlWithoutScheme += '/';
209236

@@ -223,6 +250,45 @@ export async function publish(args) {
223250
// env
224251
// });
225252
// }
253+
} else {
254+
logger.info(`Skipping npm config auth token setup - using OIDC authentication`);
255+
256+
// Check for OIDC environment variables (GitHub Actions sets these)
257+
const hasOidcEnv = !!(process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN);
258+
if (hasOidcEnv) {
259+
logger.info(`✓ GitHub Actions OIDC environment detected`);
260+
logger.info(` ACTIONS_ID_TOKEN_REQUEST_URL: ${process.env.ACTIONS_ID_TOKEN_REQUEST_URL ? '(set)' : '(not set)'}`);
261+
logger.info(` ACTIONS_ID_TOKEN_REQUEST_TOKEN: ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ? '(set)' : '(not set)'}`);
262+
} else {
263+
logger.warn(`⚠ GitHub Actions OIDC environment variables not detected!`);
264+
logger.warn(` This may indicate:`);
265+
logger.warn(` - The workflow doesn't have 'id-token: write' permission`);
266+
logger.warn(` - Not running in GitHub Actions`);
267+
logger.warn(` - Running on a self-hosted runner without OIDC support`);
268+
}
269+
270+
// Log token-related env vars that might interfere with OIDC
271+
// npm won't use OIDC if ANY token variable is set (even to empty string)
272+
const tokenVars = ['NPM_TOKEN', 'NODE_AUTH_TOKEN', 'NPM_CONFIG__AUTH', 'NPM_CONFIG_TOKEN'];
273+
const setTokenVars = tokenVars.filter(v => v in env);
274+
if (setTokenVars.length > 0) {
275+
logger.warn(`⚠ Token environment variables detected that may interfere with OIDC: ${setTokenVars.join(', ')}`);
276+
logger.info(` Removing these variables to allow OIDC fallback...`);
277+
} else {
278+
logger.info(`✓ No conflicting token environment variables detected`);
279+
}
280+
281+
// Clear any existing auth tokens that might interfere with OIDC
282+
// npm OIDC works best when there's no conflicting token configuration
283+
let registryUrlWithoutScheme = (args.registry || 'https://registry.npmjs.org/').replace(/https?:\/\//, '');
284+
if (!registryUrlWithoutScheme.endsWith('/')) registryUrlWithoutScheme += '/';
285+
try {
286+
const clearCmd = `npm config delete //${registryUrlWithoutScheme}:_authToken`;
287+
logger.info(`Clearing any existing auth token for OIDC: ${clearCmd}`);
288+
tryExecSync(clearCmd, { cwd: packageDirectory, env });
289+
} catch {
290+
// Ignore errors - token might not exist
291+
}
226292
}
227293

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

259325

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

0 commit comments

Comments
 (0)