Skip to content

Commit a6cfc75

Browse files
Saadnajmitido64
andauthored
ci(0.78): move "NPM Publish (Dry Run)" to Github Actions (#2533)
- Backport of #2532 - Backport of #2436 (the former change depends on it) - Adds a version plan to publish a new release --------- Co-authored-by: Tommy Nguyen <[email protected]>
1 parent b19f02d commit a6cfc75

File tree

10 files changed

+260
-116
lines changed

10 files changed

+260
-116
lines changed

.ado/apple-pr.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ stages:
2626
dependsOn: []
2727
jobs:
2828
- template: /.ado/jobs/test-javascript.yml@self
29-
- template: /.ado/jobs/npm-publish-dry-run.yml@self
3029

3130
# https://github.com/microsoft/react-native-macos/issues/2344
3231
# The Verdaccio server consistently hangs on creation, which is required for the integration tests

.ado/jobs/npm-publish-dry-run.yml

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

.ado/jobs/npm-publish.yml

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,57 @@ jobs:
1616
targetPath: $(System.DefaultWorkingDirectory)
1717
artifactName: github-npm-js-publish
1818
steps:
19-
- template: /.ado/templates/npm-publish-steps.yml@self
19+
- checkout: self
20+
clean: true
21+
fetchFilter: blob:none
22+
persistCredentials: true
23+
24+
- template: /.ado/templates/configure-git.yml@self
25+
26+
- script: |
27+
PUBLISH_TAG=$(jq -r '.release.version.generatorOptions.currentVersionResolverMetadata.tag' nx.json)
28+
echo "##vso[task.setvariable variable=publishTag]$PUBLISH_TAG"
29+
echo "Using publish tag from nx.json: $PUBLISH_TAG"
30+
displayName: Read publish tag from nx.json
31+
32+
- script: |
33+
yarn install
34+
displayName: Install npm dependencies
35+
36+
- script: |
37+
node .ado/scripts/prepublish-check.mjs --verbose --skip-auth --tag $(publishTag)
38+
displayName: Verify release config
39+
40+
- script: |
41+
echo Target branch: $(System.PullRequest.TargetBranch)
42+
yarn nx release --dry-run --verbose
43+
displayName: Version and publish packages (dry run)
44+
condition: and(succeeded(), ne(variables['publish_react_native_macos'], '1'))
45+
46+
# Disable Nightly publishing on the main branch
47+
- ${{ if endsWith(variables['Build.SourceBranchName'], '-stable') }}:
48+
- script: |
49+
echo "//registry.npmjs.org/:_authToken=$(npmAuthToken)" > ~/.npmrc
50+
node .ado/scripts/prepublish-check.mjs --verbose --tag $(publishTag)
51+
displayName: Set and validate npm auth
52+
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))
53+
54+
- script: |
55+
git switch $(Build.SourceBranchName)
56+
yarn nx release --skip-publish --verbose
57+
env:
58+
GITHUB_TOKEN: $(githubAuthToken)
59+
displayName: Version Packages and Github Release
60+
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))
61+
62+
- script: |
63+
if [[ -f .rnm-publish ]]; then
64+
yarn nx release publish --tag ${{ parameters['publishTag'] }} --excludeTaskDependencies
65+
fi
66+
displayName: Publish packages
67+
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))
68+
69+
- script: |
70+
rm -f ~/.npmrc
71+
displayName: Remove npmrc if it exists
72+
condition: always()

.ado/scripts/npmAddUser.mjs

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,19 @@
11
#!/usr/bin/env node
22
// @ts-check
33

4+
import * as assert from "node:assert/strict";
45
import { exec } from "node:child_process";
56

6-
/**
7-
* @template T
8-
* @param {T} arg
9-
* @param {string} message
10-
* @returns {asserts arg is NonNullable<T>}
11-
*/
12-
function assert(arg, message) {
13-
if (!arg) {
14-
throw new Error(message);
15-
}
16-
}
17-
187
const { [2]: username, [3]: password, [4]: email, [5]: registry } = process.argv;
19-
assert(username, "Please specify username");
20-
assert(password, "Please specify password");
21-
assert(email, "Please specify email");
8+
assert.ok(username, "Please specify username");
9+
assert.ok(password, "Please specify password");
10+
assert.ok(email, "Please specify email");
2211

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

