Skip to content

Commit 703d6fd

Browse files
robhoganfacebook-github-bot
authored andcommitted
Migrate npm publish to OIDC Trusted Publishing via a reusable workflow (#57099)
Summary: Replaces the long-lived `GHA_NPM_TOKEN` automation token with npm Trusted Publishing (OIDC) for every `npm publish` invoked from this repo's GitHub Actions. Why a reusable workflow: npmjs.com Trusted Publishing accepts only ONE (org, repo, workflow_filename, environment) tuple per package. Today, packages are published from multiple workflow files: - `react-native` from publish-release.yml + nightly.yml - every `react-native/*` from nightly.yml + publish-bumped-packages.yml A naive per-workflow OIDC migration would require two Trusted Publisher entries per package, which npm doesn't support. Instead, this diff funnels every `npm publish` through one new file — `.github/workflows/publish-npm.yml` — invoked via `workflow_call` from the existing top-level workflows. The OIDC `job_workflow_ref` claim therefore always resolves to `publish-npm.yml`, so each package needs exactly one Trusted Publisher entry pointing here. What changes: * New `.github/workflows/publish-npm.yml`: reusable workflow with a `mode` input. `mode: react-native` runs the full Android + iOS prebuilt + JS build path (used by release & nightly, publishes `react-native` and — in nightly mode — every `react-native/*` package via `scripts/releases-ci/publish-npm.js`). `mode: monorepo-packages` runs only the JS build and publishes the delta-bumped packages via `scripts/releases-ci/publish-updated-packages.js` (used by publish-bumped-packages.yml). Both jobs grant `id-token: write` so the npm CLI can mint the OIDC token for Trusted Publishing. * `.github/workflows/publish-release.yml`: replace the `build_npm_package` job's inline build/publish steps with a `uses: ./.github/workflows/publish-npm.yml` call. The template-publish, rn-diff-purge, npm-verify, and Maven-verify steps move into a new `post_publish` follow-up job that `needs: [build_npm_package]`. Drops `GHA_NPM_TOKEN` from the env. * `.github/workflows/nightly.yml`: same — `build_npm_package` now delegates to the reusable workflow. Drops `GHA_NPM_TOKEN` and the obsolete `Verify NPM token` precheck (Trusted Publishing has no pre-mintable token to validate; failures surface at `npm publish` time). * `.github/workflows/publish-bumped-packages.yml`: shrinks to a thin trigger wrapper that calls the reusable workflow with `mode: monorepo-packages`. * `.github/workflows/create-release.yml`: drop the obsolete `Verify NPM token` step. * `.github/actions/build-npm-package`: drop the `gha-npm-token` input and the `Set npm credentials` step that wrote `_authToken`. Pass `registry-url: https://registry.npmjs.org` to setup-node so `actions/setup-node@v6` writes a `.npmrc` configured to consume the OIDC token at publish time. * `.github/actions/setup-node`: thread a new `registry-url` input through to `actions/setup-node@v6`. The publish scripts themselves (`scripts/releases-ci/publish-npm.js`, `scripts/releases-ci/publish-updated-packages.js`, `scripts/releases/utils/npm-utils.js`) are unchanged: they shell out to plain `npm publish`, which performs the OIDC exchange transparently when it sees a GitHub Actions OIDC environment and a Trusted Publisher configured for the package on npmjs.com. Note: this diff only changes the workflow definitions. Each package on npmjs.com must additionally be configured with a Trusted Publisher pointing at: - org: facebook - repo: react-native - workflow filename: publish-npm.yml - environment: (blank — none configured) The npm CLI's OIDC exchange returns 404 until that registry-side config is in place. Trusted Publisher entries are additive on npmjs.com (don't enable "Require Trusted Publishing" yet) so the existing token-based flow keeps working through the cutover. See the stack landing notes for the full package list and UI steps. Backport: this also needs picking back to `*-stable` branches before "Require Trusted Publishing" is enabled on any package, since GitHub Actions runs the workflow file from the ref that triggers it. Changelog: [Internal] Reviewed By: cortinico, cipolleschi Differential Revision: D107805971
1 parent df0d7de commit 703d6fd

7 files changed

Lines changed: 151 additions & 106 deletions

File tree

.github/actions/build-npm-package/action.yml

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ inputs:
44
release-type:
55
required: true
66
description: The type of release we are building. It could be nightly, release or dry-run
7-
gha-npm-token:
8-
required: false
9-
description: The GHA npm token, required only to publish to npm
10-
default: ''
117
gradle-cache-encryption-key:
128
description: The encryption key needed to store the Gradle Configuration cache
139
skip-apple-prebuilts:
@@ -45,6 +41,8 @@ runs:
4541
cache-encryption-key: ${{ inputs.gradle-cache-encryption-key }}
4642
- name: Setup node.js
4743
uses: ./.github/actions/setup-node
44+
with:
45+
registry-url: 'https://registry.npmjs.org'
4846
- name: Install dependencies
4947
uses: ./.github/actions/yarn-install
5048
- name: Build packages
@@ -53,12 +51,9 @@ runs:
5351
- name: Build types
5452
shell: bash
5553
run: yarn build-types --skip-snapshot
56-
# Continue with publish steps
57-
- name: Set npm credentials
58-
if: ${{ inputs.release-type == 'release' ||
59-
inputs.release-type == 'nightly' }}
60-
shell: bash
61-
run: echo "//registry.npmjs.org/:_authToken=${{ inputs.gha-npm-token }}" > ~/.npmrc
54+
# `npm publish` below authenticates via npm Trusted Publishing (OIDC).
55+
# The caller (the reusable `publish-npm.yml` workflow) MUST grant
56+
# `id-token: write`; this composite action runs inside that job.
6257
- name: Publish NPM
6358
shell: bash
6459
run: |

.github/actions/setup-node/action.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ inputs:
55
description: 'The node.js version to use'
66
required: false
77
default: '22.14.0'
8+
registry-url:
9+
description: |
10+
Optional npm registry URL passed through to actions/setup-node. Set on
11+
jobs that publish to npm so setup-node writes a `.npmrc` configured to
12+
pick up the OIDC-minted token from npm Trusted Publishing.
13+
required: false
14+
default: ''
815
runs:
916
using: "composite"
1017
steps:
@@ -13,3 +20,4 @@ runs:
1320
with:
1421
node-version: ${{ inputs.node-version }}
1522
cache: yarn
23+
registry-url: ${{ inputs.registry-url }}

.github/workflows/create-release.yml

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,6 @@ jobs:
2828
token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
2929
fetch-depth: 0
3030
fetch-tags: 'true'
31-
- name: Verify NPM token
32-
run: |
33-
if [[ -z "$GHA_NPM_TOKEN" ]]; then
34-
echo "⚠️ No NPM token found. Skipping validation."
35-
exit 0
36-
fi
37-
echo "//registry.npmjs.org/:_authToken=$GHA_NPM_TOKEN" > ~/.npmrc
38-
if ! npm whoami > /dev/null 2>&1; then
39-
echo "❌ NPM token is invalid or expired. Aborting release."
40-
exit 1
41-
fi
42-
echo "✅ NPM token is valid ($(npm whoami))"
43-
rm -f ~/.npmrc
44-
env:
45-
GHA_NPM_TOKEN: ${{ secrets.GHA_NPM_TOKEN }}
4631
- name: Check if on stable branch
4732
id: check_stable_branch
4833
run: |

.github/workflows/nightly.yml

Lines changed: 17 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -63,51 +63,28 @@ jobs:
6363
release-type: ${{ needs.set_release_type.outputs.RELEASE_TYPE }}
6464
gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
6565

66+
# Delegate the actual npm publish to the shared reusable workflow so
67+
# every `npm publish` in this repo originates from one workflow file —
68+
# required because npm Trusted Publishing only accepts one
69+
# (org, repo, workflow_filename) per package.
6670
build_npm_package:
67-
runs-on: 8-core-ubuntu
6871
needs:
6972
[
7073
set_release_type,
7174
build_android,
7275
prebuild_apple_dependencies,
7376
prebuild_react_native_core,
7477
]
75-
container:
76-
image: reactnativecommunity/react-native-android:latest
77-
env:
78-
TERM: "dumb"
79-
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
80-
# Set the encoding to resolve a known character encoding issue with decompressing tar.gz files in containers
81-
# via Gradle: https://github.com/gradle/gradle/issues/23391#issuecomment-1878979127
82-
LC_ALL: C.UTF8
83-
# By default we only build ARM64 to save time/resources. For release/nightlies, we override this value to build all archs.
84-
ORG_GRADLE_PROJECT_reactNativeArchitectures: "arm64-v8a"
85-
REACT_NATIVE_DOWNLOADS_DIR: /opt/react-native-downloads
86-
env:
87-
GHA_NPM_TOKEN: ${{ secrets.GHA_NPM_TOKEN }}
88-
ORG_GRADLE_PROJECT_SIGNING_PWD: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_PWD }}
89-
ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_KEY }}
90-
ORG_GRADLE_PROJECT_SONATYPE_USERNAME: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPE_USERNAME }}
91-
ORG_GRADLE_PROJECT_SONATYPE_PASSWORD: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPE_PASSWORD }}
92-
steps:
93-
- name: Checkout
94-
uses: actions/checkout@v6
95-
- name: Verify NPM token
96-
run: |
97-
if [[ -z "$GHA_NPM_TOKEN" ]]; then
98-
echo "⚠️ No NPM token found. Skipping validation."
99-
exit 0
100-
fi
101-
echo "//registry.npmjs.org/:_authToken=$GHA_NPM_TOKEN" > ~/.npmrc
102-
if ! npm whoami > /dev/null 2>&1; then
103-
echo "❌ NPM token is invalid or expired. Aborting release."
104-
exit 1
105-
fi
106-
echo "✅ NPM token is valid ($(npm whoami))"
107-
rm -f ~/.npmrc
108-
- name: Build and Publish NPM Package
109-
uses: ./.github/actions/build-npm-package
110-
with:
111-
release-type: ${{ needs.set_release_type.outputs.RELEASE_TYPE }}
112-
gha-npm-token: ${{ env.GHA_NPM_TOKEN }}
113-
gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
78+
# The top-level `permissions: contents: read` is the ceiling for
79+
# GITHUB_TOKEN in every job here, including reusable-workflow calls.
80+
# Re-grant `id-token: write` at the job level so publish-npm.yml's
81+
# `publish-react-native` job can mint the OIDC token that npm
82+
# Trusted Publishing exchanges for a publish token.
83+
permissions:
84+
contents: read
85+
id-token: write
86+
uses: ./.github/workflows/publish-npm.yml
87+
secrets: inherit
88+
with:
89+
mode: react-native
90+
release-type: ${{ needs.set_release_type.outputs.RELEASE_TYPE }}

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

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,13 @@ on:
77
- "*-stable"
88

