Unverified Commit 13db1686 authored by Hong Minhee's avatar Hong Minhee
Browse files

Fix npm trusted publishing by making build.yaml the sole entry point

npm's trusted publishing (OIDC) validates the directly triggered workflow,
not reusable workflows called via workflow_call. This was causing publish
failures because npm was looking for the caller workflow instead of the
reusable workflow.

Changes:

- build.yaml: Changed from workflow_call to workflow_run + workflow_dispatch
  triggers, making it the sole entry point for npm publishing
- main.yaml: Renamed workflow to 'main', removed publish-npm job (now handled
  automatically by build.yaml via workflow_run)
- publish-pr.yaml: Changed to trigger build.yaml via workflow_dispatch API
  instead of calling it as a reusable workflow

See: https://docs.npmjs.com/trusted-publishers/



Co-Authored-By: default avatarClaude Opus 4.5 <noreply@anthropic.com>
parent eafcb5e4
Loading
Loading
Loading
Loading
+51 −10
Original line number Diff line number Diff line
name: Publish to npm (reusable)
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
#
# NOTE: This workflow is named "build" to maintain compatibility with legacy
# maintenance branches (1.9-maintenance, 1.8-maintenance, etc.) which have
# their own build.yaml that directly publishes to npm.
#
# IMPORTANT: This workflow MUST be the sole entry point for npm publishing
# to work with npm's trusted publishing (OIDC). npm validates the workflow
# that is directly triggered, not reusable workflows called via workflow_call.
# See: https://docs.npmjs.com/trusted-publishers/
#
# The workflow is triggered in two ways:
# 1. workflow_run: Automatically after main.yaml completes (for regular releases)
# 2. workflow_dispatch: Manually triggered (for PR pre-releases)
name: build

on:
  workflow_call:
  workflow_run:
    workflows: [main]
    types: [completed]
  workflow_dispatch:
    inputs:
      run_id:
        description: 'Run ID of the workflow that created the npm-packages artifact'
        required: true
        type: string
      tag:
        description: 'npm dist-tag to use (e.g., "latest", "dev", "pr-123")'
        required: true
        type: string
      package_pattern:
        description: 'Glob pattern for package tarballs to publish'
        required: false
        type: string
        default: 'fedify-*.tgz'

