Skip to content

Commit c433c1b

Browse files
committed
fix: unify all npm publish workflows into a single top-level publish-npm.yml
## Problem npm Trusted Publishing matches the `workflow_ref` OIDC claim, which is always the top-level workflow filename. npm allows only ONE trusted publisher per package. The prior migration (#57099) used `workflow_call` to route all publishes through `publish-npm.yml`, but `workflow_ref` resolves to the *caller* (e.g. `nightly.yml`), not the reusable child — so the Trusted Publisher entry for `publish-npm.yml` never matches. ## Solution Merge all three publish entry points into `publish-npm.yml` itself, triggered by all three event types: - `push.tags: v0.*` → release mode (was publish-release.yml) - `schedule + workflow_dispatch` → nightly mode (was nightly.yml) - `push.branches: main, *-stable` → bumped-packages mode (was publish-bumped-packages.yml) A `determine_mode` job inspects the trigger and sets the mode. Downstream jobs use conditional `if:` expressions to run only the relevant build/publish steps. Since `publish-npm.yml` is now always the top-level workflow, `workflow_ref` always resolves to `publish-npm.yml` ✅. ## Key design points - **No JS changes** — the publish scripts are unchanged. The build and publish still happen in the same job, on the same runner, with the same container. No artifact handoff or pack-only mode needed. - **Reusable workflow_call children are fine** — prebuild-ios-*.yml, generate-changelog.yml, etc. remain as `workflow_call` children. Only the file that calls `npm publish` must be the top-level workflow; child workflows don't affect the OIDC claim. - **Old workflow files kept as stubs** — publish-release.yml, nightly.yml, and publish-bumped-packages.yml are replaced with minimal deprecation notices so that external links/dashboards don't 404. - **`always()` + explicit result checks** — publish_react_native depends on build_android (nightly-only) and prebuild_* jobs. In release mode, build_android is skipped. The `always()` prevents cascading skips, while explicit result checks ensure we don't publish after a failed build. ## npm Trusted Publisher config (manual step) For each of the 24 packages, configure on npmjs.com: Organization: react Repository: react-native Workflow: publish-npm.yml Environment: npm-publish
1 parent 79adce3 commit c433c1b

4 files changed

Lines changed: 214 additions & 283 deletions

File tree

.github/workflows/nightly.yml

Lines changed: 0 additions & 90 deletions
This file was deleted.

.github/workflows/publish-bumped-packages.yml

Lines changed: 0 additions & 19 deletions
This file was deleted.

.github/workflows/publish-npm.yml

Lines changed: 214 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,139 @@
1-
# Reusable workflow that performs every `npm publish` in this repo.
1+
# Single top-level workflow for every npm publish in this repo.
22
#
3-
# Why this exists: npmjs.com Trusted Publishing accepts only ONE
4-
# (org, repo, workflow_filename, environment) tuple per package. If
5-
# `react-native` were published from `publish-release.yml` AND
6-
# `nightly.yml` directly, we'd need two Trusted Publisher entries per
7-
# package — npm rejects that. By moving every `npm publish` into this
8-
# single reusable workflow file, the OIDC `job_workflow_ref` claim
9-
# always resolves to `publish-npm.yml` regardless of which top-level
10-
# workflow triggered the run, so each package needs exactly one
11-
# Trusted Publisher entry pointing here.
3+
# Why: npmjs.com Trusted Publishing matches the `workflow_ref` OIDC claim,
4+
# which is always the TOP-LEVEL workflow filename. npm allows only ONE
5+
# trusted publisher per package, so every `npm publish` must originate
6+
# from the same top-level file. By consolidating all publish triggers
7+
# here, the OIDC claim is always `publish-npm.yml`.
128
#
13-
# See https://docs.npmjs.com/trusted-publishers and
14-
# https://docs.github.com/en/actions/sharing-automations/reusing-workflows .
15-
name: Publish to npm (reusable)
9+
# This replaces the previous separate entry points:
10+
# - publish-release.yml (tag push) → mode=release
11+
# - nightly.yml (cron/dispatch) → mode=nightly
12+
# - publish-bumped-packages.yml (branch push) → mode=bumped-packages
13+
#
14+
# See https://docs.npmjs.com/trusted-publishers
15+
name: Publish to npm
1616

