Skip to content

Commit 13db168

Browse files
dahliaclaude
andcommitted
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: Claude Opus 4.5 <noreply@anthropic.com>
1 parent eafcb5e commit 13db168

3 files changed

Lines changed: 138 additions & 20 deletions

File tree

.github/workflows/build.yaml

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,68 @@
1-
name: Publish to npm (reusable)
1+
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
2+
#
3+
# NOTE: This workflow is named "build" to maintain compatibility with legacy
4+
# maintenance branches (1.9-maintenance, 1.8-maintenance, etc.) which have
5+
# their own build.yaml that directly publishes to npm.
6+
#
7+
# IMPORTANT: This workflow MUST be the sole entry point for npm publishing
8+
# to work with npm's trusted publishing (OIDC). npm validates the workflow
9+
# that is directly triggered, not reusable workflows called via workflow_call.
10+
# See: https://docs.npmjs.com/trusted-publishers/
11+
#
12+
# The workflow is triggered in two ways:
13+
# 1. workflow_run: Automatically after main.yaml completes (for regular releases)
14+
# 2. workflow_dispatch: Manually triggered (for PR pre-releases)
15+
name: build
216

317
on:
4-
workflow_call:
18+
workflow_run:
19+
workflows: [main]
20+
types: [completed]
21+
workflow_dispatch:
522
inputs:
23+
run_id:
24+
description: 'Run ID of the workflow that created the npm-packages artifact'
25+
required: true
26+
type: string
627
tag:
728
description: 'npm dist-tag to use (e.g., "latest", "dev", "pr-123")'
829
required: true
930
type: string
10-
package_pattern:
11-
description: 'Glob pattern for package tarballs to publish'
12-
required: false
13-
type: string
14-
default: 'fedify-*.tgz'
1531