jobs:
  npm-publish:
    # For workflow_run: only run if the triggering workflow succeeded and was a push event
    # For workflow_dispatch: always run
    if: >-
      github.event_name == 'workflow_dispatch' ||
      (github.event.workflow_run.conclusion == 'success' &&
       github.event.workflow_run.event == 'push')
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
    - name: Determine run ID and tag
      id: config
      run: |
        if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
          echo "run_id=${{ inputs.run_id }}" >> $GITHUB_OUTPUT
          echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
        else
          echo "run_id=${{ github.event.workflow_run.id }}" >> $GITHUB_OUTPUT
          # Determine tag based on ref type from the triggering workflow
          if [[ "${{ github.event.workflow_run.head_branch }}" == refs/tags/* ]] || \
             [[ -n "$(echo '${{ github.event.workflow_run.head_branch }}' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+')" ]]; then
            echo "tag=latest" >> $GITHUB_OUTPUT
          else
            echo "tag=dev" >> $GITHUB_OUTPUT
          fi
        fi
    - uses: actions/download-artifact@v4
      with:
        name: npm-packages
        run-id: ${{ steps.config.outputs.run_id }}
        github-token: ${{ secrets.GITHUB_TOKEN }}
    - run: ls -la
    - name: Setup Node.js
      uses: actions/setup-node@v4
@@ -33,8 +73,9 @@ jobs:
    - name: Publish packages
      run: |
        set -ex
        for pkg in ${{ inputs.package_pattern }}; do
          if [[ "${{ inputs.tag }}" = "latest" ]]; then
        TAG="${{ steps.config.outputs.tag }}"
        for pkg in fedify-*.tgz; do
          if [[ "$TAG" = "latest" ]]; then
            npm publish --logs-dir=. --provenance --access public "$pkg" \
              || grep "Cannot publish over previously published version" *.log
          else
@@ -42,7 +83,7 @@ jobs:
              --logs-dir=. \
              --provenance \
              --access public \
              --tag "${{ inputs.tag }}" \
              --tag "$TAG" \
              "$pkg" \
              || grep "Cannot publish over previously published version" *.log
          fi
+10 −7
Original line number Diff line number Diff line
name: build
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
#
# Main CI workflow for testing, linting, and publishing to JSR.
# npm publishing is handled separately by build.yaml, which is triggered
# automatically via workflow_run after this workflow completes successfully.
# This separation is required for npm's trusted publishing (OIDC) to work
# correctly. See build.yaml for more details.
name: main
on: [push, pull_request]

concurrency:
@@ -295,12 +302,8 @@ jobs:
          ((attempt++))
        done

  publish-npm:
    if: github.event_name == 'push'
    needs: [publish]
    uses: ./.github/workflows/build.yaml
    with:
      tag: ${{ github.ref_type == 'tag' && 'latest' || 'dev' }}
  # NOTE: npm publishing is handled by build.yaml via workflow_run trigger.
  # Do not add npm publish steps here - it will break trusted publishing.

  publish-examples-blog:
    if: github.event_name == 'push'
+77 −3
Original line number Diff line number Diff line
@@ -129,12 +129,86 @@ jobs:
          echo 'EOFLINKS'
        } >> $GITHUB_OUTPUT

  # Trigger build.yaml via workflow_dispatch to publish to npm.
  # This is required because npm's trusted publishing (OIDC) validates
  # the directly triggered workflow, not reusable workflows called via
  # workflow_call. By triggering build.yaml directly, npm sees build.yaml
  # as the entry point and validates against it.
  publish-npm:
    if: inputs.publish_packages
    needs: [publish-packages]
    uses: ./.github/workflows/build.yaml
    runs-on: ubuntu-latest
    permissions:
      actions: write
    steps:
    - name: Trigger build.yaml workflow
      uses: actions/github-script@v7
      with:
        script: |
          await github.rest.actions.createWorkflowDispatch({
            owner: context.repo.owner,
            repo: context.repo.repo,
            workflow_id: 'build.yaml',
            ref: 'main',
            inputs: {
              run_id: '${{ github.run_id }}',
              tag: 'pr-${{ inputs.pr_number }}'
            }
          });
          console.log('Triggered build.yaml workflow with run_id=${{ github.run_id }}, tag=pr-${{ inputs.pr_number }}');
    - name: Wait for npm publish to complete
      uses: actions/github-script@v7
      with:
      tag: pr-${{ inputs.pr_number }}
        script: |
          // Wait a bit for the workflow to start
          await new Promise(resolve => setTimeout(resolve, 10000));

          // Poll for the triggered workflow run
          const maxAttempts = 30;
          const pollInterval = 20000; // 20 seconds

          for (let attempt = 1; attempt <= maxAttempts; attempt++) {
            const runs = await github.rest.actions.listWorkflowRuns({
              owner: context.repo.owner,
              repo: context.repo.repo,
              workflow_id: 'build.yaml',
              event: 'workflow_dispatch',
              per_page: 5
            });

            // Find the run that was triggered by this workflow
            const matchingRun = runs.data.workflow_runs.find(run => {
              // Check if it's recent (within last 5 minutes)
              const runTime = new Date(run.created_at);
              const now = new Date();
              const diffMinutes = (now - runTime) / 1000 / 60;
              return diffMinutes < 5;
            });

            if (matchingRun) {
              console.log(`Found workflow run: ${matchingRun.html_url}`);

              if (matchingRun.status === 'completed') {
                if (matchingRun.conclusion === 'success') {
                  console.log('npm publish completed successfully');
                  return;
                } else {
                  core.setFailed(`npm publish failed with conclusion: ${matchingRun.conclusion}`);
                  return;
                }
              }

              console.log(`Attempt ${attempt}/${maxAttempts}: Workflow status is ${matchingRun.status}, waiting...`);
            } else {
              console.log(`Attempt ${attempt}/${maxAttempts}: Workflow run not found yet, waiting...`);
            }

            if (attempt < maxAttempts) {
              await new Promise(resolve => setTimeout(resolve, pollInterval));
            }
          }

          core.setFailed('Timed out waiting for npm publish workflow to complete');

  publish-docs:
    if: inputs.publish_docs