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());