2615
child.stdout.on("data", d => {
27-
assert(child.stdin, "Missing stdin on child process");
16+
assert.ok(child.stdin, "Missing stdin on child process");
2817

2918
process.stdout.write(d);
3019
process.stdout.write("\n");

.ado/scripts/prepublish-check.mjs

Lines changed: 126 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { spawnSync } from "node:child_process";
33
import * as fs from "node:fs";
44
import * as util from "node:util";
55

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

8+
const NPM_DEFEAULT_REGISTRY = "https://registry.npmjs.org/"
99
const NPM_TAG_NEXT = "next";
1010
const NPM_TAG_NIGHTLY = "nightly";
1111
const RNMACOS_LATEST = "react-native-macos@latest";
@@ -21,8 +21,18 @@ const RNMACOS_NEXT = "react-native-macos@next";
2121
* };
2222
* };
2323
* }} NxConfig;
24-
* @typedef {{ tag?: string; update?: boolean; verbose?: boolean; }} Options;
25-
* @typedef {{ npmTag: string; prerelease?: string; isNewTag?: boolean; }} TagInfo;
24+
* @typedef {{
25+
* "mock-branch"?: string;
26+
* "skip-auth"?: boolean;
27+
* tag?: string;
28+
* update?: boolean;
29+
* verbose?: boolean;
30+
* }} Options;
31+
* @typedef {{
32+
* npmTag: string;
33+
* prerelease?: string;
34+
* isNewTag?: boolean;
35+
* }} TagInfo;
2636
*/
2737

