diff --git a/.github/workflow-scripts/__tests__/publishTemplate-test.js b/.github/workflow-scripts/__tests__/publishTemplate-test.js new file mode 100644 index 00000000000000..b74d6ed62a02e8 --- /dev/null +++ b/.github/workflow-scripts/__tests__/publishTemplate-test.js @@ -0,0 +1,122 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ +const {publishTemplate, verifyPublishedTemplate} = require('../publishTemplate'); + +const mockRun = jest.fn(); +const mockSleep = jest.fn(); +const mockGetNpmPackageInfo = jest.fn(); +const silence = () => {}; + +jest.mock('../utils.js', () => ({ + log: silence, + run: mockRun, + sleep: mockSleep, + getNpmPackageInfo: mockGetNpmPackageInfo, +})); + +const getMockGithub = () => ({ + rest: { + actions: { + createWorkflowDispatch: jest.fn(), + } + } +}); + +describe("#publishTemplate", () => { + beforeEach(jest.clearAllMocks); + + it("checks commits for magic #publish-package-to-npm&latest string and sets latest", async () => { + mockRun.mockReturnValueOnce(` + The commit message + + #publish-packages-to-npm&latest`); + + const github = getMockGithub(); + await publishTemplate( + github, + '0.76.0', + true, + ); + expect(github.rest.actions.createWorkflowDispatch).toHaveBeenCalledWith({ + owner: 'react-native-community', + repo: 'template', + workflow_id: 'release.yml', + ref: '0.76-stable', + inputs: { + dry_run: true, + is_latest_on_npm: true, + version: '0.76.0', + }, + }); + }); + + it("pubished as is_latest_on_npm = false if missing magic string", async () => { + mockRun.mockReturnValueOnce(` + The commit message without magic + `); + + const github = getMockGithub(); + await publishTemplate( + github, + '0.76.0', + false, + ); + expect(github.rest.actions.createWorkflowDispatch).toHaveBeenCalledWith({ + owner: 'react-native-community', + repo: 'template', + workflow_id: 'release.yml', + ref: '0.76-stable', + inputs: { + dry_run: false, + is_latest_on_npm: false, + version: '0.76.0', + }, + }); + }); +}); + +describe("#verifyPublishedTemplate", () => { + beforeEach(jest.clearAllMocks); + + it("waits on npm updating for version but skips if not latest", async () => { + mockGetNpmPackageInfo + // template@ + .mockReturnValueOnce(Promise.reject('mock http/404')) + .mockReturnValueOnce(Promise.resolve()); + mockSleep + .mockReturnValueOnce(Promise.resolve()) + .mockImplementation(() => { throw new Error('Should not be called again!') }); + + const version = '0.77.0'; + await verifyPublishedTemplate(version); + + expect(mockGetNpmPackageInfo).toHaveBeenLastCalledWith('@react-native-community/template', version); + }); + + it("waits on npm updating version and latest tag", async () => { + const version = '0.77.0'; + mockGetNpmPackageInfo + // template@ + .mockReturnValueOnce(Promise.reject('mock http/404')) + .mockReturnValueOnce(Promise.resolve()) + // template@latest != version + .mockReturnValueOnce(Promise.resolve({ version: '0.76.5' })) + // template@latest == version + .mockReturnValueOnce(Promise.resolve({ version })); + mockSleep + .mockReturnValueOnce(Promise.resolve()) + .mockReturnValueOnce(Promise.resolve()) + .mockImplementation(() => { throw new Error('Should not be called again!') }); + + await verifyPublishedTemplate(version, true); + + expect(mockGetNpmPackageInfo).toHaveBeenCalledWith('@react-native-community/template', version); + expect(mockGetNpmPackageInfo).toHaveBeenCalledWith('@react-native-community/template', 'latest'); + }); +}); diff --git a/.github/workflow-scripts/publishTemplate.js b/.github/workflow-scripts/publishTemplate.js new file mode 100644 index 00000000000000..e1b7fa607b7a28 --- /dev/null +++ b/.github/workflow-scripts/publishTemplate.js @@ -0,0 +1,89 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ +const {run, sleep, getNpmPackageInfo, log} = require('./utils.js'); + +const TAG_AS_LATEST_REGEX=/#publish-packages-to-npm&latest/ + +/** + * Create a Github Action to publish the community template matching the released version + * of React Native. + */ +module.exports.publishTemplate = async ( + github, + version, // 0.75.0-rc.0, note no 'v' prefix + dryRun = true, +) => { + log(`📤 Get the ${TEMPLATE_NPM_PKG} repo to publish ${version}`); + + const commitMessage = run('git log -n1 --pretty=%B'); + const isLatest = TAG_AS_LATEST_REGEX.test(commitMessage); + + const majorMinor = /^v?(\d+\.\d+)/.exec(version); + + if (!majorMinor) { + log(`🔥 can't capture MAJOR.MINOR from '${version}', giving up.`); + process.exit(1); + } + + // MAJOR.MINOR-stable + const ref = `${majorMinor[1]}-stable`; + + await github.rest.actions.createWorkflowDispatch({ + owner: 'react-native-community', + repo: 'template', + workflow_id: 'release.yml', + ref, + inputs: { + dry_run: dryRun, + is_latest_on_npm: isLatest, + version, + }, + }); +}; + +const SLEEP_S = 10; +const TEMPLATE_NPM_PKG = '@react-native-community/template'; + +/** + * Will verify that @latest and the @ have been published. + * + * NOTE: This will infinitely query each step until successful, make sure the + * calling job has a timeout. + */ +module.exports.verifyPublishedTemplate = async (version, latest = false) => { + log(`🔍 Is ${TEMPLATE_NPM_PKG}@${version} on npm?`); + + while (true) { + try { + await getNpmPackageInfo(TEMPLATE_NPM_PKG, version); + log(`🎉 Found ${TEMPLATE_NPM_PKG}@${version} on npm`); + break; + } catch(e) { + log(`Nope, fetch failed: ${e.message}`); + } + await sleep(SLEEP_S); + } + + log(`🔍 Does latest → ${version} on npm?`); + + while (latest) { + try { + const pkg = await getNpmPackageInfo(TEMPLATE_NPM_PKG, 'latest'); + if (pkg.version === version) { + log(`🎉 ${TEMPLATE_NPM_PKG}@latest → ${version} on npm`); + break; + } else { + log(`🐌 ${TEMPLATE_NPM_PKG}@latest → ${pkg.version} on npm, retrying...`); + } + } catch(e) { + log(`🚨 Fetch failed (will retry): ${e.message}`); + } + await sleep(SLEEP_S); + } +}; diff --git a/.github/workflow-scripts/utils.js b/.github/workflow-scripts/utils.js new file mode 100644 index 00000000000000..74f5e797ba3645 --- /dev/null +++ b/.github/workflow-scripts/utils.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +const {execSync} = require('child_process'); + +function run(...cmd) { + return execSync(cmd, 'utf8').toString().trim(); +} +module.exports.run = run; + +await function sleep(seconds) { + return new Promise(resolve => setTimeout(resolve, seconds * 1000)); +} +module.exports.sleep = sleep; + +async function getNpmPackageInfo(pkg, versionOrTag) { + return fetch(`https://registry.npmjs.org/${pkg}/${versionOrTag}`).then(resp => res.json()); +} +module.exports.getNpmPackageInfo = getNpmPackageInfo; + +module.exports.log = (...args) => console.log(...args); +