1717
on:
18-
workflow_call:
19-
inputs:
20-
mode:
21-
description: |
22-
'react-native' runs the full Android/iOS-prebuilt + JS build
23-
and publishes via scripts/releases-ci/publish-npm.js (which
24-
publishes `react-native` and, in nightly mode, every
25-
@react-native/* package). 'monorepo-packages' runs only the
26-
JS build and publishes via
27-
scripts/releases-ci/publish-updated-packages.js (delta-based,
28-
gated on a #publish-packages-to-npm commit message).
29-
type: string
30-
required: true
31-
release-type:
32-
description: "For mode=react-native: release | nightly | dry-run."
33-
type: string
34-
required: false
35-
default: "dry-run"
36-
skip-apple-prebuilts:
37-
description: "For mode=react-native: skip downloading prebuilt Apple artifacts."
38-
type: boolean
39-
required: false
40-
default: false
18+
push:
19+
tags:
20+
- "v0.*.*" # This should match v0.X.Y
21+
- "v0.*.*-rc.*" # This should match v0.X.Y-RC.0
22+
branches:
23+
- "main"
24+
- "*-stable"
25+
workflow_dispatch:
26+
# nightly build @ 2:15 AM UTC
27+
schedule:
28+
- cron: "15 2 * * *"
29+
30+
permissions:
31+
contents: read
4132

4233
jobs:
43-
publish-react-native:
44-
if: inputs.mode == 'react-native'
34+
# ─── Determine what kind of publish this is ──────────────────────
35+
determine_mode:
36+
runs-on: ubuntu-latest
37+
if: github.repository == 'react/react-native'
38+
outputs:
39+
mode: ${{ steps.mode.outputs.mode }}
40+
release-type: ${{ steps.mode.outputs.release-type }}
41+
steps:
42+
- id: mode
43+
run: |
44+
if [[ "${{ github.ref_type }}" == "tag" ]]; then
45+
echo "mode=release" >> $GITHUB_OUTPUT
46+
echo "release-type=release" >> $GITHUB_OUTPUT
47+
elif [[ "${{ github.event_name }}" == "schedule" || "${{ github.event_name }}" == "workflow_dispatch" ]]; then
48+
echo "mode=nightly" >> $GITHUB_OUTPUT
49+
echo "release-type=nightly" >> $GITHUB_OUTPUT
50+
elif [[ "${{ github.event_name }}" == "push" ]]; then
51+
echo "mode=bumped-packages" >> $GITHUB_OUTPUT
52+
echo "release-type=" >> $GITHUB_OUTPUT
53+
fi
54+
- run: |
55+
echo "Mode: ${{ steps.mode.outputs.mode }}"
56+
echo "Release type: ${{ steps.mode.outputs.release-type }}"
57+
58+
# ─── Release-only: extract Hermes version for draft release ──────
59+
set_hermes_version:
60+
runs-on: ubuntu-latest
61+
if: github.ref_type == 'tag'
62+
outputs:
63+
HERMES_VERSION: ${{ steps.set_hermes_version.outputs.HERMES_VERSION }}
64+
steps:
65+
- name: Checkout
66+
uses: actions/checkout@v6
67+
- id: set_hermes_version
68+
run: |
69+
hermes_version=$(grep -oE 'HERMES_VERSION_NAME=([0-9]+\.[0-9]+\.[0-9]+)' packages/react-native/sdks/hermes-engine/version.properties | cut -d'=' -f2)
70+
echo "HERMES_VERSION=$hermes_version" >> $GITHUB_OUTPUT
71+
echo "HERMES_VERSION=$hermes_version"
72+
73+
# ─── Apple prebuilds (release + nightly) ─────────────────────────
74+
prebuild_apple_dependencies:
75+
needs: [determine_mode]
76+
if: needs.determine_mode.outputs.mode == 'release' || needs.determine_mode.outputs.mode == 'nightly'
77+
uses: ./.github/workflows/prebuild-ios-dependencies.yml
78+
secrets: inherit
79+
80+
prebuild_react_native_core:
81+
needs: [determine_mode, prebuild_apple_dependencies]
82+
if: needs.determine_mode.outputs.mode == 'release' || needs.determine_mode.outputs.mode == 'nightly'
83+
uses: ./.github/workflows/prebuild-ios-core.yml
84+
secrets: inherit
85+
with:
86+
use-hermes-prebuilt: ${{ needs.determine_mode.outputs.mode == 'nightly' }}
87+
version-type: ${{ needs.determine_mode.outputs.mode == 'nightly' && 'nightly' || '' }}
88+
89+
# ─── Android build (nightly only — releases handle this in the
90+
# build-npm-package action's Gradle step) ─────────────────────
91+
build_android:
92+
needs: [determine_mode]
93+
if: needs.determine_mode.outputs.mode == 'nightly'
94+
runs-on: ubuntu-latest
95+
container:
96+
image: reactnativecommunity/react-native-android:latest
97+
env:
98+
TERM: "dumb"
99+
# Set the encoding to resolve a known character encoding issue with decompressing tar.gz files in containers
100+
# via Gradle: https://github.com/gradle/gradle/issues/23391#issuecomment-1878979127
101+
LC_ALL: C.UTF8
102+
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
103+
ORG_GRADLE_PROJECT_SIGNING_PWD: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_PWD }}
104+
ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_KEY }}
105+
ORG_GRADLE_PROJECT_SONATYPE_USERNAME: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPE_USERNAME }}
106+
ORG_GRADLE_PROJECT_SONATYPE_PASSWORD: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPE_PASSWORD }}
107+
REACT_NATIVE_DOWNLOADS_DIR: /opt/react-native-downloads
108+
steps:
109+
- name: Checkout
110+
uses: actions/checkout@v6
111+
- name: Build Android
112+
uses: ./.github/actions/build-android
113+
with:
114+
release-type: nightly
115+
gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
116+
117+
# ─── Build + Publish: react-native + all @react-native/* packages
118+
# (release and nightly modes) ─────────────────────────────────
119+
publish_react_native:
120+
needs:
121+
[
122+
determine_mode,
123+
build_android,
124+
prebuild_apple_dependencies,
125+
prebuild_react_native_core,
126+
]
127+
# For nightly, also wait on build_android. Use always() so this
128+
# job isn't skipped when build_android is skipped (release mode).
129+
# The explicit status checks below handle the real gating.
130+
if: |
131+
always() &&
132+
(needs.determine_mode.outputs.mode == 'release' || needs.determine_mode.outputs.mode == 'nightly') &&
133+
needs.determine_mode.result == 'success' &&
134+
needs.prebuild_apple_dependencies.result == 'success' &&
135+
needs.prebuild_react_native_core.result == 'success' &&
136+
(needs.determine_mode.outputs.mode == 'release' || needs.build_android.result == 'success')
45137
runs-on: ubuntu-latest
46138
environment: npm-publish
47139
# `id-token: write` is required so the npm CLI can mint the OIDC
@@ -91,14 +183,17 @@ jobs:
91183
- name: Build and Publish NPM Package
92184
uses: ./.github/actions/build-npm-package
93185
with:
94-
release-type: ${{ inputs.release-type }}
186+
release-type: ${{ needs.determine_mode.outputs.release-type }}
95187
gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
96-
skip-apple-prebuilts: ${{ inputs.skip-apple-prebuilts && 'true' || 'false' }}
97188

98-
publish-monorepo-packages:
99-
if: inputs.mode == 'monorepo-packages'
189+
# ─── Publish bumped monorepo packages (branch push mode) ─────────
190+
publish_bumped_packages:
191+
needs: [determine_mode]
192+
if: needs.determine_mode.outputs.mode == 'bumped-packages'
100193
runs-on: ubuntu-latest
101194
environment: npm-publish
195+
# `id-token: write` is required so the npm CLI can mint the OIDC
196+
# token that npm Trusted Publishing exchanges for a publish token.
102197
permissions:
103198
contents: read
104199
id-token: write
@@ -134,3 +229,80 @@ jobs:
134229
run: yarn build-types --skip-snapshot
135230
- name: Find and publish all bumped packages
136231
run: node ./scripts/releases-ci/publish-updated-packages.js
232+
233+
# ─── Release-only: post-publish steps ────────────────────────────
234+
post_publish:
235+
runs-on: ubuntu-latest
236+
needs: [determine_mode, publish_react_native]
237+
if: needs.determine_mode.outputs.mode == 'release'
238+
env:
239+
REACT_NATIVE_BOT_GITHUB_TOKEN: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
240+
steps:
241+
- name: Checkout
242+
uses: actions/checkout@v6
243+
with:
244+
fetch-depth: 0
245+
fetch-tags: true
246+
- name: Publish @react-native-community/template
247+
id: publish-template-to-npm
248+
uses: actions/github-script@v8
249+
with:
250+
github-token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
251+
script: |
252+
const {publishTemplate} = require('./.github/workflow-scripts/publishTemplate.js')
253+
const version = "${{ github.ref_name }}"
254+
const isDryRun = false
255+
await publishTemplate(github, version, isDryRun);
256+
- name: Wait for template to be published
257+
timeout-minutes: 3
258+
uses: actions/github-script@v8
259+
with:
260+
github-token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
261+
script: |
262+
const {verifyPublishedTemplate, isLatest} = require('./.github/workflow-scripts/publishTemplate.js')
263+
const version = "${{ github.ref_name }}"
264+
await verifyPublishedTemplate(version, isLatest());
265+
- name: Update rn-diff-purge to generate upgrade-support diff
266+
run: |
267+
curl -X POST https://api.github.com/repos/react-native-community/rn-diff-purge/dispatches \
268+
-H "Accept: application/vnd.github.v3+json" \
269+
-H "Authorization: Bearer $REACT_NATIVE_BOT_GITHUB_TOKEN" \
270+
-d "{\"event_type\": \"publish\", \"client_payload\": { \"version\": \"${{ github.ref_name }}\" }}"
271+
- name: Verify Release is on NPM
272+
timeout-minutes: 3
273+
uses: actions/github-script@v8
274+
with:
275+
github-token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
276+
script: |
277+
const {verifyReleaseOnNpm} = require('./.github/workflow-scripts/verifyReleaseOnNpm.js');
278+
const {isLatest} = require('./.github/workflow-scripts/publishTemplate.js');
279+
const version = "${{ github.ref_name }}";
280+
await verifyReleaseOnNpm(version, isLatest());
281+
- name: Verify that artifacts are on Maven
282+
uses: actions/github-script@v8
283+
with:
284+
script: |
285+
const {verifyArtifactsAreOnMaven} = require('./.github/workflow-scripts/verifyArtifactsAreOnMaven.js');
286+
const version = "${{ github.ref_name }}";
287+
await verifyArtifactsAreOnMaven(version);
288+
289+
# ─── Release-only: changelog, podfile bump, draft release ────────
290+
generate_changelog:
291+
needs: [determine_mode, publish_react_native]
292+
if: needs.determine_mode.outputs.mode == 'release'
293+
uses: ./.github/workflows/generate-changelog.yml
294+
secrets: inherit
295+
296+
bump_podfile_lock:
297+
needs: [determine_mode, publish_react_native]
298+
if: needs.determine_mode.outputs.mode == 'release'
299+
uses: ./.github/workflows/bump-podfile-lock.yml
300+
secrets: inherit
301+
302+
create_draft_release:
303+
needs: [determine_mode, generate_changelog, set_hermes_version]
304+
if: needs.determine_mode.outputs.mode == 'release'
305+
uses: ./.github/workflows/create-draft-release.yml
306+
secrets: inherit
307+
with:
308+
hermesVersion: ${{ needs.set_hermes_version.outputs.HERMES_VERSION }}

0 commit comments

Comments
 (0)