Skip to content

Commit b3a73ab

Browse files
authored
chore: use NPM trusted publishing (#3071)
Replaces the ADO-based npm publish pipeline with a GitHub Actions workflow that uses npm Trusted Publishing (OIDC) for the main publish step — no stored npm token required for `npm publish`. ## Changes - **`.github/workflows/microsoft-npm-publish.yml`** — New workflow triggered on `*-stable` branch pushes. Runs in an `npm-publish` GitHub environment (add protection rules there). Uses `id-token: write` + `--provenance` so yarn exchanges a GitHub OIDC token directly with npmjs.com rather than a stored secret. The `npm dist-tag add` step (applying additional dist-tags) still requires a `NPM_TOKEN` secret since OIDC doesn't cover that operation. - **`.ado/scripts/configure-publish.mts`** — Added `enablePublishingOnGitHubActions()` that writes `publish_react_native_macos=1` to `GITHUB_OUTPUT`, so the new workflow can gate its publish steps on this output. Previously only the ADO `##vso[task.setvariable...]` signal was emitted. - **`.ado/scripts/apply-additional-tags.mjs`** — Token can now come from `NODE_AUTH_TOKEN` env var as a fallback to `--token`. When using the env-var path (GHA), the token is not passed as a CLI argument — `actions/setup-node` has already wired `NODE_AUTH_TOKEN` into `.npmrc`, so `npm dist-tag add` picks it up from there. The `--token` CLI arg path (ADO) is unchanged. ## Manual steps required before this workflow runs 1. **npmjs.com**: add a Trusted Publisher for `react-native-macos` and `@react-native-macos/virtualized-lists` — repo `microsoft/react-native-macos`, workflow `microsoft-npm-publish.yml`, environment `npm-publish` 2. **GitHub repo Settings → Environments**: create the `npm-publish` environment with desired approval rules 3. **GitHub secret**: add `NPM_TOKEN` to that environment (granular automation token scoped to the two packages, used only for dist-tag operations) 4. **ADO**: once verified, disable `.ado/publish.yml` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent efb2eb5 commit b3a73ab

4 files changed

Lines changed: 89 additions & 49 deletions

File tree

.ado/jobs/npm-publish.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
displayName: Install npm dependencies
3535
3636
- script: |
37-
node .ado/scripts/configure-publish.mts --verbose --skip-auth
37+
node .ado/scripts/configure-publish.mts --verbose
3838
displayName: Verify release config
3939
4040
# Disable Nightly publishing on the main branch
@@ -52,9 +52,11 @@ jobs:
5252
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))
5353
5454
- script: |
55-
node .ado/scripts/apply-additional-tags.mjs --tags "$(additionalTags)" --token "$(npmAuthToken)"
55+
node .ado/scripts/apply-additional-tags.mjs --tags "$(additionalTags)"
5656
displayName: Apply additional dist-tags
5757
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))
58+
env:
59+
NODE_AUTH_TOKEN: $(npmAuthToken)
5860
5961
- script: |
6062
yarn config unset npmPublishAccess || true

.ado/scripts/apply-additional-tags.mjs

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import * as util from "node:util";
55

66
/**
77
* Apply additional dist-tags to published packages
8-
* Usage: node apply-additional-tags.mjs --tags <tags> --token <token>
8+
* Usage: node apply-additional-tags.mjs --tags <tags>
99
* node apply-additional-tags.mjs --tags <tags> --dry-run
1010
* Where tags is a comma-separated list of tags (e.g., "next,v0.79-stable")
11+
*
12+
* Auth token is read from the NODE_AUTH_TOKEN environment variable.
1113
*/
1214