2838
/**
@@ -80,6 +90,38 @@ function loadNxConfig(configFile) {
8090
return JSON.parse(nx);
8191
}
8292

93+
function verifyNpmAuth(registry = NPM_DEFEAULT_REGISTRY) {
94+
const npmErrorRegex = /npm error code (\w+)/;
95+
const spawnOptions = {
96+
stdio: /** @type {const} */ ("pipe"),
97+
shell: true,
98+
windowsVerbatimArguments: true,
99+
};
100+
101+
const whoamiArgs = ["whoami", "--registry", registry];
102+
const whoami = spawnSync("npm", whoamiArgs, spawnOptions);
103+
if (whoami.status !== 0) {
104+
const error = whoami.stderr.toString();
105+
const m = error.match(npmErrorRegex);
106+
switch (m && m[1]) {
107+
case "EINVALIDNPMTOKEN":
108+
throw new Error(`Invalid auth token for npm registry: ${registry}`);
109+
case "ENEEDAUTH":
110+
throw new Error(`Missing auth token for npm registry: ${registry}`);
111+
default:
112+
throw new Error(error);
113+
}
114+
}
115+
116+
const tokenArgs = ["token", "list", "--registry", registry];
117+
const token = spawnSync("npm", tokenArgs, spawnOptions);
118+
if (token.status !== 0) {
119+
const error = token.stderr.toString();
120+
const m = error.match(npmErrorRegex);
121+
throw new Error(m ? `Auth token for '${registry}' returned error code ${m[1]}` : error);
122+
}
123+
}
124+
83125
/**
84126
* Returns a numerical value for a given version string.
85127
* @param {string} version
@@ -91,15 +133,65 @@ function versionToNumber(version) {
91133
}
92134

93135
/**
94-
* Returns the currently checked out branch. Note that this function prefers
95-
* predefined CI environment variables over local clone.
136+
* Returns the target branch name. If not targetting any branches (e.g., when
137+
* executing this script locally), `undefined` is returned.
138+
* @returns {string | undefined}
139+
*/
140+
function getTargetBranch() {
141+
// Azure Pipelines
142+
if (process.env["TF_BUILD"] === "True") {
143+
// https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services
144+
const targetBranch = process.env["SYSTEM_PULLREQUEST_TARGETBRANCH"];
145+
return targetBranch?.replace(/^refs\/heads\//, "");
146+
}
147+
148+
// GitHub Actions
149+
if (process.env["GITHUB_ACTIONS"] === "true") {
150+
// https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
151+
return process.env["GITHUB_BASE_REF"];
152+
}
153+
154+
return undefined;
155+
}
156+
157+
/**
158+
* Returns the current branch name. In a pull request, the target branch name is
159+
* returned.
160+
* @param {Options} options
96161
* @returns {string}
97162
*/
98-
function getCurrentBranch() {
99-
// https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services
100-
const adoSourceBranchName = process.env["BUILD_SOURCEBRANCHNAME"];
101-
if (adoSourceBranchName) {
102-
return adoSourceBranchName.replace(/^refs\/heads\//, "");
163+
function getCurrentBranch(options) {
164+
const targetBranch = getTargetBranch();
165+
if (targetBranch) {
166+
return targetBranch;
167+
}
168+
169+
// Azure DevOps Pipelines
170+
if (process.env["TF_BUILD"] === "True") {
171+
// https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services
172+
const sourceBranch = process.env["BUILD_SOURCEBRANCHNAME"];
173+
if (sourceBranch) {
174+
return sourceBranch.replace(/^refs\/heads\//, "");
175+
}
176+
}
177+
178+
// GitHub Actions
179+
if (process.env["GITHUB_ACTIONS"] === "true") {
180+
// https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
181+
const headRef = process.env["GITHUB_HEAD_REF"];
182+
if (headRef) {
183+
return headRef; // For pull requests
184+
}
185+
186+
const ref = process.env["GITHUB_REF"];
187+
if (ref) {
188+
return ref.replace(/^refs\/heads\//, ""); // For push events
189+
}
190+
}
191+
192+
const { "mock-branch": mockBranch } = options;
193+
if (mockBranch) {
194+
return mockBranch;
103195
}
104196

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

180-
/**
181-
* @param {string} file
182-
* @param {string} tag
183-
* @returns {void}
184-
*/
185-
function verifyPublishPipeline(file, tag) {
186-
const data = fs.readFileSync(file, { encoding: "utf-8" });
187-
const m = data.match(/publishTag: '(latest|next|nightly|v\d+\.\d+-stable)'/);
188-
if (!m) {
189-
throw new Error(`${file}: Could not find npm publish tag`);
190-
}
191-
192-
if (m[1] !== tag) {
193-
throw new Error(`${file}: 'publishTag' must be set to '${tag}'`);
194-
}
195-
}
196-
197272
/**
198273
* Verifies the configuration and enables publishing on CI.
199274
* @param {NxConfig} config
200275
* @param {string} currentBranch
201276
* @param {TagInfo} tag
277+
* @param {Options} options
202278
* @returns {asserts config is NxConfig["release"]}
203279
*/
204-
function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNewTag }) {
280+
function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNewTag }, options) {
205281
/** @type {string[]} */
206282
const errors = [];
207283

@@ -244,7 +320,7 @@ function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNe
244320
generatorOptions.fallbackCurrentVersionResolver = "disk";
245321
}
246322
} else if (typeof generatorOptions.fallbackCurrentVersionResolver === "string") {
247-
errors.push("'release.version.generatorOptions.fallbackCurrentVersionResolver' must be unset");
323+
errors.push("'release.version.generatorOptions.fallbackCurrentVersionResolver' must be removed");
248324
generatorOptions.fallbackCurrentVersionResolver = undefined;
249325
}
250326

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

256-
verifyPublishPipeline(ADO_PUBLISH_PIPELINE, tag);
257-
enablePublishingOnAzurePipelines();
332+
if (options["skip-auth"]) {
333+
info("Skipped npm auth validation");
334+
} else {
335+
verifyNpmAuth();
336+
}
337+
338+
// Don't enable publishing in PRs
339+
if (!getTargetBranch()) {
340+
enablePublishingOnAzurePipelines();
341+
}
258342
}
259343

260344
/**
261345
* @param {Options} options
262346
* @returns {number}
263347
*/
264348
function main(options) {
265-
const branch = getCurrentBranch();
349+
const branch = getCurrentBranch(options);
266350
if (!branch) {
267351
error("Could not get current branch");
268352
return 1;
@@ -273,10 +357,11 @@ function main(options) {
273357
const config = loadNxConfig(NX_CONFIG_FILE);
274358
try {
275359
if (isMainBranch(branch)) {
276-
enablePublishing(config, branch, { npmTag: NPM_TAG_NIGHTLY, prerelease: NPM_TAG_NIGHTLY });
360+
const info = { npmTag: NPM_TAG_NIGHTLY, prerelease: NPM_TAG_NIGHTLY };
361+
enablePublishing(config, branch, info, options);
277362
} else if (isStableBranch(branch)) {
278363
const tag = getTagForStableBranch(branch, options, logger);
279-
enablePublishing(config, branch, tag);
364+
enablePublishing(config, branch, tag, options);
280365
}
281366
} catch (e) {
282367
if (options.update) {
@@ -296,6 +381,13 @@ function main(options) {
296381
const { values } = util.parseArgs({
297382
args: process.argv.slice(2),
298383
options: {
384+
"mock-branch": {
385+
type: "string",
386+
},
387+
"skip-auth": {
388+
type: "boolean",
389+
default: false,
390+
},
299391
tag: {
300392
type: "string",
301393
default: NPM_TAG_NEXT,

.ado/templates/apple-tools-setup.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
steps:
2-
- task: NodeTool@0
2+
- task: UseNode@1
33
inputs:
4-
versionSpec: '23.x'
4+
version: '23.x'
55

66
- script: |
77
brew bundle --file .ado/Brewfile

0 commit comments

Comments
 (0)