Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(0.76): set up proper publishing pipeline #2308

Merged
merged 5 commits into from
Dec 4, 2024
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
4 changes: 1 addition & 3 deletions .ado/jobs/npm-publish-dry-run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,4 @@ jobs:
submodules: recursive # set to 'true' for a single level of submodules or 'recursive' to get submodules of submodules
persistCredentials: true # set to 'true' to leave the OAuth token in the Git config after the initial fetch

- template: /.ado/templates/apple-steps-publish.yml@self
parameters:
build_type: 'dry-run'
- template: /.ado/templates/npm-publish.yml@self
44 changes: 1 addition & 43 deletions .ado/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ variables:
- group: InfoSec-SecurityResults
- name: tags
value: production,externalfacing
# Remember to update this in previous stable branches when creating a new stable branch
- name : latestStableBranch
value: '0.76-stable'

resources:
repositories:
Expand Down Expand Up @@ -76,46 +73,7 @@ extends:
submodules: recursive # set to 'true' for a single level of submodules or 'recursive' to get submodules of submodules
persistCredentials: true # set to 'true' to leave the OAuth token in the Git config after the initial fetch

# Setup the repo to be ready for release. This includes:
# - Autogenerating the next version number
# - Calling the approprate scripts that upstream React Native uses to prepare a release
# - Skipping the actual `git tag`, `git push`, and `npm publish steps as we do that here instead

- ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}:
- template: .ado/templates/apple-steps-publish.yml@self
parameters:
build_type: nightly
- ${{ elseif endsWith(variables['Build.SourceBranchName'], '-stable') }}:
- template: .ado/templates/apple-steps-publish.yml@self
parameters:
build_type: release
- ${{ else }}:
- task: CmdLine@2
displayName: Unknown branch, skipping publish
inputs:
script: |
echo "Skipping publish for branch $(Build.SourceBranchName)"
exit 1
# Set the NPM dist-tag and do the actual NPM publish

- bash: echo "##vso[task.setvariable variable=npmDistTag]latest"
displayName: Set dist-tag to latest
condition: eq(variables['Build.SourceBranchName'], variables.latestStableBranch)

- bash: echo "##vso[task.setvariable variable=npmDistTag]canary"
displayName: Set dist-tag to canary
condition: eq(variables['Build.SourceBranchName'], 'main')

- bash: echo "##vso[task.setvariable variable=npmDistTag]v${{variables['Build.SourceBranchName']}}"
displayName: Set dist-tag to v0.x-stable
condition: and(ne(variables['Build.SourceBranchName'], 'main'), ne(variables['Build.SourceBranchName'], variables.latestStableBranch))

- task: CmdLine@2
displayName: Actual NPM Publish
inputs:
script: |
npm publish ./packages/react-native --tag $(npmDistTag) --registry https://registry.npmjs.org/ --//registry.npmjs.org/:_authToken=$(npmAuthToken)
- template: /.ado/templates/npm-publish.yml@self

# Set the git tag and push the version update back to Github

Expand Down
262 changes: 262 additions & 0 deletions .ado/scripts/prepublish-check.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
// @ts-check
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.yml";
const NX_CONFIG_FILE = "nx.json";

const NPM_TAG_NEXT = "next";
const NPM_TAG_NIGHTLY = "nightly";

/**
* @typedef {typeof import("../../nx.json")} NxConfig
* @typedef {{ tag?: string; update?: boolean; }} Options
*/

/**
* Exports a variable, `publish_react_native_macos`, to signal that we want to
* enable publishing on Azure Pipelines.
*
* Note that pipelines need to read this variable separately and do the actual
* work to publish bits.
*/
function enablePublishingOnAzurePipelines() {
console.log(`##vso[task.setvariable variable=publish_react_native_macos]1`);
}

/**
* Logs an error message to the console.
* @param {string} message
*/
function error(message) {
console.error("❌", message);
}

/**
* Returns whether the given branch is considered main branch.
* @param {string} branch
*/
function isMainBranch(branch) {
// There is currently no good way to consistently get the main branch. We
// hardcode the value for now.
return branch === "main";
}

/**
* Returns whether the given branch is considered a stable branch.
* @param {string} branch
*/
function isStableBranch(branch) {
return /^\d+\.\d+-stable$/.test(branch);
}

/**
* Loads Nx configuration.
* @param {string} configFile
* @returns {NxConfig}
*/
function loadNxConfig(configFile) {
const nx = fs.readFileSync(configFile, { encoding: "utf-8" });
return JSON.parse(nx);
}

/**
* Returns a numerical value for a given version string.
* @param {string} version
* @returns {number}
*/
function versionToNumber(version) {
const [major, minor] = version.split("-")[0].split(".");
return Number(major) * 1000 + Number(minor);
}

/**
* Returns the currently checked out branch. Note that this function prefers
* predefined CI environment variables over local clone.
* @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\//, "");
}

// Depending on how the repo was cloned, HEAD may not exist. We only use this
// method as fallback.
const { stdout } = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
return stdout.toString().trim();
}

/**
* Returns the latest published version of `react-native-macos` from npm.
* @returns {number}
*/
function getLatestVersion() {
const { stdout } = spawnSync("npm", ["view", "react-native-macos@latest", "version"]);
return versionToNumber(stdout.toString().trim());
}