99
jobs:
10+
# Delegate to the shared reusable workflow so every `npm publish` in
11+
# this repo originates from one workflow file — required because npm
12+
# Trusted Publishing only accepts one (org, repo, workflow_filename)
13+
# per package.
1014
publish_bumped_packages:
11-
runs-on: ubuntu-latest
1215
if: github.repository == 'facebook/react-native'
13-
env:
14-
GHA_NPM_TOKEN: ${{ secrets.GHA_NPM_TOKEN }}
15-
steps:
16-
- name: Checkout
17-
uses: actions/checkout@v6
18-
- name: Setup node.js
19-
uses: ./.github/actions/setup-node
20-
- name: Run Yarn Install
21-
uses: ./.github/actions/yarn-install
22-
- name: Build packages
23-
run: yarn build
24-
- name: Build types
25-
run: yarn build-types --skip-snapshot
26-
- name: Set NPM auth token
27-
run: echo "//registry.npmjs.org/:_authToken=$GHA_NPM_TOKEN" > ~/.npmrc
28-
- name: Find and publish all bumped packages
29-
run: node ./scripts/releases-ci/publish-updated-packages.js
16+
uses: ./.github/workflows/publish-npm.yml
17+
secrets: inherit
18+
with:
19+
mode: monorepo-packages

