Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .ado/apple-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ stages:
dependsOn: []
jobs:
- template: /.ado/jobs/test-javascript.yml@self
- template: /.ado/jobs/npm-publish-dry-run.yml@self

# https://github.com/microsoft/react-native-macos/issues/2344
# The Verdaccio server consistently hangs on creation, which is required for the integration tests
Expand Down
10 changes: 0 additions & 10 deletions .ado/jobs/npm-publish-dry-run.yml

This file was deleted.

55 changes: 54 additions & 1 deletion .ado/jobs/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,57 @@ jobs:
targetPath: $(System.DefaultWorkingDirectory)
artifactName: github-npm-js-publish
steps:
- template: /.ado/templates/npm-publish-steps.yml@self
- checkout: self
clean: true
fetchFilter: blob:none
persistCredentials: true

- template: /.ado/templates/configure-git.yml@self

- script: |
PUBLISH_TAG=$(jq -r '.release.version.generatorOptions.currentVersionResolverMetadata.tag' nx.json)
echo "##vso[task.setvariable variable=publishTag]$PUBLISH_TAG"
echo "Using publish tag from nx.json: $PUBLISH_TAG"
displayName: Read publish tag from nx.json

- script: |
yarn install
displayName: Install npm dependencies

- script: |
node .ado/scripts/prepublish-check.mjs --verbose --skip-auth --tag $(publishTag)
displayName: Verify release config

- script: |
echo Target branch: $(System.PullRequest.TargetBranch)
yarn nx release --dry-run --verbose
displayName: Version and publish packages (dry run)
condition: and(succeeded(), ne(variables['publish_react_native_macos'], '1'))

# Disable Nightly publishing on the main branch
- ${{ if endsWith(variables['Build.SourceBranchName'], '-stable') }}:
- script: |
echo "//registry.npmjs.org/:_authToken=$(npmAuthToken)" > ~/.npmrc
node .ado/scripts/prepublish-check.mjs --verbose --tag $(publishTag)
displayName: Set and validate npm auth
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))

- script: |
git switch $(Build.SourceBranchName)
yarn nx release --skip-publish --verbose
env:
GITHUB_TOKEN: $(githubAuthToken)
displayName: Version Packages and Github Release
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))

- script: |
if [[ -f .rnm-publish ]]; then
yarn nx release publish --tag ${{ parameters['publishTag'] }} --excludeTaskDependencies
fi
displayName: Publish packages
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))

- script: |
rm -f ~/.npmrc
displayName: Remove npmrc if it exists
condition: always()
23 changes: 6 additions & 17 deletions .ado/scripts/npmAddUser.mjs
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
#!/usr/bin/env node
// @ts-check

import * as assert from "node:assert/strict";
import { exec } from "node:child_process";

/**
* @template T
* @param {T} arg
* @param {string} message
* @returns {asserts arg is NonNullable<T>}
*/
function assert(arg, message) {
if (!arg) {
throw new Error(message);
}
}

const { [2]: username, [3]: password, [4]: email, [5]: registry } = process.argv;
assert(username, "Please specify username");
assert(password, "Please specify password");
assert(email, "Please specify email");
assert.ok(username, "Please specify username");
assert.ok(password, "Please specify password");
assert.ok(email, "Please specify email");

const child = exec(`npm adduser${registry ? ` --registry ${registry}` : ""}`);
assert(child.stdout, "Missing stdout on child process");
assert.ok(child.stdout, "Missing stdout on child process");