/**
* Returns the npm tag and prerelease identifier for the specified branch.
*
* @privateRemarks
* Note that the current implementation treats minor versions as major. If
* upstream ever decides to change the versioning scheme, we will need to make
* changes accordingly.
*
* @param {string} branch
* @param {Options} options
* @returns {{ npmTag: string; prerelease?: string; }}
*/
function getTagForStableBranch(branch, { tag }) {
if (!isStableBranch(branch)) {
throw new Error("Expected a stable branch");
}

const latestVersion = getLatestVersion();
const currentVersion = versionToNumber(branch);

// Patching latest version
if (currentVersion === latestVersion) {
return { npmTag: "latest" };
}

// Patching an older stable version
if (currentVersion < latestVersion) {
return { npmTag: "v" + branch };
}

// Publishing a new latest version
if (tag === "latest") {
return { npmTag: tag };
}

// Publishing a release candidate
return { npmTag: NPM_TAG_NEXT, prerelease: "rc" };
}

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

const { defaultBase, release } = config;

// `defaultBase` determines what we diff against when looking for tags or
// released version and must therefore be set to either the main branch or one
// of the stable branches.
if (currentBranch !== defaultBase) {
errors.push(`'defaultBase' must be set to '${currentBranch}'`);
config.defaultBase = currentBranch;
}

// Determines whether we need to add "nightly" or "rc" to the version string.
const { currentVersionResolverMetadata, preid } = release.version.generatorOptions;
if (preid !== prerelease) {
errors.push(`'release.version.generatorOptions.preid' must be set to '${prerelease || ""}'`);
if (prerelease) {
release.version.generatorOptions.preid = prerelease;
} else {
// @ts-expect-error `preid` is optional
release.version.generatorOptions.preid = undefined;
}
}

// What the published version should be tagged as e.g., "latest" or "nightly".
if (currentVersionResolverMetadata.tag !== tag) {
errors.push(`'release.version.generatorOptions.currentVersionResolverMetadata.tag' must be set to '${tag}'`);
release.version.generatorOptions.currentVersionResolverMetadata.tag = tag;
}

if (errors.length > 0) {
errors.forEach(error);
throw new Error("Nx Release is not correctly configured for the current branch");
}

enablePublishingOnAzurePipelines();
}

/**
* @param {string} file
* @param {string} tag
* @returns {boolean}
*/
function verifyPublishPipeline(file, tag) {
const data = fs.readFileSync(file, { encoding: "utf-8" });
const m = data.match(/publishTag: '(\w*?)'/);
if (!m) {
error(`${file}: Could not find npm publish tag`);
return false;
}

if (m[1] !== tag) {
error(`${file}: 'publishTag' needs to be set to '${tag}'`);
return false;
}

return true;
}

/**
* @param {Options} options
* @returns {number}
*/
function main(options) {
const branch = getCurrentBranch();
if (!branch) {
error("Could not get current branch");
return 1;
}

if (!verifyPublishPipeline(ADO_PUBLISH_PIPELINE, options.tag || NPM_TAG_NEXT)) {
return 1;
}

const config = loadNxConfig(NX_CONFIG_FILE);
try {
if (isMainBranch(branch)) {
enablePublishing(config, branch, NPM_TAG_NIGHTLY, NPM_TAG_NIGHTLY);
} else if (isStableBranch(branch)) {
const { npmTag, prerelease } = getTagForStableBranch(branch, options);
enablePublishing(config, branch, npmTag, prerelease);
}
} catch (e) {
if (options.update) {
const fd = fs.openSync(NX_CONFIG_FILE, "w");
fs.writeSync(fd, JSON.stringify(config, undefined, 2));
fs.writeSync(fd, "\n");
fs.closeSync(fd)
} else {
console.error(`${e}`);
}
return 1;
}

return 0;
}

const { values } = util.parseArgs({
args: process.argv.slice(2),
options: {
tag: {
type: "string",
default: NPM_TAG_NEXT,
},
update: {
type: "boolean",
default: false,
},
},
strict: true,
});

process.exitCode = main(values);
11 changes: 3 additions & 8 deletions .ado/scripts/verdaccio.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,9 @@ case ${1-} in

"publish")
checkpoint=$(git rev-parse HEAD)
yarn set-version 1000.0.0-pr
git commit --all --message 'bump' --no-verify
packages=()
for json in $(yarn workspaces list --no-private --json); do
packages+=(--package $(node --print "JSON.parse('$json').name"))
done
npx beachball change --no-fetch --type patch --message 'bump for testing purposes' ${packages[@]}
npx beachball $* --no-push --registry $NPM_REGISTRY --yes --access public --no-generate-changelog
cp nx.test.json nx.json
yarn nx release version 1000.0.0
yarn nx release publish --registry $NPM_REGISTRY
git reset --hard $checkpoint
;;
esac
45 changes: 0 additions & 45 deletions .ado/templates/apple-steps-publish.yml

This file was deleted.

Loading