メインコンテンツへスキップ
1090文字
5分
編集

npmパッケージ公開のOIDC Trusted publishingへの移行

#Classic tokenの廃止と代替手段

2025年12月9日、npmのClassic tokenが完全に廃止された(npm classic tokens revoked, session-based auth and CLI token management now available)。

代わりにローカルではnpm loginによるセッションベースの認証、GitHub ActionsなどのCI/CD環境では2FAをBypassしたGranular Access TokensまたはOIDC Trasted publishingを利用できる。

#Trasted publishingのメリット

npm公式がTrasted publishingを推奨している(Security best practices: Prefer trusted publishing over tokens)。

Trasted publishingは、OpenID Connect (OIDC) を利用しGitHub Actionsとnpmが直接信頼関係を結ぶ仕組みである。 実際、CI/CD環境においてTrasted publishingはTokenを用いた方法の上位互換と言える。

Trasted publishingにはTokenと比較して次の特性がある:

  • 漏洩リスクがない:Secretが存在せず、CI実行時に短命なトークンが発行されるのみ。
  • 管理コストがない:定期的なトークン再発行やSecretのローテーションをする必要がない

#Trusted Publishingへの移行

#STEP 1: npmパッケージ設定の変更

まず、npmでパッケージのSettingsからTrusted Publisherを登録する。

  1. npm公式サイトにログインし、対象パッケージのページへ移動。
  2. Settingsタブをクリックし、Trusted PublisherにあるSelect your publisherから利用するCI/CD環境を選択する。
  3. GitHub Actionsを選択した場合、以下の情報を入力する。
    • Organization or user: GitHub Actionsを実行するリポジトリのOrganizationまたはUser名
    • Repository: GitHub Actionsを実行するリポジトリ名
    • Workflow filename: npm publishを実行するワークフローファイル名
    • Environment name: GitHub Actions environmentを利用している場合は環境名を指定

この設定により、「指定したリポジトリ」の「指定したワークフローファイル」からのみ、Publish権限が発行されるようになる。

#STEP 2: GitHub Actionsワークフローの修正

次に、GitHub ActionsのYAMLファイルを修正する。主な変更点は permissions の追加と env の削除。

修正前の例:

yaml
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '24.x'
          registry-url: 'https://registry.npmjs.org'
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # ← 削除

修正後の例:

yaml
jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write  # ←  OIDCトークン発行のために必須
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
        # npm v11.5.1以上が必要
        # Node.js v20やv22、古い特定のv24バージョンを利用している場合、別途npmの更新が必要
          node-version: '24.x'
          registry-url: 'https://registry.npmjs.org'
#    - run: npm install -g npm@latest
      - run: npm publish

#Trasted publishingの制約

npmのTrasted publishingは、現状いくつか制約がある。

  • Trasted publisherが1つに限定される。
    • 特定のリポジトリの特定のファイルからのみしか利用できない。
  • プライベートリポジトリではprovenanceを生成できない。
    • npm publish --provenanceを実行するとpublishに失敗する。
  • Trasted publisherとpackage.jsonのrepositoryフィールドの一致が必要
    • repositoryが未記入または一致しない場合、422 Unprocessable Entityエラーが発生する。
    • "repository": "https://github.com/orgs/repository-name.git"といった指定が必要。
    • これはprovenanceを生成しない場合でも発生する。
  • npm v11.5.1以上が必要。
    • 最新のNode.js v24以上であれば問題はないが、それ未満では別途npmの更新が必要。
    • それと分かるエラーは出ないため注意。遭遇した例では404エラーが発生した。
  • 利用できるCI/CD環境はnpmが対応している環境に限定される。

#GitHub Actionsワークフローの実装例

デフォルトブランチにコミットが行われる度に、可能ならリリースまたはアルファリリースを行うリポジトリの実装例。 Trashed publisherは単一のワークフローファイルに紐付ける必要があるため、Trasted publishingを行う場合リリースとアルファリリースでワークフローを分けることは出来ない。

yaml
name: publish
on:
  push:
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  CI: true

permissions:
  contents: read
  id-token: write

jobs:
  check:
    runs-on: ubuntu-latest
    timeout-minutes: 3
    outputs:
      can_publish: ${{ steps.can-publish.outputs.can_publish }}
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with:
          node-version: "24.x"
          registry-url: "https://registry.npmjs.org"
      - name: Install dependencies
        run: yarn install --frozen-lockfile
      - name: Build package
        run: yarn build
      - name: Check if can publish
        id: can-publish
        run: |
          npx can-npm-publish --verbose && echo "can_publish=true" >> $GITHUB_OUTPUT || echo "can_publish=false" >> $GITHUB_OUTPUT
        continue-on-error: true
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist
          retention-days: 1

  publish-release:
    needs: check
    if: needs.check.outputs.can_publish == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 3
    steps:
      - uses: actions/checkout@v5

      - uses: actions/setup-node@v5
        with:
          node-version: "24.x"
          registry-url: "https://registry.npmjs.org"
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist
      - name: Publish
        run: |
          npm publish

  publish-alpha:
    needs: check
    if: needs.check.outputs.can_publish != 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 3
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v5
        with:
          node-version: "24.x"
          registry-url: "https://registry.npmjs.org"
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist
      - name: Update version
        run: |
          git config --global user.email "github-actions[bot]@users.noreply.github.com"
          git config --global user.name "github-actions[bot]"
          yarn version --prepatch --preid alpha-$(($(date +%s%N)/1000000))-$(git rev-parse --short HEAD)
      - name: Publish
        run: |
          npm publish --tag alpha