child.stdout.on("data", d => {
assert(child.stdin, "Missing stdin on child process");
assert.ok(child.stdin, "Missing stdin on child process");

process.stdout.write(d);
process.stdout.write("\n");
Expand Down
160 changes: 126 additions & 34 deletions .ado/scripts/prepublish-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { spawnSync } from "node:child_process";
import * as fs from "node:fs";
import * as util from "node:util";

const ADO_PUBLISH_PIPELINE = ".ado/templates/npm-publish-steps.yml";
const NX_CONFIG_FILE = "nx.json";

const NPM_DEFEAULT_REGISTRY = "https://registry.npmjs.org/"
const NPM_TAG_NEXT = "next";
const NPM_TAG_NIGHTLY = "nightly";
const RNMACOS_LATEST = "react-native-macos@latest";
Expand All @@ -21,8 +21,18 @@ const RNMACOS_NEXT = "react-native-macos@next";
* };
* };
* }} NxConfig;
* @typedef {{ tag?: string; update?: boolean; verbose?: boolean; }} Options;
* @typedef {{ npmTag: string; prerelease?: string; isNewTag?: boolean; }} TagInfo;
* @typedef {{
* "mock-branch"?: string;
* "skip-auth"?: boolean;
* tag?: string;
* update?: boolean;
* verbose?: boolean;
* }} Options;
* @typedef {{
* npmTag: string;
* prerelease?: string;
* isNewTag?: boolean;
* }} TagInfo;
*/

/**
Expand Down Expand Up @@ -80,6 +90,38 @@ function loadNxConfig(configFile) {
return JSON.parse(nx);
}

function verifyNpmAuth(registry = NPM_DEFEAULT_REGISTRY) {
const npmErrorRegex = /npm error code (\w+)/;
const spawnOptions = {
stdio: /** @type {const} */ ("pipe"),
shell: true,
windowsVerbatimArguments: true,
};

const whoamiArgs = ["whoami", "--registry", registry];
const whoami = spawnSync("npm", whoamiArgs, spawnOptions);
if (whoami.status !== 0) {
const error = whoami.stderr.toString();
const m = error.match(npmErrorRegex);
switch (m && m[1]) {
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(error);
}
}

const tokenArgs = ["token", "list", "--registry", registry];
const token = spawnSync("npm", tokenArgs, spawnOptions);
if (token.status !== 0) {
const error = token.stderr.toString();
const m = error.match(npmErrorRegex);
throw new Error(m ? `Auth token for '${registry}' returned error code ${m[1]}` : error);
}
}

