-
Notifications
You must be signed in to change notification settings - Fork 24.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replace sh scripts with tested JS scripts to release template (#46363)
Summary: The previous scripts to trigger the react-native-communty/template release workflow has not been working. This is a rewrite is js, along with some testing to make this more robust. I've have a PR to combine the publish and tag steps in the template publication: react-native-community/template#65, this takes advantage of that change. Changelog: [Internal] Pull Request resolved: #46363 Test Plan: 1. Unit tests 2. Once the infrastructure lands in the `react-native-community/template` workflow, we can trigger a dry run. ## TODO: - ~~Still needs to be used in the GH release workflow.~~ - ~~Template release workflow needs to land the dry_run input change.~~ ## Changelog: [Internal] Reviewed By: cipolleschi Differential Revision: D62296008 Pulled By: blakef fbshipit-source-id: 217326c44b1d820e36a1d847cf9ad24d228087c1
- Loading branch information
Showing
4 changed files
with
305 additions
and
38 deletions.
There are no files selected for viewing
158 changes: 158 additions & 0 deletions
158
.github/workflow-scripts/__tests__/publishTemplate-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
/** | ||
* 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 and not 'latest'", async () => { | ||
const NOT_LATEST = false; | ||
mockGetNpmPackageInfo | ||
// template@<version> | ||
.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( | ||
'@react-native-community/template', | ||
version, | ||
); | ||
}); | ||
|
||
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( | ||
'@react-native-community/template', | ||
'latest', | ||
); | ||
}); | ||
|
||
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(() => | ||
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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
/** | ||
* 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/; | ||
|
||
/** | ||
* Should this commit be `latest` on npm? | ||
*/ | ||
function isLatest() { | ||
const commitMessage = run('git log -n1 --pretty=%B'); | ||
return TAG_AS_LATEST_REGEX.test(commitMessage); | ||
} | ||
module.exports.isLatest = isLatest; | ||
|
||
/** | ||
* Create a Github Action to publish the community template matching the released version | ||
* of React Native. | ||
*/ | ||
module.exports.publishTemplate = async (github, version, dryRun = true) => { | ||
log(`📤 Get the ${TEMPLATE_NPM_PKG} repo to publish ${version}`); | ||
|
||
const is_latest_on_npm = isLatest(); | ||
|
||
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, | ||
// 0.75.0-rc.0, note no 'v' prefix | ||
version: version.replace(/^v/, ''), | ||
}, | ||
}); | ||
}; | ||
|
||
const SLEEP_S = 10; | ||
const MAX_RETRIES = 3 * 6; // 3 minutes | ||
const TEMPLATE_NPM_PKG = '@react-native-community/template'; | ||
|
||
/** | ||
* Will verify that @latest and the @<version> 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, | ||
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); | ||
} | ||
|
||
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); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/** | ||
* 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; | ||
|
||
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 => | ||
res.json(), | ||
); | ||
} | ||
module.exports.getNpmPackageInfo = getNpmPackageInfo; | ||
|
||
module.exports.log = (...args) => console.log(...args); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters