diff --git a/.ado/publish.yml b/.ado/publish.yml index 51b034951a6..9caeaace8f3 100644 --- a/.ado/publish.yml +++ b/.ado/publish.yml @@ -211,53 +211,6 @@ extends: - script: dir /s "$(Pipeline.Workspace)\published-packages" displayName: Show created npm packages - # Beachball usually takes care about the NPM package tagging based on the values in package.json files. - # We use the ESRP Release where we must provide the tag explictly (the productstate parameter). - # Fortunately, we just use two tags: latest and some custom tag like "canary", "v0.73-stable", etc. - # The npmGroupByTag.js script groups the created NPM package by these two tags into the specified folders. - - pwsh: | - node .ado/scripts/npmGroupByTag.js "$(Pipeline.Workspace)\published-packages" "$(Pipeline.Workspace)\published-packages\custom-tag" "$(Pipeline.Workspace)\published-packages\latest-tag" - displayName: Group npm packages by tag - - - script: dir /s "$(Pipeline.Workspace)\published-packages" - displayName: Show grouped npm packages by tag - - # Publish NPM packages using ESRP Release task with the custom tag such as "canary", "v0.73-stable", etc. - - task: 'SFP.release-tasks.custom-build-release-task.EsrpRelease@10' - displayName: 'ESRP Release to npmjs.com (custom tag)' - condition: and(succeeded(), ${{ not(parameters.skipNpmPublish) }}, eq(variables['NpmCustomFolderHasContent'], 'true')) - inputs: - connectedservicename: 'ESRP-CodeSigning-OGX-JSHost-RNW' - usemanagedidentity: false - keyvaultname: 'OGX-JSHost-KV' - authcertname: 'OGX-JSHost-Auth4' - signcertname: 'OGX-JSHost-Sign3' - clientid: '0a35e01f-eadf-420a-a2bf-def002ba898d' - domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2' - contenttype: npm - folderlocation: '$(NpmCustomFolder)' - productstate: '$(NpmCustomTag)' - owners: 'vmorozov@microsoft.com' - approvers: 'khosany@microsoft.com' - - # Publish NPM packages using ESRP Release task with the "latest" tag. - - task: 'SFP.release-tasks.custom-build-release-task.EsrpRelease@10' - displayName: 'ESRP Release to npmjs.com (latest)' - condition: and(succeeded(), ${{ not(parameters.skipNpmPublish) }}, eq(variables['NpmLatestFolderHasContent'], 'true')) - inputs: - connectedservicename: 'ESRP-CodeSigning-OGX-JSHost-RNW' - usemanagedidentity: false - keyvaultname: 'OGX-JSHost-KV' - authcertname: 'OGX-JSHost-Auth4' - signcertname: 'OGX-JSHost-Sign3' - clientid: '0a35e01f-eadf-420a-a2bf-def002ba898d' - domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2' - contenttype: npm - folderlocation: '$(NpmLatestFolder)' - productstate: 'latest' - owners: 'vmorozov@microsoft.com' - approvers: 'khosany@microsoft.com' - # Beachball reverts to local state after publish, but we want the updates it added - script: git pull origin $(SourceBranchWithFolders) displayName: git pull @@ -274,6 +227,29 @@ extends: parameters: buildEnvironment: Continuous + - script: echo NpmDistTag is $(NpmDistTag) + displayName: Show NPM dist tag + + - script: dir /s "$(Pipeline.Workspace)\published-packages" + displayName: Show npm packages before ESRP release + + - task: 'SFP.release-tasks.custom-build-release-task.EsrpRelease@10' + displayName: 'ESRP Release to npmjs.com' + condition: and(succeeded(), ne(variables['NpmDistTag'], '')) + inputs: + connectedservicename: 'ESRP-CodeSigning-OGX-JSHost-RNW' + usemanagedidentity: false + keyvaultname: 'OGX-JSHost-KV' + authcertname: 'OGX-JSHost-Auth4' + signcertname: 'OGX-JSHost-Sign3' + clientid: '0a35e01f-eadf-420a-a2bf-def002ba898d' + domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2' + contenttype: npm + folderlocation: '$(Pipeline.Workspace)\published-packages' + productstate: '$(NpmDistTag)' + owners: 'vmorozov@microsoft.com' + approvers: 'khosany@microsoft.com' + - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 displayName: 📒 Generate Manifest Npm inputs: diff --git a/.ado/release.yml b/.ado/release.yml index 1ea74d65666..8aa9db3ec64 100644 --- a/.ado/release.yml +++ b/.ado/release.yml @@ -26,12 +26,11 @@ extends: customBuildTags: - ES365AIMigrationTooling-Release stages: - - stage: PushToPrivateAdoStage - displayName: ADO - react-native + - stage: Release + displayName: Publish artifacts jobs: - - job: PushPackages - displayName: Push packages - condition: succeeded() + - job: PushPrivateAdo + displayName: ADO - react-native timeoutInMinutes: 0 templateContext: inputs: @@ -52,12 +51,9 @@ extends: inputs: script: nuget.exe push *.nupkg -ApiKey AzureArtifacts -Source https://pkgs.dev.azure.com/ms/_packaging/react-native/nuget/v3/index.json -NonInteractive -Verbosity Detailed -SkipDuplicate -NoSymbols workingDirectory: $(Pipeline.Workspace)/ReactWindows-final-nuget - - stage: PushToPublicAdoStage - displayName: ADO - react-native-public - jobs: - - job: PushPackages - displayName: Push packages - condition: succeeded() + + - job: PushPublicAdo + displayName: ADO - react-native-public timeoutInMinutes: 0 templateContext: inputs: @@ -78,13 +74,11 @@ extends: inputs: script: nuget.exe push *.nupkg -ApiKey AzureArtifacts -Source https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/nuget/v3/index.json -NonInteractive -Verbosity Detailed -SkipDuplicate -NoSymbols workingDirectory: $(Pipeline.Workspace)/ReactWindows-final-nuget - - stage: PushToNuGetStage - displayName: nuget.org - Push nuget packages - variables: - - group: RNW Secrets - jobs: - - job: PushPackages - displayName: Push packages + + - job: PushNuGetOrg + displayName: nuget.org - Push nuget packages + variables: + - group: RNW Secrets timeoutInMinutes: 0 templateContext: inputs: diff --git a/.ado/scripts/npmGroupByTag.js b/.ado/scripts/npmGroupByTag.js deleted file mode 100644 index 23d6320e689..00000000000 --- a/.ado/scripts/npmGroupByTag.js +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/env node -// @ts-check - -// Groups packed npm tarballs into tag-specific folders so ESRP can publish with the -// correct productstate value per tag. - -const fs = require('fs'); -const path = require('path'); - -/** - * @typedef {Object} PackageJsonBeachball - * @property {string | undefined} [defaultNpmTag] - */ - -/** - * @typedef {Object} PackageJson - * @property {string | undefined} [name] - * @property {string | undefined} [version] - * @property {boolean | undefined} [private] - * @property {PackageJsonBeachball | undefined} [beachball] - */ - -/** - * @returns {{packRootArg: string, customRootArg: string, latestRootArg: string}} - */ -function ensureArgs() { - const [, , packRootArg, customRootArg, latestRootArg] = process.argv; - if (!packRootArg || !customRootArg || !latestRootArg) { - console.error('Usage: node npmGroupByTag.js '); - process.exit(1); - } - return {packRootArg, customRootArg, latestRootArg}; -} - -/** - * @param {string} filePath - * @returns {unknown} - */ -function readJson(filePath) { - return JSON.parse(fs.readFileSync(filePath, 'utf8')); -} - -/** - * @param {string} pkgName - * @param {string} version - * @returns {string} - */ -function sanitizedTarballName(pkgName, version) { - const prefix = pkgName.startsWith('@') - ? pkgName.slice(1).replace(/\//g, '-').replace(/@/g, '-') - : pkgName.replace(/@/g, '-'); - return `${prefix}-${version}.tgz`; -} - -/** - * @param {string} tarballName - * @returns {string} - */ -function normalizePackedTarballName(tarballName) { - // beachball prefixes packed tarballs with a monotonically increasing number to avoid collisions - // when multiple packages share the same filename. Strip that prefix (single or repeated) for comparison. - return tarballName.replace(/^(?:\d+[._-])+/u, ''); -} - -/** - * @param {string} root - * @returns {string[]} - */ -function findPackageJsons(root) { - /** @type {string[]} */ - const results = []; - /** @type {string[]} */ - const stack = [root]; - - while (stack.length) { - const current = stack.pop(); - if (!current) { - continue; - } - /** @type {fs.Stats | undefined} */ - let stats; - try { - stats = fs.statSync(current); - } catch (e) { - continue; - } - - if (!stats.isDirectory()) { - continue; - } - - const entries = fs.readdirSync(current, {withFileTypes: true}); - for (const entry of entries) { - if (entry.name === 'node_modules' || entry.name === '.git') { - continue; - } - const entryPath = path.join(current, entry.name); - if (entry.isDirectory()) { - stack.push(entryPath); - } else if (entry.isFile() && entry.name === 'package.json') { - results.push(entryPath); - } - } - } - - return results; -} - -/** - * @param {string} name - * @param {string} value - */ -function setPipelineVariable(name, value) { - console.log(`##vso[task.setvariable variable=${name}]${value}`); -} - -(function main() { - const {packRootArg, customRootArg, latestRootArg} = ensureArgs(); - - const repoRoot = process.env.BUILD_SOURCESDIRECTORY || process.cwd(); - const packRoot = path.resolve(packRootArg); - const customRoot = path.resolve(customRootArg); - const latestRoot = path.resolve(latestRootArg); - - fs.mkdirSync(customRoot, {recursive: true}); - fs.mkdirSync(latestRoot, {recursive: true}); - - /** @type {string | null} */ - let customTag = null; - try { - const vnextPackageJson = /** @type {PackageJson} */ ( - readJson(path.join(repoRoot, 'vnext', 'package.json')) - ); - const tagFromVnext = vnextPackageJson?.beachball?.defaultNpmTag; - if (tagFromVnext && tagFromVnext !== 'latest') { - customTag = tagFromVnext; - } - } catch (e) { - console.warn('Unable to read vnext/package.json to determine custom tag.'); - } - - /** @type {string[]} */ - const tarballs = fs.existsSync(packRoot) - ? fs.readdirSync(packRoot).filter(file => file.endsWith('.tgz')) - : []; - - if (!tarballs.length) { - setPipelineVariable('NpmCustomTag', customTag || ''); - setPipelineVariable('NpmCustomFolder', customRoot); - setPipelineVariable('NpmCustomFolderHasContent', 'false'); - setPipelineVariable('NpmLatestFolder', latestRoot); - setPipelineVariable('NpmLatestFolderHasContent', 'false'); - return; - } - - /** @type {Set} */ - const customTarballs = new Set(); - - if (customTag) { - for (const packageJsonPath of findPackageJsons(repoRoot)) { - /** @type {PackageJson | undefined} */ - let pkg; - try { - pkg = /** @type {PackageJson} */ (readJson(packageJsonPath)); - } catch (e) { - continue; - } - - if (!pkg?.name || !pkg?.version) { - continue; - } - - const pkgTag = pkg?.beachball?.defaultNpmTag; - if (pkgTag === customTag && pkg.private !== true) { - customTarballs.add(sanitizedTarballName(pkg.name, pkg.version)); - } - } - } - - let customCount = 0; - let latestCount = 0; - - for (const tarball of tarballs) { - const sourcePath = path.join(packRoot, tarball); - const normalizedName = normalizePackedTarballName(tarball); - const destinationRoot = customTag && customTarballs.has(normalizedName) ? customRoot : latestRoot; - const destinationPath = path.join(destinationRoot, tarball); - fs.mkdirSync(path.dirname(destinationPath), {recursive: true}); - fs.renameSync(sourcePath, destinationPath); - if (destinationRoot === customRoot) { - customCount++; - } else { - latestCount++; - } - } - - setPipelineVariable('NpmCustomTag', customTag || ''); - setPipelineVariable('NpmCustomFolder', customRoot); - setPipelineVariable('NpmCustomFolderHasContent', customCount ? 'true' : 'false'); - setPipelineVariable('NpmLatestFolder', latestRoot); - setPipelineVariable('NpmLatestFolderHasContent', latestCount ? 'true' : 'false'); -})(); diff --git a/.ado/scripts/setVersionEnvVars.js b/.ado/scripts/setVersionEnvVars.js index 0d026245cc9..63358f259a9 100644 --- a/.ado/scripts/setVersionEnvVars.js +++ b/.ado/scripts/setVersionEnvVars.js @@ -37,6 +37,11 @@ const versionEnvVars = { publishCommitId: commitId, reactDevDependency: pkgJson.devDependencies['react'], reactNativeDevDependency: pkgJson.devDependencies['react-native'], + npmDistTag: pkgJson?.beachball?.defaultNpmTag?.trim(), +} + +if (!versionEnvVars.npmDistTag) { + throw new Error('defaultNpmTag is missing in vnext/package.json'); } // Set the build number so the build in the publish pipeline and the release pipeline are named with the convenient version @@ -54,8 +59,14 @@ console.log(`##vso[task.setvariable variable=npmVersion]${versionEnvVars.npmVers console.log(`##vso[task.setvariable variable=publishCommitId]${versionEnvVars.publishCommitId}`); console.log(`##vso[task.setvariable variable=reactDevDependency]${versionEnvVars.reactDevDependency}`); console.log(`##vso[task.setvariable variable=reactNativeDevDependency]${versionEnvVars.reactNativeDevDependency}`); +console.log(`##vso[task.setvariable variable=NpmDistTag]${versionEnvVars.npmDistTag}`); + +const runnerTemp = process.env.RUNNER_TEMP; +if (!runnerTemp) { + throw new Error('RUNNER_TEMP environment variable is not set'); +} -const dirPath = path.resolve(process.env.RUNNER_TEMP, 'versionEnvVars'); +const dirPath = path.resolve(runnerTemp, 'versionEnvVars'); fs.mkdirSync(dirPath, {recursive: true}); fs.writeFileSync(path.resolve(dirPath, 'versionEnvVars.js'), @@ -68,4 +79,5 @@ console.log("##vso[task.setvariable variable=npmVersion]${versionEnvVars.npmVers console.log("##vso[task.setvariable variable=publishCommitId]${versionEnvVars.publishCommitId}"); console.log("##vso[task.setvariable variable=reactDevDependency]${versionEnvVars.reactDevDependency}"); console.log("##vso[task.setvariable variable=reactNativeDevDependency]${versionEnvVars.reactNativeDevDependency}"); +console.log("##vso[task.setvariable variable=NpmDistTag]${versionEnvVars.npmDistTag}"); `);