/**
* Returns a numerical value for a given version string.
* @param {string} version
Expand All @@ -91,15 +133,65 @@ function versionToNumber(version) {
}

/**
* Returns the currently checked out branch. Note that this function prefers
* predefined CI environment variables over local clone.
* Returns the target branch name. If not targetting any branches (e.g., when
* executing this script locally), `undefined` is returned.
* @returns {string | undefined}
*/
function getTargetBranch() {
// Azure Pipelines
if (process.env["TF_BUILD"] === "True") {
// https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services
const targetBranch = process.env["SYSTEM_PULLREQUEST_TARGETBRANCH"];
return targetBranch?.replace(/^refs\/heads\//, "");
}

// GitHub Actions
if (process.env["GITHUB_ACTIONS"] === "true") {
// https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
return process.env["GITHUB_BASE_REF"];
}

return undefined;
}

/**
* Returns the current branch name. In a pull request, the target branch name is
* returned.
* @param {Options} options
* @returns {string}
*/
function getCurrentBranch() {
// https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services
const adoSourceBranchName = process.env["BUILD_SOURCEBRANCHNAME"];
if (adoSourceBranchName) {
return adoSourceBranchName.replace(/^refs\/heads\//, "");
function getCurrentBranch(options) {
const targetBranch = getTargetBranch();
if (targetBranch) {
return targetBranch;
}

// Azure DevOps Pipelines
if (process.env["TF_BUILD"] === "True") {
// https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services
const sourceBranch = process.env["BUILD_SOURCEBRANCHNAME"];
if (sourceBranch) {
return sourceBranch.replace(/^refs\/heads\//, "");
}
}

// GitHub Actions
if (process.env["GITHUB_ACTIONS"] === "true") {
// https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
const headRef = process.env["GITHUB_HEAD_REF"];
if (headRef) {
return headRef; // For pull requests
}

const ref = process.env["GITHUB_REF"];
if (ref) {
return ref.replace(/^refs\/heads\//, ""); // For push events
}
}

const { "mock-branch": mockBranch } = options;
if (mockBranch) {
return mockBranch;
}

// Depending on how the repo was cloned, HEAD may not exist. We only use this
Expand Down Expand Up @@ -177,31 +269,15 @@ function getTagForStableBranch(branch, { tag }, log) {
return { npmTag: NPM_TAG_NEXT, prerelease: "rc" };
}

/**
* @param {string} file
* @param {string} tag
* @returns {void}
*/
function verifyPublishPipeline(file, tag) {
const data = fs.readFileSync(file, { encoding: "utf-8" });
const m = data.match(/publishTag: '(latest|next|nightly|v\d+\.\d+-stable)'/);
if (!m) {
throw new Error(`${file}: Could not find npm publish tag`);
}

if (m[1] !== tag) {
throw new Error(`${file}: 'publishTag' must be set to '${tag}'`);
}
}

/**
* Verifies the configuration and enables publishing on CI.
* @param {NxConfig} config
* @param {string} currentBranch
* @param {TagInfo} tag
* @param {Options} options
* @returns {asserts config is NxConfig["release"]}
*/
function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNewTag }) {
function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNewTag }, options) {
/** @type {string[]} */
const errors = [];

Expand Down Expand Up @@ -244,7 +320,7 @@ function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNe
generatorOptions.fallbackCurrentVersionResolver = "disk";
}
} else if (typeof generatorOptions.fallbackCurrentVersionResolver === "string") {
errors.push("'release.version.generatorOptions.fallbackCurrentVersionResolver' must be unset");
errors.push("'release.version.generatorOptions.fallbackCurrentVersionResolver' must be removed");
generatorOptions.fallbackCurrentVersionResolver = undefined;
}

Expand All @@ -253,16 +329,24 @@ function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNe
throw new Error("Nx Release is not correctly configured for the current branch");
}

verifyPublishPipeline(ADO_PUBLISH_PIPELINE, tag);
enablePublishingOnAzurePipelines();
if (options["skip-auth"]) {
info("Skipped npm auth validation");
} else {
verifyNpmAuth();
}

// Don't enable publishing in PRs
if (!getTargetBranch()) {
enablePublishingOnAzurePipelines();
}
}

/**
* @param {Options} options
* @returns {number}
*/
function main(options) {
const branch = getCurrentBranch();
const branch = getCurrentBranch(options);
if (!branch) {
error("Could not get current branch");
return 1;
Expand All @@ -273,10 +357,11 @@ function main(options) {
const config = loadNxConfig(NX_CONFIG_FILE);
try {
if (isMainBranch(branch)) {
enablePublishing(config, branch, { npmTag: NPM_TAG_NIGHTLY, prerelease: NPM_TAG_NIGHTLY });
const info = { npmTag: NPM_TAG_NIGHTLY, prerelease: NPM_TAG_NIGHTLY };
enablePublishing(config, branch, info, options);
} else if (isStableBranch(branch)) {
const tag = getTagForStableBranch(branch, options, logger);
enablePublishing(config, branch, tag);
enablePublishing(config, branch, tag, options);
}
} catch (e) {
if (options.update) {
Expand All @@ -296,6 +381,13 @@ function main(options) {
const { values } = util.parseArgs({
args: process.argv.slice(2),
options: {
"mock-branch": {
type: "string",
},
"skip-auth": {
type: "boolean",
default: false,
},
tag: {
type: "string",
default: NPM_TAG_NEXT,
Expand Down
4 changes: 2 additions & 2 deletions .ado/templates/apple-tools-setup.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
steps:
- task: NodeTool@0
- task: UseNode@1
inputs:
versionSpec: '23.x'
version: '23.x'

- script: |
brew bundle --file .ado/Brewfile
Expand Down
Loading