.github/workflows/publish-npm.yml

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Reusable workflow that performs every `npm publish` in this repo.
2+
#
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.
12+
#
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)
16+
17+
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
41+
42+
jobs:
43+
publish-react-native:
44+
if: inputs.mode == 'react-native'
45+
runs-on: 8-core-ubuntu
46+
# `id-token: write` is required so the npm CLI can mint the OIDC
47+
# token that npm Trusted Publishing exchanges for a publish token.
48+
permissions:
49+
contents: read
50+
id-token: write
51+
container:
52+
image: reactnativecommunity/react-native-android:latest
53+
env:
54+
TERM: "dumb"
55+
# Set the encoding to resolve a known character encoding issue with decompressing tar.gz files in containers
56+
# via Gradle: https://github.com/gradle/gradle/issues/23391#issuecomment-1878979127
57+
LC_ALL: C.UTF8
58+
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
59+
# By default we only build ARM64 to save time/resources. For release/nightlies, we override this value to build all archs.
60+
ORG_GRADLE_PROJECT_reactNativeArchitectures: "arm64-v8a"
61+
REACT_NATIVE_DOWNLOADS_DIR: /opt/react-native-downloads
62+
env:
63+
ORG_GRADLE_PROJECT_SIGNING_PWD: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_PWD }}
64+
ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_KEY }}
65+
ORG_GRADLE_PROJECT_SONATYPE_USERNAME: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPE_USERNAME }}
66+
ORG_GRADLE_PROJECT_SONATYPE_PASSWORD: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPE_PASSWORD }}
67+
steps:
68+
- name: Checkout
69+
uses: actions/checkout@v6
70+
with:
71+
fetch-depth: 0
72+
fetch-tags: true
73+
- name: Build and Publish NPM Package
74+
uses: ./.github/actions/build-npm-package
75+
with:
76+
release-type: ${{ inputs.release-type }}
77+
gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
78+
skip-apple-prebuilts: ${{ inputs.skip-apple-prebuilts && 'true' || 'false' }}
79+
80+
publish-monorepo-packages:
81+
if: inputs.mode == 'monorepo-packages'
82+
runs-on: ubuntu-latest
83+
permissions:
84+
contents: read
85+
id-token: write
86+
steps:
87+
- name: Checkout
88+
uses: actions/checkout@v6
89+
- name: Setup node.js
90+
uses: ./.github/actions/setup-node
91+
with:
92+
registry-url: "https://registry.npmjs.org"
93+
- name: Run Yarn Install
94+
uses: ./.github/actions/yarn-install
95+
- name: Build packages
96+
run: yarn build
97+
- name: Build types
98+
run: yarn build-types --skip-snapshot
99+
- name: Find and publish all bumped packages
100+
run: node ./scripts/releases-ci/publish-updated-packages.js

