diff --git a/.ado/jobs/npm-publish.yml b/.ado/jobs/npm-publish.yml index 7da6c6029a76..0fa3d7196c68 100644 --- a/.ado/jobs/npm-publish.yml +++ b/.ado/jobs/npm-publish.yml @@ -34,7 +34,7 @@ jobs: displayName: Install npm dependencies - script: | - node .ado/scripts/configure-publish.mts --verbose --skip-auth + node .ado/scripts/configure-publish.mts --verbose displayName: Verify release config # Disable Nightly publishing on the main branch @@ -52,9 +52,11 @@ jobs: condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1')) - script: | - node .ado/scripts/apply-additional-tags.mjs --tags "$(additionalTags)" --token "$(npmAuthToken)" + node .ado/scripts/apply-additional-tags.mjs --tags "$(additionalTags)" displayName: Apply additional dist-tags condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1')) + env: + NODE_AUTH_TOKEN: $(npmAuthToken) - script: | yarn config unset npmPublishAccess || true diff --git a/.ado/scripts/apply-additional-tags.mjs b/.ado/scripts/apply-additional-tags.mjs index 73bb0f35829f..eebeda1bafb5 100644 --- a/.ado/scripts/apply-additional-tags.mjs +++ b/.ado/scripts/apply-additional-tags.mjs @@ -5,9 +5,11 @@ import * as util from "node:util"; /** * Apply additional dist-tags to published packages - * Usage: node apply-additional-tags.mjs --tags --token + * Usage: node apply-additional-tags.mjs --tags * node apply-additional-tags.mjs --tags --dry-run * Where tags is a comma-separated list of tags (e.g., "next,v0.79-stable") + * + * Auth token is read from the NODE_AUTH_TOKEN environment variable. */ const registry = "https://registry.npmjs.org/"; @@ -19,7 +21,6 @@ const packages = [ /** * @typedef {{ * tags?: string; - * token?: string; * "dry-run"?: boolean; * }} Options; */ @@ -28,14 +29,18 @@ const packages = [ * @param {Options} options * @returns {number} */ -function main({ tags, token, "dry-run": dryRun }) { +function main({ tags, "dry-run": dryRun }) { if (!tags) { console.log("No additional tags to apply"); return 0; } + const token = process.env.NODE_AUTH_TOKEN; + if (!dryRun && !token) { - console.error("Error: npm auth token is required (use --dry-run to preview)"); + console.error( + "Error: NODE_AUTH_TOKEN is required (use --dry-run to preview)" + ); return 1; } @@ -58,17 +63,10 @@ function main({ tags, token, "dry-run": dryRun }) { for (const tag of tags.split(",")) { for (const pkg of packages) { console.log(`Adding dist-tag '${tag}' to ${pkg}@${version}`); + const result = spawnSync( "npm", - [ - "dist-tag", - "add", - `${pkg}@${version}`, - tag, - "--registry", - registry, - `--//registry.npmjs.org/:_authToken=${token}`, - ], + ["dist-tag", "add", `${pkg}@${version}`, tag, "--registry", registry], { stdio: "inherit", shell: true } ); @@ -88,9 +86,6 @@ const { values } = util.parseArgs({ tags: { type: "string", }, - token: { - type: "string", - }, "dry-run": { type: "boolean", default: false, diff --git a/.ado/scripts/configure-publish.mts b/.ado/scripts/configure-publish.mts index 0d080975bbcb..31b336441f56 100644 --- a/.ado/scripts/configure-publish.mts +++ b/.ado/scripts/configure-publish.mts @@ -2,7 +2,8 @@ import { $, argv, echo, fs } from 'zx'; import { resolve } from 'node:path'; -const NPM_DEFAULT_REGISTRY = 'https://registry.npmjs.org/'; +const isGitHubActions = process.env['GITHUB_ACTIONS'] === 'true'; + const NPM_TAG_NEXT = 'next'; export type ReleaseState = 'STABLE_IS_LATEST' | 'STABLE_IS_NEW' | 'STABLE_IS_OLD'; @@ -21,19 +22,20 @@ export interface TagInfo { interface Options { 'mock-branch'?: string; - 'skip-auth'?: boolean; tag?: string; verbose?: boolean; } -/** - * Exports a variable, `publish_react_native_macos`, to signal that we want to - * enable publishing on Azure Pipelines. - */ function enablePublishingOnAzurePipelines() { echo(`##vso[task.setvariable variable=publish_react_native_macos]1`); } +function enablePublishingOnGitHubActions() { + if (process.env['GITHUB_OUTPUT']) { + fs.appendFileSync(process.env['GITHUB_OUTPUT'], `publish_react_native_macos=1\n`); + } +} + export function isMainBranch(branch: string): boolean { return branch === 'main'; } @@ -160,23 +162,6 @@ export function getPublishTags( } } -async function verifyNpmAuth(registry = NPM_DEFAULT_REGISTRY) { - const whoami = await $`npm whoami --registry ${registry}`.nothrow(); - if (whoami.exitCode !== 0) { - const errText = whoami.stderr; - const m = errText.match(/npm error code (\w+)/); - const errorCode = m && m[1]; - switch (errorCode) { - case 'EINVALIDNPMTOKEN': - throw new Error(`Invalid auth token for npm registry: ${registry}`); - case 'ENEEDAUTH': - throw new Error(`Missing auth token for npm registry: ${registry}`); - default: - throw new Error(errText); - } - } -} - async function enablePublishing(tagInfo: TagInfo, options: Options) { const [primaryTag, ...additionalTags] = tagInfo.npmTags; @@ -195,15 +180,15 @@ async function enablePublishing(tagInfo: TagInfo, options: Options) { } } - if (options['skip-auth']) { - echo('ℹ️ Skipped npm auth validation'); - } else { - await verifyNpmAuth(); - } - // Don't enable publishing in PRs if (!getTargetBranch()) { - enablePublishingOnAzurePipelines(); + if (isGitHubActions) { + enablePublishingOnGitHubActions(); + } else if (process.env['TF_BUILD'] === 'True') { + enablePublishingOnAzurePipelines(); + } else { + echo('ℹ️ Local run — publishing not enabled'); + } } } @@ -215,7 +200,6 @@ if (isDirectRun) { // Parse CLI args using zx's argv (minimist) const options: Options = { 'mock-branch': argv['mock-branch'] as string | undefined, - 'skip-auth': Boolean(argv['skip-auth']), tag: typeof argv['tag'] === 'string' ? argv['tag'] : NPM_TAG_NEXT, verbose: Boolean(argv['verbose']), }; diff --git a/.github/workflows/microsoft-npm-publish.yml b/.github/workflows/microsoft-npm-publish.yml new file mode 100644 index 000000000000..3f196f0a8516 --- /dev/null +++ b/.github/workflows/microsoft-npm-publish.yml @@ -0,0 +1,59 @@ +name: Publish to npm + +on: + push: + branches: + - "*-stable" + +jobs: + publish: + name: Publish to npm + runs-on: ubuntu-latest + + if: github.repository == 'microsoft/react-native-macos' + + # Matches the environment name registered as a Trusted Publisher on npmjs.com. + environment: npm-publish + + permissions: + contents: read + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + filter: blob:none + fetch-depth: 0 + + - name: Setup toolchain + uses: ./.github/actions/microsoft-setup-toolchain + with: + node-version: "22" + + - name: Install dependencies + run: yarn install --immutable + + - name: Verify release config + id: configure-publish + run: node .ado/scripts/configure-publish.mts --verbose + + - name: Configure yarn for npm publishing + if: steps.configure-publish.outputs.publish_react_native_macos == '1' + run: | + yarn config set npmPublishAccess public + yarn config set npmPublishRegistry "https://registry.npmjs.org" + + - name: Publish packages + if: steps.configure-publish.outputs.publish_react_native_macos == '1' + run: | + yarn workspaces foreach -vv --all --topological --no-private npm publish \ + --provenance \ + --tag "${{ steps.configure-publish.outputs.publishTag }}" \ + --tolerate-republish + + - name: Remove npm auth configuration + if: always() + run: | + yarn config unset npmPublishAccess || true + yarn config unset npmPublishRegistry || true