npm Trusted Publishers: The Complete Guide

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:

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:

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:

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:

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:

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:

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:

  1. GitHub OIDC token (from GitHub Actions runtime)
  2. npmjs.com Trusted Publisher config (what you configure in UI)
  3. 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:

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:

Phase 3: Configure Trusted Publisher on npmjs.com

  1. Navigate to package settings:

    • URL: https://www.npmjs.com/package/YOUR-PACKAGE-NAME/access
    • Requires: You must have publish permissions
  2. Find "Trusted publishers" section:

    • May be labeled "Publishing access" or similar
    • Look for "Add trusted publisher" or "Configure publishing" button
  3. Select Provider:

    • Choose "GitHub Actions" from dropdown
  4. Fill in the form (use values from Phase 1):

    Field Value Notes
    Owner owner-name From git remote, case-sensitive
    Repository repo-name From git remote, exact match required
    Workflow publish.yml Just filename, include .yml extension
    Environment (empty) Leave blank unless you use GitHub Environments
  5. 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
    
  6. 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:

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

package.json

npmjs.com Configuration

Testing

During First Publish

After Success

Key Insights

  1. The challenge: npmjs.com provides no validation - errors only appear during publish
  2. Most critical: Repository URL consistency across three sources
  3. Most misleading: Error messages don't indicate the actual problem
  4. Best practice: Use validation scripts BEFORE configuring Trusted Publisher
  5. Debug strategy: Add safe diagnostic logging to workflow
  6. Success indicator: 422 error means OIDC works - just fix repository URL

References