.github/workflows/publish-release.yml

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,44 +43,34 @@ jobs:
4343
secrets: inherit
4444
needs: [prebuild_apple_dependencies]
4545

46+
# Delegate the actual npm publish to the shared reusable workflow so
47+
# every `npm publish` in this repo originates from one workflow file —
48+
# required because npm Trusted Publishing only accepts one
49+
# (org, repo, workflow_filename) per package.
4650
build_npm_package:
47-
runs-on: 8-core-ubuntu
4851
needs:
4952
[
5053
set_release_type,
5154
prebuild_apple_dependencies,
5255
prebuild_react_native_core,
5356
]
54-
container:
55-
image: reactnativecommunity/react-native-android:latest
56-
env:
57-
TERM: "dumb"
58-
# Set the encoding to resolve a known character encoding issue with decompressing tar.gz files in containers
59-
# via Gradle: https://github.com/gradle/gradle/issues/23391#issuecomment-1878979127
60-
LC_ALL: C.UTF8
61-
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
62-
# By default we only build ARM64 to save time/resources. For release/nightlies, we override this value to build all archs.
63-
ORG_GRADLE_PROJECT_reactNativeArchitectures: "arm64-v8a"
64-
REACT_NATIVE_DOWNLOADS_DIR: /opt/react-native-downloads
57+
uses: ./.github/workflows/publish-npm.yml
58+
secrets: inherit
59+
with:
60+
mode: react-native
61+
release-type: ${{ needs.set_release_type.outputs.RELEASE_TYPE }}
62+
63+
post_publish:
64+
runs-on: ubuntu-latest
65+
needs: [build_npm_package]
6566
env:
66-
GHA_NPM_TOKEN: ${{ secrets.GHA_NPM_TOKEN }}
67-
ORG_GRADLE_PROJECT_SIGNING_PWD: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_PWD }}
68-
ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_KEY }}
69-
ORG_GRADLE_PROJECT_SONATYPE_USERNAME: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPE_USERNAME }}
70-
ORG_GRADLE_PROJECT_SONATYPE_PASSWORD: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPE_PASSWORD }}
7167
REACT_NATIVE_BOT_GITHUB_TOKEN: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
7268
steps:
7369
- name: Checkout
7470
uses: actions/checkout@v6
7571
with:
7672
fetch-depth: 0
7773
fetch-tags: true
78-
- name: Build and Publish NPM Package
79-
uses: ./.github/actions/build-npm-package
80-
with:
81-
release-type: ${{ needs.set_release_type.outputs.RELEASE_TYPE }}
82-
gha-npm-token: ${{ env.GHA_NPM_TOKEN }}
83-
gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
8474
- name: Publish @react-native-community/template
8575
id: publish-template-to-npm
8676
uses: actions/github-script@v8

0 commit comments

Comments
 (0)