From d190ccafd3faa7ef3d807bbb10981129fe44ef9e Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Mon, 12 Feb 2024 10:43:48 -0800 Subject: [PATCH] Add retry to monorepo publish script (#42964) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/42964 We've seen npm publishes fail occasionally in CI as part of this script, most recently in S391653. This change adds a single retry, per package, during the execution of this script, in an attempt to reduce the chance of manual interventions after a broken pipeline. Changelog: [Internal] Reviewed By: cipolleschi Differential Revision: D53607808 fbshipit-source-id: 526d9c33d51ec57702efba3c199bad313c1bf2d4 --- ...nd-and-publish-all-bumped-packages-test.js | 92 +++++++++++++++++++ .../find-and-publish-all-bumped-packages.js | 46 +++++++--- 2 files changed, 126 insertions(+), 12 deletions(-) diff --git a/scripts/monorepo/__tests__/find-and-publish-all-bumped-packages-test.js b/scripts/monorepo/__tests__/find-and-publish-all-bumped-packages-test.js index e30468a1c0..ab961bde30 100644 --- a/scripts/monorepo/__tests__/find-and-publish-all-bumped-packages-test.js +++ b/scripts/monorepo/__tests__/find-and-publish-all-bumped-packages-test.js @@ -172,6 +172,98 @@ describe('findAndPublishAllBumpedPackages', () => { ] `); }); + + describe('retry behaviour', () => { + beforeEach(() => { + execSync.mockImplementation((command: string) => { + switch (command) { + case 'git log -1 --pretty=%B': + return BUMP_COMMIT_MESSAGE; + } + }); + getPackagesMock.mockResolvedValue({ + '@react-native/package-a': { + name: '@react-native/package-a', + path: 'absolute/path/to/package-a', + packageJson: { + version: '0.72.1', + }, + }, + '@react-native/package-b': { + name: '@react-native/package-b', + path: 'absolute/path/to/package-b', + packageJson: { + version: '0.72.1', + }, + }, + }); + fetchMock.mockResolvedValue({ + json: () => + Promise.resolve({ + versions: {'0.72.0': {}}, + }), + }); + }); + + test('should retry once if `npm publish` fails', async () => { + execMock.mockImplementationOnce(() => ({code: 0})); + execMock.mockImplementationOnce(() => ({ + code: 1, + stderr: '503 Service Unavailable', + })); + execMock.mockImplementationOnce(() => ({code: 0})); + + const consoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + await findAndPublishAllBumpedPackages(); + + expect(consoleError.mock.calls.flat().join('\n')).toMatchInlineSnapshot(` + "Failed to publish @react-native/package-b. npm publish exited with code 1: + 503 Service Unavailable" + `); + expect(execMock.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "npm publish", + Object { + "cwd": "absolute/path/to/package-a", + }, + ], + Array [ + "npm publish", + Object { + "cwd": "absolute/path/to/package-b", + }, + ], + Array [ + "npm publish", + Object { + "cwd": "absolute/path/to/package-b", + }, + ], + ] + `); + }); + + test('should exit with error if one or more packages fail after retry', async () => { + execMock.mockImplementationOnce(() => ({code: 0})); + execMock.mockImplementation(() => ({ + code: 1, + stderr: '503 Service Unavailable', + })); + + const consoleLog = jest + .spyOn(console, 'log') + .mockImplementation(() => {}); + + await findAndPublishAllBumpedPackages(); + + expect(consoleLog).toHaveBeenLastCalledWith('--- Retrying once! ---'); + expect(process.exitCode).toBe(1); + }); + }); }); describe('getTagsFromCommitMessage', () => { diff --git a/scripts/monorepo/find-and-publish-all-bumped-packages.js b/scripts/monorepo/find-and-publish-all-bumped-packages.js index b96647b03a..55230030c5 100644 --- a/scripts/monorepo/find-and-publish-all-bumped-packages.js +++ b/scripts/monorepo/find-and-publish-all-bumped-packages.js @@ -71,6 +71,7 @@ async function findAndPublishAllBumpedPackages() { console.log('Publishing updated packages to npm'); const tags = getTagsFromCommitMessage(commitMessage); + const failedPackages = []; for (const packageName of packagesToUpdate) { const package = packages[packageName]; @@ -78,21 +79,23 @@ async function findAndPublishAllBumpedPackages() { `- Publishing ${package.name} (${package.packageJson.version})`, ); - const result = publishPackage(package.path, { - tags, - otp: NPM_CONFIG_OTP, - }); - - if (result.code !== 0) { - console.error( - `Failed to publish ${package.name}. npm publish exited with code ${result.code}:`, - ); - console.error(result.stderr); - process.exitCode = 1; - return; + try { + runPublish(package.name, package.path, tags); + } catch { + console.log('--- Retrying once! ---'); + try { + runPublish(package.name, package.path, tags); + } catch (e) { + failedPackages.push(package.name); + } } } + if (failedPackages.length) { + process.exitCode = 1; + return; + } + console.log('Done ✅'); } @@ -106,6 +109,25 @@ function getTagsFromCommitMessage(msg /*: string */) /*: Array */ { .slice(1); } +function runPublish( + packageName /*: string */, + packagePath /*: string */, + tags /*: Array */, +) { + const result = publishPackage(packagePath, { + tags, + otp: NPM_CONFIG_OTP, + }); + + if (result.code !== 0) { + console.error( + `Failed to publish ${packageName}. npm publish exited with code ${result.code}:`, + ); + console.error(result.stderr); + throw new Error(result.stderr); + } +} + if (require.main === module) { // eslint-disable-next-line no-void void findAndPublishAllBumpedPackages();