npm Trusted Publishers: The Complete Guide
Co-authored by Claude Code
这一篇小结是由 Claude Code 整理而成、执行,而我只是负责在整个过程中进行复核和评审。初衷是自己没有那么多时间写笔记,每次解决一些有意思的问题时想着记录又犯完美主意的毛病怕写出来的东西太肤浅,拖着拖着就忘了细节不写了。
What npm Official Docs Don't Tell You
The official npm Trusted Publishers documentation covers basic UI configuration but critically omits:
1. Repository URL Must Match Exactly
What they say: Configure Owner and Repository in UI.
What they don't say:
- Your
package.jsonrepository.urlmust exactly match the GitHub repository detected by OIDC - Mismatch causes
422 Unprocessable Entityerror with cryptic message about "provenance verification" - No validation happens until you actually try to publish
Impact: You can configure everything correctly in the UI, but publish will fail if package.json is wrong.
2. Error Messages Are Misleading
What they say: Nothing.
What they don't say:
404 Not Founddoesn't mean package doesn't exist - it means OIDC authentication was rejected401 Unauthorizedduringnpm whoamiis expected - OIDC only works during publish- Error messages don't indicate which configuration parameter is wrong
Impact: You waste hours debugging because error messages point in the wrong direction.
3. No Configuration Validation
What they say: Fill in the form and save.
What they don't say:
- npmjs.com accepts any configuration without validation
- No way to test if configuration is correct before publishing
- No feedback if workflow name, repository name, or owner is wrong
Impact: You discover configuration errors only when workflow runs and fails.
4. Sigstore Provenance Requirements
What they say: "Provenance is automatically generated."
What they don't say:
- npm Trusted Publishers use Sigstore for cryptographic provenance verification - but npm's official documentation never mentions Sigstore at all
- The repository.url matching requirement comes from Sigstore's verification process, not npm's documentation
- Provenance includes repository information from GitHub OIDC token
- This repository information must match
package.jsonrepository.urlexactly - Sigstore signs and verifies provenance against both sources
- Any mismatch between OIDC metadata and package.json causes Sigstore verification to fail with 422 error
The documentation gap: npm's official Trusted Publishers documentation completely omits the fact that Sigstore is performing the actual verification. The repository URL matching requirement is only documented in the npm/provenance GitHub repository, not in user-facing documentation. This is why so many users encounter 422 errors without understanding the root cause.
Impact: Even with correct Trusted Publisher config, publish fails if package.json doesn't match. Users waste hours debugging because npm docs don't explain the underlying Sigstore verification process.
Credit: The provenance verification system is built on Sigstore, an open-source project providing transparent and secure software supply chain infrastructure.
5. Debugging Is Opaque
What they say: Nothing about debugging.
What they don't say:
- Which environment variables indicate OIDC is working
- How to verify OIDC token is available
- What diagnostic information is safe to log
- How to distinguish between different failure modes
Impact: When things fail, you have no visibility into what went wrong or how to fix it.
6. npm Version Requirement
What they say: "Trusted publishing requires npm >=11.5.1"
What they don't say:
- GitHub Actions runners may have older npm versions
- You need to explicitly upgrade npm in your workflow
- Older npm versions silently fail to use OIDC
Impact: OIDC might not be attempted at all if npm version is too old.
Prerequisites and Hidden Requirements
Requirement 1: npm Version >=11.5.1
# In your workflow - REQUIRED
- name: Upgrade npm for OIDC support
run: |
npm install -g npm@latest
npm --version # Verify >= 11.5.1
Requirement 2: Repository URL Consistency
This is the most critical and most undocumented requirement.
The Three Sources of Repository Information
When you publish with OIDC, three sources must agree on the repository:
- GitHub OIDC token (from GitHub Actions runtime)
- npmjs.com Trusted Publisher config (what you configure in UI)
- package.json repository.url (in your code)
All three must match exactly or publish will fail.
Verification Script
#!/bin/bash
# verify-repo-consistency.sh
# Run this BEFORE configuring Trusted Publisher
echo "Checking repository information consistency..."
echo ""
# 1. Get repository from git
GIT_REPO=$(git remote get-url origin | sed 's/.*github.com[/:]//; s/.git$//')
echo "1. Git remote: https://github.com/$GIT_REPO"
# 2. Get repository from package.json
PKG_REPO=$(node -e "console.log(require('./package.json').repository?.url || 'NOT SET')")
echo "2. package.json: $PKG_REPO"
# 3. Extract comparable format
PKG_REPO_CLEAN=$(echo "$PKG_REPO" | sed 's|https://github.com/||; s|git+||; s|.git$||')
echo "3. Normalized: https://github.com/$PKG_REPO_CLEAN"
echo ""
# Check consistency
if [ "$GIT_REPO" = "$PKG_REPO_CLEAN" ]; then
echo "PASS: All repository information matches"
echo ""
echo "Use these values for npmjs.com Trusted Publisher:"
OWNER=$(echo "$GIT_REPO" | cut -d'/' -f1)
REPO=$(echo "$GIT_REPO" | cut -d'/' -f2)
echo " Owner: $OWNER"
echo " Repository: $REPO"
exit 0
else
echo "FAIL: Repository mismatch detected!"
echo ""
echo "Fix package.json to match git remote:"
echo ' "repository": {'
echo ' "type": "git",'
echo " \"url\": \"https://github.com/$GIT_REPO\""
echo ' }'
exit 1
fi
Requirement 3: Workflow Permissions
# REQUIRED in your workflow file
permissions:
id-token: write # For OIDC
contents: read # For checkout (optional, depends on your workflow)
Requirement 4: Correct package.json Format
{
"repository": {
"type": "git",
"url": "https://github.com/owner/repo"
}
}
Common mistakes:
"url": "git+https://github.com/owner/repo.git"- Removegit+prefix and.gitsuffix"url": "https://github.com/owner/repo-website"- Must match actual git repository"url": "[email protected]:owner/repo.git"- Use HTTPS format, not SSH"url": "https://github.com/owner/repo"- Correct format
For monorepos, add directory field:
{
"repository": {
"type": "git",
"url": "https://github.com/owner/repo",
"directory": "packages/my-package"
}
}
Step-by-Step Configuration
Phase 1: Pre-Configuration Validation
Before touching npmjs.com UI, gather and verify information:
# Run in your package directory
cd path/to/your/package
# 1. Verify git repository
echo "Git repository:"
git remote get-url origin
# 2. Verify package.json repository
echo ""
echo "package.json repository:"
cat package.json | grep -A3 '"repository"'
# 3. Verify workflow file exists
echo ""
echo "Workflow file:"
ls -la .github/workflows/*.yml
# 4. Extract configuration values
echo ""
echo "Configuration for npmjs.com:"
GIT_REMOTE=$(git remote get-url origin | sed 's/.*github.com[/:]//; s/.git$//')
OWNER=$(echo "$GIT_REMOTE" | cut -d'/' -f1)
REPO=$(echo "$GIT_REMOTE" | cut -d'/' -f2)
echo " Owner: $OWNER"
echo " Repository: $REPO"
echo " Workflow: (your workflow filename, e.g., publish.yml)"
Save these values - you'll need them for UI configuration.
Phase 2: Update Workflow File
Add OIDC support to your GitHub Actions workflow:
name: Publish Package
on:
workflow_dispatch: # Manual trigger for testing
release:
types: [published]
permissions:
id-token: write # REQUIRED for OIDC
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
# CRITICAL: Upgrade npm
- name: Upgrade npm for OIDC
run: |
npm install -g npm@latest
npm --version
# RECOMMENDED: Add diagnostics (see Safe Diagnostic Logging section)
- name: Verify OIDC availability
run: |
if [ -n "${ACTIONS_ID_TOKEN_REQUEST_URL}" ]; then
echo "OIDC token available"
else
echo "OIDC token NOT available"
exit 1
fi
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
# NO npm login or .npmrc configuration needed!
- name: Publish
run: npm publish --access public
Key points:
- Include
id-token: writepermission - Upgrade npm to latest
- NO npm token configuration
- NO .npmrc setup
- Just run
npm publishdirectly
Phase 3: Configure Trusted Publisher on npmjs.com
-
Navigate to package settings:
- URL:
https://www.npmjs.com/package/YOUR-PACKAGE-NAME/access - Requires: You must have publish permissions
- URL:
-
Find "Trusted publishers" section:
- May be labeled "Publishing access" or similar
- Look for "Add trusted publisher" or "Configure publishing" button
-
Select Provider:
- Choose "GitHub Actions" from dropdown
-
Fill in the form (use values from Phase 1):
Field Value Notes Owner owner-nameFrom git remote, case-sensitive Repository repo-nameFrom git remote, exact match required Workflow publish.ymlJust filename, include .yml extension Environment (empty) Leave blank unless you use GitHub Environments -
Double-check before saving:
Provider: GitHub Actions Owner: YourOrg - Must match GitHub org/user exactly Repository: your-repo - Must match git remote exactly Workflow: publish.yml - Just filename, not path Environment: (empty) - Leave blank -
Save and verify:
- Refresh the page
- Confirm the trusted publisher appears in the list
- Format should show:
GitHub Actions: Owner/Repository (Workflow: publish.yml)
Phase 4: Pre-Publish Self-Validation
Before triggering your workflow, run this validation:
#!/bin/bash
# pre-publish-validation.sh
echo ""
echo " Pre-Publish Validation"
echo ""
echo ""
FAILED=0
# Check 1: Workflow file
echo "[CHECK] Checking workflow file..."
if [ ! -f .github/workflows/publish.yml ]; then
echo " Workflow file not found"
FAILED=1
else
echo " Found: .github/workflows/publish.yml"
fi
# Check 2: Workflow permissions
echo ""
echo "[CHECK] Checking workflow permissions..."
if grep -q "id-token: write" .github/workflows/publish.yml; then
echo " id-token: write permission found"
else
echo " Missing id-token: write permission"
FAILED=1
fi
# Check 3: npm upgrade step
echo ""
echo "[CHECK] Checking npm upgrade step..."
if grep -q "npm install -g npm@latest" .github/workflows/publish.yml; then
echo " npm upgrade step found"
else
echo " npm upgrade step missing (recommended)"
fi
# Check 4: Repository consistency
echo ""
echo "[CHECK] Checking repository consistency..."
GIT_REPO=$(git remote get-url origin | sed 's/.*github.com[/:]//; s/.git$//')
PKG_REPO=$(node -e "console.log(require('./package.json').repository?.url || '')" | sed 's|https://github.com/||; s|git+||; s|.git$||')
echo " Git remote: https://github.com/$GIT_REPO"
echo " package.json: https://github.com/$PKG_REPO"
if [ "$GIT_REPO" = "$PKG_REPO" ]; then
echo " Repository URLs match"
else
echo " Repository URLs DO NOT match"
FAILED=1
fi
# Check 5: Extract values for UI
echo ""
echo "[CHECK] Configuration values for npmjs.com:"
OWNER=$(echo "$GIT_REPO" | cut -d'/' -f1)
REPO=$(echo "$GIT_REPO" | cut -d'/' -f2)
echo " Owner: $OWNER"
echo " Repository: $REPO"
echo " Workflow: publish.yml"
echo ""
if [ $FAILED -eq 0 ]; then
echo "All checks passed - ready to publish!"
exit 0
else
echo "Some checks failed - fix errors before publishing"
exit 1
fi
Self-Validation Scripts
These scripts help you validate configuration before publishing, catching errors that npmjs.com UI won't detect.
Script 1: Check Repository Consistency
#!/bin/bash
# check-repo-consistency.sh
# Verifies git remote matches package.json
set -e
PKG_FILE="${1:-package.json}"
if [ ! -f "$PKG_FILE" ]; then
echo "package.json not found"
exit 1
fi
GIT_REMOTE=$(git remote get-url origin 2>/dev/null || echo "")
if [ -z "$GIT_REMOTE" ]; then
echo "Not in a git repository"
exit 1
fi
# Extract repository from git remote
GIT_REPO=$(echo "$GIT_REMOTE" | sed 's/.*github.com[/:]//; s/.git$//')
# Extract repository from package.json
PKG_REPO=$(node -e "
const pkg = require('./$PKG_FILE');
if (!pkg.repository) {
console.log('NOT_SET');
process.exit(0);
}
const url = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository.url;
console.log(url || 'NOT_SET');
")
if [ "$PKG_REPO" = "NOT_SET" ]; then
echo "package.json does not have repository field"
echo ""
echo "Add this to package.json:"
echo ' "repository": {'
echo ' "type": "git",'
echo " \"url\": \"https://github.com/$GIT_REPO\""
echo ' }'
exit 1
fi
# Normalize package.json repository URL
PKG_REPO_CLEAN=$(echo "$PKG_REPO" | sed 's|https://github.com/||; s|git+||; s|.git$||; s|[email protected]:||')
echo "Repository Comparison:"
echo " Git remote: $GIT_REPO"
echo " package.json: $PKG_REPO_CLEAN"
echo ""
if [ "$GIT_REPO" = "$PKG_REPO_CLEAN" ]; then
echo "Repositories match!"
echo ""
OWNER=$(echo "$GIT_REPO" | cut -d'/' -f1)
REPO=$(echo "$GIT_REPO" | cut -d'/' -f2)
echo "Use these for Trusted Publisher config:"
echo " Owner: $OWNER"
echo " Repository: $REPO"
exit 0
else
echo "Repositories DO NOT match!"
echo ""
echo "Fix package.json:"
echo ' "repository": {'
echo ' "type": "git",'
echo " \"url\": \"https://github.com/$GIT_REPO\""
echo ' }'
exit 1
fi
Script 2: Workflow Configuration Validator
#!/bin/bash
# validate-workflow.sh
# Checks GitHub Actions workflow for OIDC requirements
WORKFLOW_FILE="${1:-.github/workflows/publish.yml}"
if [ ! -f "$WORKFLOW_FILE" ]; then
echo "Workflow file not found: $WORKFLOW_FILE"
exit 1
fi
echo "Validating: $WORKFLOW_FILE"
echo ""
FAILED=0
# Check 1: id-token permission
if grep -q "id-token.*write" "$WORKFLOW_FILE"; then
echo "Has id-token: write permission"
else
echo "Missing id-token: write permission"
echo " Add to workflow:"
echo " permissions:"
echo " id-token: write"
FAILED=1
fi
# Check 2: npm upgrade
if grep -q "npm install -g npm" "$WORKFLOW_FILE" || grep -q "npm@latest" "$WORKFLOW_FILE"; then
echo "Has npm upgrade step"
else
echo "No npm upgrade step (recommended)"
echo " Add to workflow:"
echo " - run: npm install -g npm@latest"
fi
# Check 3: No hardcoded tokens
if grep -q "NODE_AUTH_TOKEN" "$WORKFLOW_FILE" || grep -q "NPM_TOKEN" "$WORKFLOW_FILE"; then
echo "Found token references (may not be needed with OIDC)"
else
echo "No hardcoded tokens"
fi
# Check 4: registry-url
if grep -q "registry-url.*npmjs.org" "$WORKFLOW_FILE"; then
echo "Has registry-url configured"
else
echo "No registry-url found (may be needed)"
fi
echo ""
if [ $FAILED -eq 0 ]; then
echo "Workflow validation passed"
exit 0
else
echo "Workflow validation failed"
exit 1
fi
Quick Automated Check
For a fast validation without detailed output, use this simpler script:
#!/bin/bash
# quick-oidc-check.sh - Fast OIDC configuration validation
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'
echo "Quick OIDC Check"
ERRORS=0
# Check git repository
GIT_REPO=$(git remote get-url origin 2>/dev/null | sed 's/.*github.com[/:]//; s/.git$//')
if [ -z "$GIT_REPO" ]; then
echo -e "${RED}${NC} Not a git repository"
exit 1
fi
echo -e "${GREEN}${NC} Git: $GIT_REPO"
# Check package.json
PKG_REPO=$(node -e "console.log(require('./package.json').repository?.url || '')" 2>/dev/null | sed 's|https://github.com/||; s|git+||; s|.git$||')
if [ "$GIT_REPO" = "$PKG_REPO" ]; then
echo -e "${GREEN}${NC} package.json matches"
else
echo -e "${RED}${NC} Repository mismatch"
ERRORS=$((ERRORS + 1))
fi
# Check workflow
if find .github/workflows -name "*.yml" -exec grep -q "id-token.*write" {} \; 2>/dev/null; then
echo -e "${GREEN}${NC} Workflow has OIDC permission"
else
echo -e "${RED}${NC} Missing id-token: write"
ERRORS=$((ERRORS + 1))
fi
# Result
echo ""
if [ $ERRORS -eq 0 ]; then
echo -e "${GREEN}Ready for OIDC${NC}"
OWNER=$(echo "$GIT_REPO" | cut -d'/' -f1)
REPO=$(echo "$GIT_REPO" | cut -d'/' -f2)
echo "Config: $OWNER / $REPO"
exit 0
else
echo -e "${RED}Fix errors above${NC}"
exit 1
fi
Usage:
chmod +x quick-oidc-check.sh
./quick-oidc-check.sh
Script 3: Complete Pre-Publish Checklist
#!/bin/bash
# pre-publish-checklist.sh
# Comprehensive validation before first publish
echo ""
echo ""
echo ""
echo ""
echo ""
ERRORS=0
WARNINGS=0
# Helper function
check() {
local status=$1
local message=$2
if [ $status -eq 0 ]; then
echo " $message"
else
echo " $message"
ERRORS=$((ERRORS + 1))
fi
}
warn() {
echo " $1"
WARNINGS=$((WARNINGS + 1))
}
# 1. Git repository
echo "Git Repository"
if git remote get-url origin >/dev/null 2>&1; then
GIT_REMOTE=$(git remote get-url origin)
GIT_REPO=$(echo "$GIT_REMOTE" | sed 's/.*github.com[/:]//; s/.git$//')
check 0 "Git remote found: $GIT_REPO"
else
check 1 "Not in a git repository"
fi
echo ""
# 2. package.json
echo "package.json"
if [ -f package.json ]; then
check 0 "package.json exists"
# Check repository field
PKG_REPO=$(node -e "console.log(require('./package.json').repository?.url || '')")
if [ -n "$PKG_REPO" ]; then
check 0 "Has repository field"
# Check consistency
PKG_REPO_CLEAN=$(echo "$PKG_REPO" | sed 's|https://github.com/||; s|git+||; s|.git$||')
if [ "$GIT_REPO" = "$PKG_REPO_CLEAN" ]; then
check 0 "Repository URL matches git remote"
else
check 1 "Repository URL mismatch: $PKG_REPO_CLEAN vs $GIT_REPO"
fi
else
check 1 "Missing repository field"
fi
else
check 1 "package.json not found"
fi
echo ""
# 3. Workflow file
echo "GitHub Actions Workflow"
WORKFLOW_FILES=$(find .github/workflows -name "*.yml" 2>/dev/null | head -5)
if [ -n "$WORKFLOW_FILES" ]; then
check 0 "Workflow files found"
# Check for id-token permission
if echo "$WORKFLOW_FILES" | xargs grep -l "id-token.*write" >/dev/null 2>&1; then
check 0 "Has id-token: write permission"
else
check 1 "Missing id-token: write permission"
fi
# Check for npm upgrade
if echo "$WORKFLOW_FILES" | xargs grep -l "npm.*@latest" >/dev/null 2>&1; then
check 0 "Has npm upgrade step"
else
warn "No npm upgrade step (recommended)"
fi
else
check 1 "No workflow files found"
fi
echo ""
# 4. Configuration summary
echo "Configuration for npmjs.com"
if [ -n "$GIT_REPO" ]; then
OWNER=$(echo "$GIT_REPO" | cut -d'/' -f1)
REPO=$(echo "$GIT_REPO" | cut -d'/' -f2)
echo " Owner: $OWNER"
echo " Repository: $REPO"
echo " Workflow: (your-workflow-name.yml)"
echo " Environment: (leave empty)"
fi
echo ""
# 5. Summary
echo ""
if [ $ERRORS -eq 0 ]; then
echo "All critical checks passed!"
if [ $WARNINGS -gt 0 ]; then
echo "$WARNINGS warning(s) - review recommendations above"
fi
echo ""
echo "Next steps:"
echo " 1. Configure Trusted Publisher on npmjs.com"
echo " 2. Test publish with workflow_dispatch"
echo " 3. Monitor workflow logs for OIDC token availability"
exit 0
else
echo "$ERRORS error(s) found - fix before publishing"
if [ $WARNINGS -gt 0 ]; then
echo "$WARNINGS warning(s) - review recommendations"
fi
exit 1
fi
Safe Diagnostic Logging
When troubleshooting, you need visibility without exposing secrets.
Safe Information to Log
- name: Safe diagnostic information
run: |
echo "GitHub Context:"
echo " Repository: ${{ github.repository }}"
echo " Workflow: ${{ github.workflow }}"
echo " Workflow ref: ${{ github.workflow_ref }}"
echo " Actor: ${{ github.actor }}"
echo " Run ID: ${{ github.run_id }}"
echo " Event: ${{ github.event_name }}"
echo ""
echo "Git Information:"
git remote -v
git log -1 --oneline
echo ""
echo "Package Information:"
node -e "
const pkg = require('./package.json');
console.log(' Name:', pkg.name);
console.log(' Version:', pkg.version);
console.log(' Repository:', pkg.repository?.url || 'not set');
"
echo ""
echo "Environment:"
echo " Node: $(node --version)"
echo " npm: $(npm --version)"
Checking OIDC Token (Safe)
- name: Verify OIDC token availability
run: |
echo "OIDC Token Check:"
# Safe: Check if variable exists
if [ -n "${ACTIONS_ID_TOKEN_REQUEST_URL}" ]; then
echo " OIDC request URL is set"
# Safe: The URL endpoint is not sensitive
echo " Endpoint: ${ACTIONS_ID_TOKEN_REQUEST_URL}"
else
echo " OIDC request URL is NOT set"
echo " Fix: Add 'id-token: write' permission to workflow"
exit 1
fi
# Safe: Check if token variable exists (but don't show value)
if [ -n "${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" ]; then
echo " OIDC request token is set"
else
echo " OIDC request token is NOT set"
exit 1
fi
NEVER Log These (Dangerous)
# DANGEROUS - DO NOT DO THIS
- name: DO NOT USE - Security Risk
run: |
# These will leak secrets:
echo "${{ secrets.NPM_TOKEN }}" # Exposes npm token
echo "${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" # Exposes OIDC token
echo "${NODE_AUTH_TOKEN}" # Exposes auth token
cat ~/.npmrc # May contain tokens
npm config list --json # Shows tokens in plain text
env | grep TOKEN # Exposes all tokens
GitHub Actions Automatic Protection
GitHub automatically masks:
- Any value from
${{ secrets.* }}-> shown as*** - Detected token patterns in output
- npm shows tokens as
(protected)in verbose mode
But you should still never intentionally output secrets.
Complete Diagnostic Step Example
- name: Pre-publish diagnostics
run: |
set -e
echo ""
echo " Package Publish Diagnostics"
echo ""
echo ""
# Package info
echo "Package:"
PKG_NAME=$(node -e "console.log(require('./package.json').name)")
PKG_VERSION=$(node -e "console.log(require('./package.json').version)")
echo " $PKG_NAME@$PKG_VERSION"
echo ""
# OIDC status
echo "OIDC Status:"
if [ -n "${ACTIONS_ID_TOKEN_REQUEST_URL}" ] && [ -n "${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" ]; then
echo " OIDC authentication available"
else
echo " OIDC NOT available - check permissions"
exit 1
fi
echo ""
# Repository consistency
echo "Repository Check:"
GIT_REPO=$(git remote get-url origin | sed 's/.*github.com[/:]//; s/.git$//')
PKG_REPO=$(node -e "console.log(require('./package.json').repository?.url || '')" | sed 's|https://github.com/||; s|git+||; s|.git$||')
echo " Git remote: $GIT_REPO"
echo " package.json: $PKG_REPO"
if [ "$GIT_REPO" = "$PKG_REPO" ]; then
echo " Repositories match"
else
echo " Repository mismatch!"
echo " This will cause 422 error"
exit 1
fi
echo ""
# GitHub context
echo "GitHub Context:"
echo " Repository: ${{ github.repository }}"
echo " Workflow: ${{ github.workflow_ref }}"
echo " Actor: ${{ github.actor }}"
echo ""
echo "All pre-publish checks passed"
Troubleshooting with Decision Trees
Quick Diagnostic Tree
npm publish failed?
│
├─ Error code: 404 Not Found
├─ Check: OIDC token available?
├─ No -> Fix: Add 'id-token: write' permission
└─ Yes -> Check: Trusted Publisher configured on npmjs.com?
├─ No -> Fix: Configure on npmjs.com
└─ Yes -> Check: Owner/Repo/Workflow match exactly?
├─ No -> Fix: Update npmjs.com configuration
└─ Yes -> Check: npm version >= 11.5.1?
└─ No -> Fix: Add npm upgrade step
│
├─ Error code: 422 Unprocessable Entity
└─ Message mentions "provenance" or "repository"?
└─ Yes -> Repository URL mismatch
├─ Check: git remote -v
├─ Check: package.json repository.url
└─ Fix: Make them match exactly
│
├─ Error code: 401 Unauthorized
└─ During: npm whoami?
├─ Yes -> This is NORMAL with OIDC
└─ Continue to publish step
└─ No -> During publish?
└─ Check OIDC token availability
│
└─ Other error
└─ Check npm publish --verbose output
└─ Look for authentication or provenance messages
Error 404: Detailed Diagnosis
# When you get: npm error 404 Not Found
# Step 1: Check OIDC token
echo "Checking OIDC..."
if [ -n "${ACTIONS_ID_TOKEN_REQUEST_URL}" ]; then
echo "OIDC available"
else
echo "OIDC NOT available"
echo "Fix: Add 'id-token: write' permission to workflow"
exit 1
fi
# Step 2: Verify npm version
echo ""
echo "Checking npm version..."
NPM_VERSION=$(npm --version)
echo "npm version: $NPM_VERSION"
if [ "$(printf '%s\n' "11.5.1" "$NPM_VERSION" | sort -V | head -n1)" = "11.5.1" ]; then
echo "npm >= 11.5.1"
else
echo "npm < 11.5.1"
echo "Fix: Add 'npm install -g npm@latest' to workflow"
exit 1
fi
# Step 3: Show configuration for manual verification
echo ""
echo "Verify this matches npmjs.com configuration:"
GIT_REPO=$(git remote get-url origin | sed 's/.*github.com[/:]//; s/.git$//')
OWNER=$(echo "$GIT_REPO" | cut -d'/' -f1)
REPO=$(echo "$GIT_REPO" | cut -d'/' -f2)
echo " Owner: $OWNER"
echo " Repository: $REPO"
echo " Workflow: ${{ github.workflow }}.yml"
echo ""
echo "If these don't match npmjs.com, update the Trusted Publisher configuration"
Error 422: Repository Mismatch Fix
# When you get: npm error 422 Unprocessable Entity
# Message: "Failed to validate repository information"
echo "Repository mismatch detected!"
echo ""
# Show current state
GIT_REPO=$(git remote get-url origin | sed 's/.*github.com[/:]//; s/.git$//')
PKG_REPO=$(node -e "console.log(require('./package.json').repository?.url || '')")
echo "Current configuration:"
echo " Git remote: https://github.com/$GIT_REPO"
echo " package.json: $PKG_REPO"
echo ""
# Generate fix
echo "Fix package.json:"
cat <<EOF
{
"repository": {
"type": "git",
"url": "https://github.com/$GIT_REPO"
}
}
EOF
echo ""
echo "After updating package.json:"
echo " 1. Commit the change"
echo " 2. Re-run the publish workflow"
Complete Working Example
Here's a complete, copy-paste-ready GitHub Actions workflow:
name: Publish to npm with OIDC
on:
workflow_dispatch:
inputs:
tag:
description: 'npm tag (latest, beta, etc.)'
required: false
default: 'latest'
release:
types: [published]
permissions:
id-token: write # REQUIRED for OIDC
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Upgrade npm for OIDC support
run: |
npm install -g npm@latest
echo "npm version: $(npm --version)"
- name: Verify OIDC token availability
run: |
if [ -n "${ACTIONS_ID_TOKEN_REQUEST_URL}" ] && [ -n "${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" ]; then
echo "OIDC token available"
echo "Endpoint: ${ACTIONS_ID_TOKEN_REQUEST_URL}"
else
echo "OIDC token NOT available"
echo "Check workflow permissions include 'id-token: write'"
exit 1
fi
- name: Verify repository configuration
run: |
echo "Checking repository consistency..."
GIT_REPO=$(git remote get-url origin | sed 's/.*github.com[/:]//; s/.git$//')
PKG_REPO=$(node -e "console.log(require('./package.json').repository?.url || '')" | sed 's|https://github.com/||; s|git+||; s|.git$||')
echo "Git remote: $GIT_REPO"
echo "package.json: $PKG_REPO"
if [ "$GIT_REPO" != "$PKG_REPO" ]; then
echo "Repository mismatch!"
echo "This will cause 422 error during publish"
exit 1
fi
echo "Repositories match"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
- name: Publish to npm
run: |
NPM_TAG="${{ github.event.inputs.tag || 'latest' }}"
echo "Publishing with tag: $NPM_TAG"
npm publish --access public --tag "$NPM_TAG" --verbose
- name: Publish summary
if: success()
run: |
PKG_NAME=$(node -e "console.log(require('./package.json').name)")
PKG_VERSION=$(node -e "console.log(require('./package.json').version)")
echo "Published: $PKG_NAME@$PKG_VERSION"
echo "Check it out: https://www.npmjs.com/package/$PKG_NAME"
Summary Checklist
Before your first publish, verify:
GitHub Repository
- [ ] Workflow file exists with OIDC permission (
id-token: write) - [ ] Workflow includes npm upgrade step (
npm install -g npm@latest) - [ ] Git remote URL confirmed with
git remote -v
package.json
- [ ] Has
repositoryfield - [ ] Repository URL matches git remote exactly
- [ ] URL format is
https://github.com/owner/repo(nogit+, no.git) - [ ] For monorepos, includes
directoryfield
npmjs.com Configuration
- [ ] Trusted Publisher configured for package
- [ ] Owner matches GitHub org/user (case-sensitive)
- [ ] Repository matches git remote (exact match)
- [ ] Workflow filename includes
.ymlextension - [ ] Environment field is empty (unless using GitHub Environments)
Testing
- [ ] Run pre-publish validation script
- [ ] All checks pass
- [ ] Test with
workflow_dispatchtrigger first
During First Publish
- [ ] Monitor logs for "OIDC token available"
- [ ] Look for "Provenance statement published"
- [ ] If fails, use diagnostic decision tree
After Success
- [ ] Verify package published:
npm view your-package version - [ ] Check provenance: Visit Sigstore link in logs
- [ ] Test install:
npm install your-package
Key Insights
- The challenge: npmjs.com provides no validation - errors only appear during publish
- Most critical: Repository URL consistency across three sources
- Most misleading: Error messages don't indicate the actual problem
- Best practice: Use validation scripts BEFORE configuring Trusted Publisher
- Debug strategy: Add safe diagnostic logging to workflow
- Success indicator: 422 error means OIDC works - just fix repository URL
References
- Official npm Trusted Publishers Docs (basic UI guide)
- GitHub Actions OIDC
- Sigstore (provenance verification)
- npm Provenance
- npm OIDC Announcement