1632
jobs:
1733
npm-publish:
34+
# For workflow_run: only run if the triggering workflow succeeded and was a push event
35+
# For workflow_dispatch: always run
36+
if: >-
37+
github.event_name == 'workflow_dispatch' ||
38+
(github.event.workflow_run.conclusion == 'success' &&
39+
github.event.workflow_run.event == 'push')
1840
runs-on: ubuntu-latest
1941
permissions:
2042
id-token: write
2143
contents: read
2244
steps:
45+
- name: Determine run ID and tag
46+
id: config
47+
run: |
48+
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
49+
echo "run_id=${{ inputs.run_id }}" >> $GITHUB_OUTPUT
50+
echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
51+
else
52+
echo "run_id=${{ github.event.workflow_run.id }}" >> $GITHUB_OUTPUT
53+
# Determine tag based on ref type from the triggering workflow
54+
if [[ "${{ github.event.workflow_run.head_branch }}" == refs/tags/* ]] || \
55+
[[ -n "$(echo '${{ github.event.workflow_run.head_branch }}' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+')" ]]; then
56+
echo "tag=latest" >> $GITHUB_OUTPUT
57+
else
58+
echo "tag=dev" >> $GITHUB_OUTPUT
59+
fi
60+
fi
2361
- uses: actions/download-artifact@v4
2462
with:
2563
name: npm-packages
64+
run-id: ${{ steps.config.outputs.run_id }}
65+
github-token: ${{ secrets.GITHUB_TOKEN }}
2666
- run: ls -la
2767
- name: Setup Node.js
2868
uses: actions/setup-node@v4
@@ -33,16 +73,17 @@ jobs:
3373
- name: Publish packages
3474
run: |
3575
set -ex
36-
for pkg in ${{ inputs.package_pattern }}; do
37-
if [[ "${{ inputs.tag }}" = "latest" ]]; then
76+
TAG="${{ steps.config.outputs.tag }}"
77+
for pkg in fedify-*.tgz; do
78+
if [[ "$TAG" = "latest" ]]; then
3879
npm publish --logs-dir=. --provenance --access public "$pkg" \
3980
|| grep "Cannot publish over previously published version" *.log
4081
else
4182
npm publish \
4283
--logs-dir=. \
4384
--provenance \
4485
--access public \
45-
--tag "${{ inputs.tag }}" \
86+
--tag "$TAG" \
4687
"$pkg" \
4788
|| grep "Cannot publish over previously published version" *.log
4889
fi

.github/workflows/main.yaml

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
name: build
1+
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
2+
#
3+
# Main CI workflow for testing, linting, and publishing to JSR.
4+
# npm publishing is handled separately by build.yaml, which is triggered
5+
# automatically via workflow_run after this workflow completes successfully.
6+
# This separation is required for npm's trusted publishing (OIDC) to work
7+
# correctly. See build.yaml for more details.
8+
name: main
29
on: [push, pull_request]
310

411
concurrency:
@@ -295,12 +302,8 @@ jobs:
295302
((attempt++))
296303
done
297304
298-
publish-npm:
299-
if: github.event_name == 'push'
300-
needs: [publish]
301-
uses: ./.github/workflows/build.yaml
302-
with:
303-
tag: ${{ github.ref_type == 'tag' && 'latest' || 'dev' }}
305+
# NOTE: npm publishing is handled by build.yaml via workflow_run trigger.
306+
# Do not add npm publish steps here - it will break trusted publishing.
304307

305308
publish-examples-blog:
306309
if: github.event_name == 'push'

.github/workflows/publish-pr.yaml

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,86 @@ jobs:
129129
echo 'EOFLINKS'
130130
} >> $GITHUB_OUTPUT
131131
132+
# Trigger build.yaml via workflow_dispatch to publish to npm.
133+
# This is required because npm's trusted publishing (OIDC) validates
134+
# the directly triggered workflow, not reusable workflows called via
135+
# workflow_call. By triggering build.yaml directly, npm sees build.yaml
136+
# as the entry point and validates against it.
132137
publish-npm:
133138
if: inputs.publish_packages
134139
needs: [publish-packages]
135-
uses: ./.github/workflows/build.yaml
136-
with:
137-
tag: pr-${{ inputs.pr_number }}
140+
runs-on: ubuntu-latest
141+
permissions:
142+
actions: write
143+
steps:
144+
- name: Trigger build.yaml workflow
145+
uses: actions/github-script@v7
146+
with:
147+
script: |
148+
await github.rest.actions.createWorkflowDispatch({
149+
owner: context.repo.owner,
150+
repo: context.repo.repo,
151+
workflow_id: 'build.yaml',
152+
ref: 'main',
153+
inputs: {
154+
run_id: '${{ github.run_id }}',
155+
tag: 'pr-${{ inputs.pr_number }}'
156+
}
157+
});
158+
console.log('Triggered build.yaml workflow with run_id=${{ github.run_id }}, tag=pr-${{ inputs.pr_number }}');
159+
- name: Wait for npm publish to complete
160+
uses: actions/github-script@v7
161+
with:
162+
script: |
163+
// Wait a bit for the workflow to start
164+
await new Promise(resolve => setTimeout(resolve, 10000));
165+
166+
// Poll for the triggered workflow run
167+
const maxAttempts = 30;
168+
const pollInterval = 20000; // 20 seconds
169+
170+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
171+
const runs = await github.rest.actions.listWorkflowRuns({
172+
owner: context.repo.owner,
173+
repo: context.repo.repo,
174+
workflow_id: 'build.yaml',
175+
event: 'workflow_dispatch',
176+
per_page: 5
177+
});
178+
179+
// Find the run that was triggered by this workflow
180+
const matchingRun = runs.data.workflow_runs.find(run => {
181+
// Check if it's recent (within last 5 minutes)
182+
const runTime = new Date(run.created_at);
183+
const now = new Date();
184+
const diffMinutes = (now - runTime) / 1000 / 60;
185+
return diffMinutes < 5;
186+
});
187+
188+
if (matchingRun) {
189+
console.log(`Found workflow run: ${matchingRun.html_url}`);
190+
191+
if (matchingRun.status === 'completed') {
192+
if (matchingRun.conclusion === 'success') {
193+
console.log('npm publish completed successfully');
194+
return;
195+
} else {
196+
core.setFailed(`npm publish failed with conclusion: ${matchingRun.conclusion}`);
197+
return;
198+
}
199+
}
200+
201+
console.log(`Attempt ${attempt}/${maxAttempts}: Workflow status is ${matchingRun.status}, waiting...`);
202+
} else {
203+
console.log(`Attempt ${attempt}/${maxAttempts}: Workflow run not found yet, waiting...`);
204+
}
205+
206+
if (attempt < maxAttempts) {
207+
await new Promise(resolve => setTimeout(resolve, pollInterval));
208+
}
209+
}
210+
211+
core.setFailed('Timed out waiting for npm publish workflow to complete');
138212
139213
publish-docs:
140214
if: inputs.publish_docs

0 commit comments

Comments
 (0)