@@ -3,9 +3,9 @@ import { spawnSync } from "node:child_process";
33import * as fs from "node:fs" ;
44import * as util from "node:util" ;
55
6- const ADO_PUBLISH_PIPELINE = ".ado/templates/npm-publish-steps.yml" ;
76const NX_CONFIG_FILE = "nx.json" ;
87
8+ const NPM_DEFEAULT_REGISTRY = "https://registry.npmjs.org/"
99const NPM_TAG_NEXT = "next" ;
1010const NPM_TAG_NIGHTLY = "nightly" ;
1111const 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 = / n p m e r r o r c o d e ( \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 ( / ^ r e f s \/ h e a d s \/ / , "" ) ;
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 ( / ^ r e f s \/ h e a d s \/ / , "" ) ;
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 ( / ^ r e f s \/ h e a d s \/ / , "" ) ;
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 ( / ^ r e f s \/ h e a d s \/ / , "" ) ; // 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 ( / p u b l i s h T a g : ' ( l a t e s t | n e x t | n i g h t l y | v \d + \. \d + - s t a b l e ) ' / ) ;
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 */
264348function 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) {
296381const { 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 ,
0 commit comments