From a52f5514edffb7932d028e2c7a52651d088b5273 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Wed, 5 Feb 2025 06:33:57 -0800 Subject: [PATCH] Automate the check for the Release on NPM (#49164) Summary: One of the steps we perform when doing a release is to run `npm view react-native` to verify that the release has been published and it is available with the right tag. As of today, we check this manually. This change aims at automating this check so that we don't have to do it manually ourselves. ## Changelog: [Internal] - Releases: automate the npm view check Pull Request resolved: https://github.com/facebook/react-native/pull/49164 Test Plan: Created a veriftyReleaseOnNPM-tests.js jest test to verify that the script works fine. Screenshot 2025-02-04 at 15 18 24 Reviewed By: cortinico Differential Revision: D69118622 Pulled By: cipolleschi fbshipit-source-id: a8d40cd2fcb164d8f7174de680b340510f3e8551 --- .../__tests__/publishTemplate-test.js | 66 +++------ .../__tests__/veriftyReleaseOnNPM-test.js | 104 ++++++++++++++ .../__tests__/verifyPublishedPackage-test.js | 136 ++++++++++++++++++ .github/workflow-scripts/publishTemplate.js | 46 ++---- .github/workflow-scripts/utils.js | 12 +- .../verifyPublishedPackage.js | 64 +++++++++ .../workflow-scripts/verifyReleaseOnNpm.js | 26 ++++ .github/workflows/publish-release.yml | 10 ++ 8 files changed, 377 insertions(+), 87 deletions(-) create mode 100644 .github/workflow-scripts/__tests__/veriftyReleaseOnNPM-test.js create mode 100644 .github/workflow-scripts/__tests__/verifyPublishedPackage-test.js create mode 100644 .github/workflow-scripts/verifyPublishedPackage.js create mode 100644 .github/workflow-scripts/verifyReleaseOnNpm.js diff --git a/.github/workflow-scripts/__tests__/publishTemplate-test.js b/.github/workflow-scripts/__tests__/publishTemplate-test.js index a956b922b7e..3ea6a1cb90c 100644 --- a/.github/workflow-scripts/__tests__/publishTemplate-test.js +++ b/.github/workflow-scripts/__tests__/publishTemplate-test.js @@ -15,12 +15,14 @@ const { const mockRun = jest.fn(); const mockSleep = jest.fn(); const mockGetNpmPackageInfo = jest.fn(); +const mockVerifyPublishedPackage = jest.fn(); const silence = () => {}; jest.mock('../utils.js', () => ({ log: silence, run: mockRun, sleep: mockSleep, + verifyPublishedPackage: mockVerifyPublishedPackage, getNpmPackageInfo: mockGetNpmPackageInfo, })); @@ -82,77 +84,43 @@ describe('#verifyPublishedTemplate', () => { it("waits on npm updating for version and not 'latest'", async () => { const NOT_LATEST = false; - 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, NOT_LATEST); - expect(mockGetNpmPackageInfo).toHaveBeenLastCalledWith( + expect(mockVerifyPublishedPackage).toHaveBeenCalledWith( '@react-native-community/template', version, + null, + 18, ); }); it('waits on npm updating version and latest tag', async () => { const IS_LATEST = true; const version = '0.77.0'; - mockGetNpmPackageInfo - // template@latest → unknown tag - .mockReturnValueOnce(Promise.reject('mock http/404')) - // template@latest != version → old tag - .mockReturnValueOnce(Promise.resolve({version: '0.76.5'})) - // template@latest == version → correct tag - .mockReturnValueOnce(Promise.resolve({version})); - mockSleep - .mockReturnValueOnce(Promise.resolve()) - .mockReturnValueOnce(Promise.resolve()) - .mockImplementation(() => { - throw new Error('Should not be called again!'); - }); await verifyPublishedTemplate(version, IS_LATEST); - expect(mockGetNpmPackageInfo).toHaveBeenCalledWith( + expect(mockVerifyPublishedPackage).toHaveBeenCalledWith( '@react-native-community/template', + version, 'latest', + 18, ); }); - describe('timeouts', () => { - let mockProcess; - beforeEach(() => { - mockProcess = jest.spyOn(process, 'exit').mockImplementation(code => { - throw new Error(`process.exit(${code}) called!`); - }); - }); - afterEach(() => mockProcess.mockRestore()); + describe('retries', () => { it('will timeout if npm does not update package version after a set number of retries', async () => { const RETRIES = 2; - mockGetNpmPackageInfo.mockReturnValue(Promise.reject('mock http/404')); - mockSleep.mockReturnValue(Promise.resolve()); - await expect(() => - verifyPublishedTemplate('0.77.0', true, RETRIES), - ).rejects.toThrowError('process.exit(1) called!'); - expect(mockGetNpmPackageInfo).toHaveBeenCalledTimes(RETRIES); - }); - it('will timeout if npm does not update latest tag after a set number of retries', async () => { - const RETRIES = 7; - const IS_LATEST = true; - mockGetNpmPackageInfo.mockReturnValue( - Promise.resolve({version: '0.76.5'}), - ); - mockSleep.mockReturnValue(Promise.resolve()); - await expect(async () => { - await verifyPublishedTemplate('0.77.0', IS_LATEST, RETRIES); - }).rejects.toThrowError('process.exit(1) called!'); - expect(mockGetNpmPackageInfo).toHaveBeenCalledTimes(RETRIES); + await verifyPublishedTemplate('0.77.0', true, RETRIES), + expect(mockVerifyPublishedPackage).toHaveBeenCalledWith( + '@react-native-community/template', + '0.77.0', + 'latest', + 2, + ); }); }); }); diff --git a/.github/workflow-scripts/__tests__/veriftyReleaseOnNPM-test.js b/.github/workflow-scripts/__tests__/veriftyReleaseOnNPM-test.js new file mode 100644 index 00000000000..1d3307f6ae7 --- /dev/null +++ b/.github/workflow-scripts/__tests__/veriftyReleaseOnNPM-test.js @@ -0,0 +1,104 @@ +/** + * 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 {verifyReleaseOnNpm} = require('../verifyReleaseOnNpm'); + +const mockVerifyPublishedPackage = jest.fn(); +const silence = () => {}; + +jest.mock('../utils.js', () => ({ + verifyPublishedPackage: mockVerifyPublishedPackage, +})); + +describe('#verifyReleaseOnNPM', () => { + beforeEach(jest.clearAllMocks); + + it("waits on npm updating for version and not 'latest'", async () => { + const NOT_LATEST = false; + const version = '0.78.0'; + await verifyReleaseOnNpm(version, NOT_LATEST); + + expect(mockVerifyPublishedPackage).toHaveBeenLastCalledWith( + 'react-native', + version, + null, + 18, + ); + }); + + it('waits on npm updating version and latest tag', async () => { + const IS_LATEST = true; + const version = '0.78.0'; + + await verifyReleaseOnNpm(version, IS_LATEST); + + expect(mockVerifyPublishedPackage).toHaveBeenCalledWith( + 'react-native', + version, + 'latest', + 18, + ); + }); + + it('waits on npm updating version, not latest and next tag', async () => { + const IS_LATEST = false; + const version = '0.78.0-rc.0'; + + await verifyReleaseOnNpm(version, IS_LATEST); + + expect(mockVerifyPublishedPackage).toHaveBeenCalledWith( + 'react-native', + version, + 'next', + 18, + ); + }); + + it('waits on npm updating version, latest and next tag', async () => { + const IS_LATEST = true; + const version = '0.78.0-rc.0'; + + await verifyReleaseOnNpm(version, IS_LATEST); + + expect(mockVerifyPublishedPackage).toHaveBeenCalledWith( + 'react-native', + version, + 'next', + 18, + ); + }); + + describe('timeouts', () => { + it('will timeout if npm does not update package version after a set number of retries', async () => { + const RETRIES = 2; + + await verifyReleaseOnNpm('0.77.0', true, RETRIES), + expect(mockVerifyPublishedPackage).toHaveBeenCalledWith( + 'react-native', + '0.77.0', + 'latest', + 2, + ); + }); + + it('will timeout if npm does not update latest tag after a set number of retries', async () => { + const RETRIES = 7; + const IS_LATEST = true; + + await verifyReleaseOnNpm('0.77.0', IS_LATEST, RETRIES); + + expect(mockVerifyPublishedPackage).toHaveBeenCalledWith( + 'react-native', + '0.77.0', + 'latest', + 7, + ); + }); + }); +}); diff --git a/.github/workflow-scripts/__tests__/verifyPublishedPackage-test.js b/.github/workflow-scripts/__tests__/verifyPublishedPackage-test.js new file mode 100644 index 00000000000..ca12bb9a646 --- /dev/null +++ b/.github/workflow-scripts/__tests__/verifyPublishedPackage-test.js @@ -0,0 +1,136 @@ +/** + * 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 {verifyPublishedPackage} = require('../verifyPublishedPackage'); + +const mockRun = jest.fn(); +const mockSleep = jest.fn(); +const mockGetNpmPackageInfo = jest.fn(); +const silence = () => {}; + +const REACT_NATIVE_PACKAGE = 'react-native'; + +jest.mock('../utils.js', () => ({ + log: silence, + run: mockRun, + sleep: mockSleep, + getNpmPackageInfo: mockGetNpmPackageInfo, +})); + +describe('#verifyPublishedPackage', () => { + beforeEach(jest.clearAllMocks); + + it("waits on npm updating for version and 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.78.0'; + await verifyPublishedPackage(REACT_NATIVE_PACKAGE, version, null); + + expect(mockGetNpmPackageInfo).toHaveBeenLastCalledWith( + REACT_NATIVE_PACKAGE, + version, + ); + }); + + it('waits on npm updating version and latest tag', async () => { + const version = '0.78.0'; + mockGetNpmPackageInfo + // template@latest → unknown tag + .mockReturnValueOnce(Promise.reject('mock http/404')) + // template@latest != version → old tag + .mockReturnValueOnce(Promise.resolve({version: '0.76.5'})) + // template@latest == version → correct tag + .mockReturnValueOnce(Promise.resolve({version})); + mockSleep + .mockReturnValueOnce(Promise.resolve()) + .mockReturnValueOnce(Promise.resolve()) + .mockImplementation(() => { + throw new Error('Should not be called again!'); + }); + + await verifyPublishedPackage(REACT_NATIVE_PACKAGE, version, 'latest'); + + expect(mockGetNpmPackageInfo).toHaveBeenCalledWith( + REACT_NATIVE_PACKAGE, + 'latest', + ); + }); + + it('waits on npm updating version and next tag', async () => { + const version = '0.78.0-rc.0'; + mockGetNpmPackageInfo + // template@latest → unknown tag + .mockReturnValueOnce(Promise.reject('mock http/404')) + // template@latest != version → old tag + .mockReturnValueOnce(Promise.resolve({version: '0.76.5'})) + // template@latest == version → correct tag + .mockReturnValueOnce(Promise.resolve({version})); + mockSleep + .mockReturnValueOnce(Promise.resolve()) + .mockReturnValueOnce(Promise.resolve()) + .mockImplementation(() => { + throw new Error('Should not be called again!'); + }); + debugger; + await verifyPublishedPackage(REACT_NATIVE_PACKAGE, version, 'next'); + + expect(mockGetNpmPackageInfo).toHaveBeenCalledWith( + REACT_NATIVE_PACKAGE, + 'next', + ); + }); + + describe('timeouts', () => { + let mockProcess; + beforeEach(() => { + mockProcess = jest.spyOn(process, 'exit').mockImplementation(code => { + throw new Error(`process.exit(${code}) called!`); + }); + }); + afterEach(() => mockProcess.mockRestore()); + it('will timeout if npm does not update package version after a set number of retries', async () => { + const RETRIES = 2; + mockGetNpmPackageInfo.mockReturnValue(Promise.reject('mock http/404')); + mockSleep.mockReturnValue(Promise.resolve()); + await expect(() => + verifyPublishedPackage( + REACT_NATIVE_PACKAGE, + '0.77.0', + 'latest', + RETRIES, + ), + ).rejects.toThrowError('process.exit(1) called!'); + expect(mockGetNpmPackageInfo).toHaveBeenCalledTimes(RETRIES); + }); + + it('will timeout if npm does not update latest tag after a set number of retries', async () => { + const RETRIES = 7; + const IS_LATEST = true; + mockGetNpmPackageInfo.mockReturnValue( + Promise.resolve({version: '0.76.5'}), + ); + mockSleep.mockReturnValue(Promise.resolve()); + await expect(async () => { + await verifyPublishedPackage( + REACT_NATIVE_PACKAGE, + '0.77.0', + 'latest', + RETRIES, + ); + }).rejects.toThrowError('process.exit(1) called!'); + expect(mockGetNpmPackageInfo).toHaveBeenCalledTimes(RETRIES); + }); + }); +}); diff --git a/.github/workflow-scripts/publishTemplate.js b/.github/workflow-scripts/publishTemplate.js index b0c8946b679..d112922c11d 100644 --- a/.github/workflow-scripts/publishTemplate.js +++ b/.github/workflow-scripts/publishTemplate.js @@ -7,7 +7,7 @@ * @format */ -const {run, sleep, getNpmPackageInfo, log} = require('./utils.js'); +const {run, sleep, log, verifyPublishedPackage} = require('./utils.js'); const TAG_AS_LATEST_REGEX = /#publish-packages-to-npm&latest/; @@ -53,8 +53,7 @@ module.exports.publishTemplate = async (github, version, dryRun = true) => { }); }; -const SLEEP_S = 10; -const MAX_RETRIES = 3 * 6; // 3 minutes +const MAX_RETRIES = 3 * 6; // 18 attempts. Waiting between attempt: 10 s. Total time: 3 mins. const TEMPLATE_NPM_PKG = '@react-native-community/template'; /** @@ -68,36 +67,15 @@ module.exports.verifyPublishedTemplate = async ( latest = false, retries = MAX_RETRIES, ) => { - log(`🔍 Is ${TEMPLATE_NPM_PKG}@${version} on npm?`); - - let count = retries; - while (count-- > 0) { - try { - const json = await getNpmPackageInfo( - TEMPLATE_NPM_PKG, - latest ? 'latest' : version, - ); - log(`🎉 Found ${TEMPLATE_NPM_PKG}@${version} on npm`); - if (!latest) { - return; - } - if (json.version === version) { - log(`🎉 ${TEMPLATE_NPM_PKG}@latest → ${version} on npm`); - return; - } - log( - `🐌 ${TEMPLATE_NPM_PKG}@latest → ${pkg.version} on npm and not ${version} as expected, retrying...`, - ); - } catch (e) { - log(`Nope, fetch failed: ${e.message}`); - } - await sleep(SLEEP_S); + try { + await verifyPublishedPackage( + TEMPLATE_NPM_PKG, + version, + latest ? 'latest' : null, + retries, + ); + } catch (e) { + console.error(e.message); + process.exit(1); } - - let msg = `🚨 Timed out when trying to verify ${TEMPLATE_NPM_PKG}@${version} on npm`; - if (latest) { - msg += ' and latest tag points to this version.'; - } - log(msg); - process.exit(1); }; diff --git a/.github/workflow-scripts/utils.js b/.github/workflow-scripts/utils.js index 403ed5ff767..fdc13b9f0fc 100644 --- a/.github/workflow-scripts/utils.js +++ b/.github/workflow-scripts/utils.js @@ -12,18 +12,22 @@ const {execSync} = require('child_process'); function run(cmd) { return execSync(cmd, 'utf8').toString().trim(); } -module.exports.run = run; async 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 => resp.json(), ); } -module.exports.getNpmPackageInfo = getNpmPackageInfo; -module.exports.log = (...args) => console.log(...args); +const log = (...args) => console.log(...args); + +module.exports = { + log, + getNpmPackageInfo, + sleep, + run, +}; diff --git a/.github/workflow-scripts/verifyPublishedPackage.js b/.github/workflow-scripts/verifyPublishedPackage.js new file mode 100644 index 00000000000..7f84283ef26 --- /dev/null +++ b/.github/workflow-scripts/verifyPublishedPackage.js @@ -0,0 +1,64 @@ +/** + * 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 {log, getNpmPackageInfo, sleep} = require('./utils'); + +const SLEEP_S = 10; +const MAX_RETRIES = 3 * 6; // 18 attempts. Waiting between attempt: 10 s. Total time: 3 mins. + +async function verifyPublishedPackage( + packageName, + version, + tag = null, + retries = MAX_RETRIES, +) { + debugger; + log(`🔍 Is ${packageName}@${version} on npm?`); + + let count = retries; + while (count-- > 0) { + try { + const json = await getNpmPackageInfo(packageName, tag ? tag : version); + log(`🎉 Found ${packageName}@${version} on npm`); + if (!tag) { + return; + } + + // check for next tag + if (tag === 'next' && json.version === version) { + log(`🎉 ${packageName}@next → ${version} on npm`); + return; + } + + // Check for latest tag + if (tag === 'latest' && json.version === version) { + log(`🎉 ${packageName}@latest → ${version} on npm`); + return; + } + + log( + `🐌 ${packageName}@${tag} → ${pkg.version} on npm and not ${version} as expected, retrying...`, + ); + } catch (e) { + log(`Nope, fetch failed: ${e.message}`); + } + await sleep(SLEEP_S); + } + + let msg = `🚨 Timed out when trying to verify ${packageName}@${version} on npm`; + if (tag) { + msg += ` and ${tag} tag points to this version.`; + } + log(msg); + process.exit(1); +} + +module.exports = { + verifyPublishedPackage, +}; diff --git a/.github/workflow-scripts/verifyReleaseOnNpm.js b/.github/workflow-scripts/verifyReleaseOnNpm.js new file mode 100644 index 00000000000..b9352233959 --- /dev/null +++ b/.github/workflow-scripts/verifyReleaseOnNpm.js @@ -0,0 +1,26 @@ +/** + * 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, log, verifyPublishedPackage} = require('./utils.js'); +const REACT_NATIVE_NPM_PKG = 'react-native'; +const MAX_RETRIES = 3 * 6; // 18 attempts. Waiting between attempt: 10 s. Total time: 3 mins. +/** + * Will verify that @latest, @next and the @ have been published. + * + * NOTE: This will infinitely query each step until successful, make sure the + * calling job has a timeout. + */ +module.exports.verifyReleaseOnNpm = async ( + version, + latest = false, + retries = MAX_RETRIES, +) => { + const tag = version.includes('-rc.') ? 'next' : latest ? 'latest' : null; + await verifyPublishedPackage(REACT_NATIVE_NPM_PKG, version, tag, retries); +}; diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index d0fc186728b..734ef4c5fe9 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -214,3 +214,13 @@ jobs: -H "Accept: application/vnd.github.v3+json" \ -H "Authorization: Bearer $REACT_NATIVE_BOT_GITHUB_TOKEN" \ -d "{\"event_type\": \"publish\", \"client_payload\": { \"version\": \"${{ github.ref_name }}\" }}" + - name: Verify Release is on NPM + timeout-minutes: 3 + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }} + script: | + const {verifyReleaseOnNpm} = require('./.github/workflow-scripts/verifyReleaseOnNpm.js'); + const {isLatest()} = require('./.github/workflow-scripts/publishTemplate.js'); + const version = "${{ github.ref_name }}"; + await verifyReleaseOnNpm(version, isLatest());