1315
const registry = "https://registry.npmjs.org/";
@@ -19,7 +21,6 @@ const packages = [
1921
/**
2022
* @typedef {{
2123
* tags?: string;
22-
* token?: string;
2324
* "dry-run"?: boolean;
2425
* }} Options;
2526
*/
@@ -28,14 +29,18 @@ const packages = [
2829
* @param {Options} options
2930
* @returns {number}
3031
*/
31-
function main({ tags, token, "dry-run": dryRun }) {
32+
function main({ tags, "dry-run": dryRun }) {
3233
if (!tags) {
3334
console.log("No additional tags to apply");
3435
return 0;
3536
}
3637

38+
const token = process.env.NODE_AUTH_TOKEN;
39+
3740
if (!dryRun && !token) {
38-
console.error("Error: npm auth token is required (use --dry-run to preview)");
41+
console.error(
42+
"Error: NODE_AUTH_TOKEN is required (use --dry-run to preview)"
43+
);
3944
return 1;
4045
}
4146

@@ -58,17 +63,10 @@ function main({ tags, token, "dry-run": dryRun }) {
5863
for (const tag of tags.split(",")) {
5964
for (const pkg of packages) {
6065
console.log(`Adding dist-tag '${tag}' to ${pkg}@${version}`);
66+
6167
const result = spawnSync(
6268
"npm",
63-
[
64-
"dist-tag",
65-
"add",
66-
`${pkg}@${version}`,
67-
tag,
68-
"--registry",
69-
registry,
70-
`--//registry.npmjs.org/:_authToken=${token}`,
71-
],
69+
["dist-tag", "add", `${pkg}@${version}`, tag, "--registry", registry],
7270
{ stdio: "inherit", shell: true }
7371
);
7472

@@ -88,9 +86,6 @@ const { values } = util.parseArgs({
8886
tags: {
8987
type: "string",
9088
},
91-
token: {
92-
type: "string",
93-
},
9489
"dry-run": {
9590
type: "boolean",
9691
default: false,

.ado/scripts/configure-publish.mts

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import { $, argv, echo, fs } from 'zx';
33
import { resolve } from 'node:path';
44

5-
const NPM_DEFAULT_REGISTRY = 'https://registry.npmjs.org/';
5+
const isGitHubActions = process.env['GITHUB_ACTIONS'] === 'true';
6+
67
const NPM_TAG_NEXT = 'next';
78

89
export type ReleaseState = 'STABLE_IS_LATEST' | 'STABLE_IS_NEW' | 'STABLE_IS_OLD';
@@ -21,19 +22,20 @@ export interface TagInfo {
2122

2223
interface Options {
2324
'mock-branch'?: string;
24-
'skip-auth'?: boolean;
2525
tag?: string;
2626
verbose?: boolean;
2727
}
2828

29-
/**
30-
* Exports a variable, `publish_react_native_macos`, to signal that we want to
31-
* enable publishing on Azure Pipelines.
32-
*/
3329
function enablePublishingOnAzurePipelines() {
3430
echo(`##vso[task.setvariable variable=publish_react_native_macos]1`);
3531
}
3632

33+
function enablePublishingOnGitHubActions() {
34+
if (process.env['GITHUB_OUTPUT']) {
35+
fs.appendFileSync(process.env['GITHUB_OUTPUT'], `publish_react_native_macos=1\n`);
36+
}
37+
}
38+
3739
export function isMainBranch(branch: string): boolean {
3840
return branch === 'main';
3941
}
@@ -160,23 +162,6 @@ export function getPublishTags(
160162
}
161163
}
162164

163-
async function verifyNpmAuth(registry = NPM_DEFAULT_REGISTRY) {
164-
const whoami = await $`npm whoami --registry ${registry}`.nothrow();
165-
if (whoami.exitCode !== 0) {
166-
const errText = whoami.stderr;
167-
const m = errText.match(/npm error code (\w+)/);
168-
const errorCode = m && m[1];
169-
switch (errorCode) {
170-
case 'EINVALIDNPMTOKEN':
171-
throw new Error(`Invalid auth token for npm registry: ${registry}`);
172-
case 'ENEEDAUTH':
173-
throw new Error(`Missing auth token for npm registry: ${registry}`);
174-
default:
175-
throw new Error(errText);
176-
}
177-
}
178-
}
179-
180165
async function enablePublishing(tagInfo: TagInfo, options: Options) {
181166
const [primaryTag, ...additionalTags] = tagInfo.npmTags;
182167

@@ -195,15 +180,15 @@ async function enablePublishing(tagInfo: TagInfo, options: Options) {
195180
}
196181
}
197182

198-
if (options['skip-auth']) {
199-
echo('ℹ️ Skipped npm auth validation');
200-
} else {
201-
await verifyNpmAuth();
202-
}
203-
204183
// Don't enable publishing in PRs
205184
if (!getTargetBranch()) {
206-
enablePublishingOnAzurePipelines();
185+
if (isGitHubActions) {
186+
enablePublishingOnGitHubActions();
187+
} else if (process.env['TF_BUILD'] === 'True') {
188+
enablePublishingOnAzurePipelines();
189+
} else {
190+
echo('ℹ️ Local run — publishing not enabled');
191+
}
207192
}
208193
}
209194

@@ -215,7 +200,6 @@ if (isDirectRun) {
215200
// Parse CLI args using zx's argv (minimist)
216201
const options: Options = {
217202
'mock-branch': argv['mock-branch'] as string | undefined,
218-
'skip-auth': Boolean(argv['skip-auth']),
219203
tag: typeof argv['tag'] === 'string' ? argv['tag'] : NPM_TAG_NEXT,
220204
verbose: Boolean(argv['verbose']),
221205
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Publish to npm
2+
3+
on:
4+
push:
5+
branches:
6+
- "*-stable"
7+
8+
jobs:
9+
publish:
10+
name: Publish to npm
11+
runs-on: ubuntu-latest
12+
13+
if: github.repository == 'microsoft/react-native-macos'
14+
15+
# Matches the environment name registered as a Trusted Publisher on npmjs.com.
16+
environment: npm-publish
17+
18+
permissions:
19+
contents: read
20+
id-token: write
21+
22+
steps:
23+
- name: Checkout
24+
uses: actions/checkout@v4
25+
with:
26+
filter: blob:none
27+
fetch-depth: 0
28+
29+
- name: Setup toolchain
30+
uses: ./.github/actions/microsoft-setup-toolchain
31+
with:
32+
node-version: "22"
33+
34+
- name: Install dependencies
35+
run: yarn install --immutable
36+
37+
- name: Verify release config
38+
id: configure-publish
39+
run: node .ado/scripts/configure-publish.mts --verbose
40+
41+
- name: Configure yarn for npm publishing
42+
if: steps.configure-publish.outputs.publish_react_native_macos == '1'
43+
run: |
44+
yarn config set npmPublishAccess public
45+
yarn config set npmPublishRegistry "https://registry.npmjs.org"
46+
47+
- name: Publish packages
48+
if: steps.configure-publish.outputs.publish_react_native_macos == '1'
49+
run: |
50+
yarn workspaces foreach -vv --all --topological --no-private npm publish \
51+
--provenance \
52+
--tag "${{ steps.configure-publish.outputs.publishTag }}" \
53+
--tolerate-republish
54+
55+
- name: Remove npm auth configuration
56+
if: always()
57+
run: |
58+
yarn config unset npmPublishAccess || true
59+
yarn config unset npmPublishRegistry || true

0 commit comments

Comments
 (0)