Skip to content

Commit 45ae0b4

Browse files
committed
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
1 parent 02b879b commit 45ae0b4

4 files changed

Lines changed: 305 additions & 38 deletions

File tree

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
const {
11+
publishTemplate,
12+
verifyPublishedTemplate,
13+
} = require('../publishTemplate');
14+
15+
const mockRun = jest.fn();
16+
const mockSleep = jest.fn();
17+
const mockGetNpmPackageInfo = jest.fn();
18+
const silence = () => {};
19+
20+
jest.mock('../utils.js', () => ({
21+
log: silence,
22+
run: mockRun,
23+
sleep: mockSleep,
24+
getNpmPackageInfo: mockGetNpmPackageInfo,
25+
}));
26+
27+
const getMockGithub = () => ({
28+
rest: {
29+
actions: {
30+
createWorkflowDispatch: jest.fn(),
31+
},
32+
},
33+
});
34+
35+
describe('#publishTemplate', () => {
36+
beforeEach(jest.clearAllMocks);
37+
38+
it('checks commits for magic #publish-package-to-npm&latest string and sets latest', async () => {
39+
mockRun.mockReturnValueOnce(`
40+
The commit message
41+
42+
#publish-packages-to-npm&latest`);
43+
44+
const github = getMockGithub();
45+
await publishTemplate(github, '0.76.0', true);
46+
expect(github.rest.actions.createWorkflowDispatch).toHaveBeenCalledWith({
47+
owner: 'react-native-community',
48+
repo: 'template',
49+
workflow_id: 'release.yml',
50+
ref: '0.76-stable',
51+
inputs: {
52+
dry_run: true,
53+
is_latest_on_npm: true,
54+
version: '0.76.0',
55+
},
56+
});
57+
});
58+
59+
it('pubished as is_latest_on_npm = false if missing magic string', async () => {
60+
mockRun.mockReturnValueOnce(`
61+
The commit message without magic
62+
`);
63+
64+
const github = getMockGithub();
65+
await publishTemplate(github, '0.76.0', false);
66+
expect(github.rest.actions.createWorkflowDispatch).toHaveBeenCalledWith({
67+
owner: 'react-native-community',
68+
repo: 'template',
69+
workflow_id: 'release.yml',
70+
ref: '0.76-stable',
71+
inputs: {
72+
dry_run: false,
73+
is_latest_on_npm: false,
74+
version: '0.76.0',
75+
},
76+
});
77+
});
78+
});
79+
80+
describe('#verifyPublishedTemplate', () => {
81+
beforeEach(jest.clearAllMocks);
82+
83+
it("waits on npm updating for version and not 'latest'", async () => {
84+
const NOT_LATEST = false;
85+
mockGetNpmPackageInfo
86+
// template@<version>
87+
.mockReturnValueOnce(Promise.reject('mock http/404'))
88+
.mockReturnValueOnce(Promise.resolve());
89+
mockSleep.mockReturnValueOnce(Promise.resolve()).mockImplementation(() => {
90+
throw new Error('Should not be called again!');
91+
});
92+
93+
const version = '0.77.0';
94+
await verifyPublishedTemplate(version, NOT_LATEST);
95+
96+
expect(mockGetNpmPackageInfo).toHaveBeenLastCalledWith(
97+
'@react-native-community/template',
98+
version,
99+
);
100+
});
101+
102+
it('waits on npm updating version and latest tag', async () => {
103+
const IS_LATEST = true;
104+
const version = '0.77.0';
105+
mockGetNpmPackageInfo
106+
// template@latest → unknown tag
107+
.mockReturnValueOnce(Promise.reject('mock http/404'))
108+
// template@latest != version → old tag
109+
.mockReturnValueOnce(Promise.resolve({version: '0.76.5'}))
110+
// template@latest == version → correct tag
111+
.mockReturnValueOnce(Promise.resolve({version}));
112+
mockSleep
113+
.mockReturnValueOnce(Promise.resolve())
114+
.mockReturnValueOnce(Promise.resolve())
115+
.mockImplementation(() => {
116+
throw new Error('Should not be called again!');
117+
});
118+
119+
await verifyPublishedTemplate(version, IS_LATEST);
120+
121+
expect(mockGetNpmPackageInfo).toHaveBeenCalledWith(
122+
'@react-native-community/template',
123+
'latest',
124+
);
125+
});
126+
127+
describe('timeouts', () => {
128+
let mockProcess;
129+
beforeEach(() => {
130+
mockProcess = jest.spyOn(process, 'exit').mockImplementation(code => {
131+
throw new Error(`process.exit(${code}) called!`);
132+
});
133+
});
134+
afterEach(() => mockProcess.mockRestore());
135+
it('will timeout if npm does not update package version after a set number of retries', async () => {
136+
const RETRIES = 2;
137+
mockGetNpmPackageInfo.mockReturnValue(Promise.reject('mock http/404'));
138+
mockSleep.mockReturnValue(Promise.resolve());
139+
await expect(() =>
140+
verifyPublishedTemplate('0.77.0', true, RETRIES),
141+
).rejects.toThrowError('process.exit(1) called!');
142+
expect(mockGetNpmPackageInfo).toHaveBeenCalledTimes(RETRIES);
143+
});
144+
145+
it('will timeout if npm does not update latest tag after a set number of retries', async () => {
146+
const RETRIES = 7;
147+
const IS_LATEST = true;
148+
mockGetNpmPackageInfo.mockReturnValue(
149+
Promise.resolve({version: '0.76.5'}),
150+
);
151+
mockSleep.mockReturnValue(Promise.resolve());
152+
await expect(async () => {
153+
await verifyPublishedTemplate('0.77.0', IS_LATEST, RETRIES);
154+
}).rejects.toThrowError('process.exit(1) called!');
155+
expect(mockGetNpmPackageInfo).toHaveBeenCalledTimes(RETRIES);
156+
});
157+
});
158+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
const {run, sleep, getNpmPackageInfo, log} = require('./utils.js');
11+
12+
const TAG_AS_LATEST_REGEX = /#publish-packages-to-npm&latest/;
13+
14+
/**
15+
* Should this commit be `latest` on npm?
16+
*/
17+
function isLatest() {
18+
const commitMessage = run('git log -n1 --pretty=%B');
19+
return TAG_AS_LATEST_REGEX.test(commitMessage);
20+
}
21+
module.exports.isLatest = isLatest;
22+
23+
/**
24+
* Create a Github Action to publish the community template matching the released version
25+
* of React Native.
26+
*/
27+
module.exports.publishTemplate = async (github, version, dryRun = true) => {
28+
log(`📤 Get the ${TEMPLATE_NPM_PKG} repo to publish ${version}`);
29+
30+
const is_latest_on_npm = isLatest();
31+
32+
const majorMinor = /^v?(\d+\.\d+)/.exec(version);
33+
34+
if (!majorMinor) {
35+
log(`🔥 can't capture MAJOR.MINOR from '${version}', giving up.`);
36+
process.exit(1);
37+
}
38+
39+
// MAJOR.MINOR-stable
40+
const ref = `${majorMinor[1]}-stable`;
41+
42+
await github.rest.actions.createWorkflowDispatch({
43+
owner: 'react-native-community',
44+
repo: 'template',
45+
workflow_id: 'release.yml',
46+
ref,
47+
inputs: {
48+
dry_run: dryRun,
49+
is_latest_on_npm,
50+
// 0.75.0-rc.0, note no 'v' prefix
51+
version: version.replace(/^v/, ''),
52+
},
53+
});
54+
};
55+
56+
const SLEEP_S = 10;
57+
const MAX_RETRIES = 3 * 6; // 3 minutes
58+
const TEMPLATE_NPM_PKG = '@react-native-community/template';
59+
60+
/**
61+
* Will verify that @latest and the @<version> have been published.
62+
*
63+
* NOTE: This will infinitely query each step until successful, make sure the
64+
* calling job has a timeout.
65+
*/
66+
module.exports.verifyPublishedTemplate = async (
67+
version,
68+
latest = false,
69+
retries = MAX_RETRIES,
70+
) => {
71+
log(`🔍 Is ${TEMPLATE_NPM_PKG}@${version} on npm?`);
72+
73+
let count = retries;
74+
while (count-- > 0) {
75+
try {
76+
const json = await getNpmPackageInfo(
77+
TEMPLATE_NPM_PKG,
78+
latest ? 'latest' : version,
79+
);
80+
log(`🎉 Found ${TEMPLATE_NPM_PKG}@${version} on npm`);
81+
if (!latest) {
82+
return;
83+
}
84+
if (json.version === version) {
85+
log(`🎉 ${TEMPLATE_NPM_PKG}@latest → ${version} on npm`);
86+
return;
87+
}
88+
log(
89+
`🐌 ${TEMPLATE_NPM_PKG}@latest → ${pkg.version} on npm and not ${version} as expected, retrying...`,
90+
);
91+
} catch (e) {
92+
log(`Nope, fetch failed: ${e.message}`);
93+
}
94+
await sleep(SLEEP_S);
95+
}
96+
97+
let msg = `🚨 Timed out when trying to verify ${TEMPLATE_NPM_PKG}@${version} on npm`;
98+
if (latest) {
99+
msg += ' and latest tag points to this version.';
100+
}
101+
log(msg);
102+
process.exit(1);
103+
};

.github/workflow-scripts/utils.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
const {execSync} = require('child_process');
11+
12+
function run(...cmd) {
13+
return execSync(cmd, 'utf8').toString().trim();
14+
}
15+
module.exports.run = run;
16+
17+
async function sleep(seconds) {
18+
return new Promise(resolve => setTimeout(resolve, seconds * 1000));
19+
}
20+
module.exports.sleep = sleep;
21+
22+
async function getNpmPackageInfo(pkg, versionOrTag) {
23+
return fetch(`https://registry.npmjs.org/${pkg}/${versionOrTag}`).then(resp =>
24+
res.json(),
25+
);
26+
}
27+
module.exports.getNpmPackageInfo = getNpmPackageInfo;
28+
29+
module.exports.log = (...args) => console.log(...args);

.github/workflows/publish-release.yml

Lines changed: 15 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -191,46 +191,23 @@ jobs:
191191
gha-npm-token: ${{ env.GHA_NPM_TOKEN }}
192192
- name: Publish @react-native-community/template
193193
id: publish-template-to-npm
194-
shell: bash
195-
run: |
196-
COMMIT_MSG=$(git log -n1 --pretty=%B);
197-
if grep -q '#publish-packages-to-npm&latest' <<< "$COMMIT_MSG"; then
198-
echo "TAG=latest" >> $GITHUB_OUTPUT
199-
IS_LATEST=true
200-
else
201-
IS_LATEST=false
202-
fi
203-
# Go from v0.75.0-rc.4 -> 0.75-stable, which is the template's branching scheme
204-
VERSION=$(grep -oE '\d+\.\d+' <<< "${{ github.ref_name }}" | { read version; echo "$version-stable"; })
205-
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
206-
207-
curl -L https://api.github.com/repos/react-native-community/template/actions/workflows/release.yaml/dispatches
208-
-H "Accept: application/vnd.github.v3+json" \
209-
-H "Authorization: Bearer $REACT_NATIVE_BOT_GITHUB_TOKEN" \
210-
-d "{\"ref\":\"$VERSION\",\"inputs\":{\"version\":\"${{ github.ref_name }}\",\"is_latest_on_npm\":\"$IS_LATEST\"}}"
194+
uses: actions/github-script@v6
195+
with:
196+
github-token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
197+
script: |
198+
const {publishTemplate} = require('./.github/workflow-scripts/publishTemplate.js')
199+
const version = "${{ github.ref_name }}"
200+
const isDryRun = false
201+
await publishTemplate(github, version, isDryRun);
211202
- name: Wait for template to be published
212203
timeout-minutes: 3
213-
env:
214-
VERSION: ${{ steps.publish-template-to-npm.outputs.VERSION }}
215-
TAG: ${{ steps.publish-template-to-npm.outputs.TAG }}
216-
shell: bash
217-
run: |
218-
echo "Waiting until @react-native-community/template is published to npm"
219-
while true; do
220-
if curl -o /dev/null -s -f "https://registry.npmjs.org/@react-native-community/template/$VERSION"; then
221-
echo "Confirm that @react-native-community/template@$VERSION is published on npm"
222-
break
223-
fi
224-
sleep 10
225-
done
226-
while [ "$TAG" == "latest" ]; do
227-
CURRENT=$(curl -s "https://registry.npmjs.org/react-native/latest" | jq -r '.version');
228-
if [ "$CURRENT" == "$VERSION" ]; then
229-
echo "Confirm that @react-native-community/template@latest == $VERSION on npm"
230-
break
231-
fi
232-
sleep 10
233-
done
204+
uses: actions/github-script@v6
205+
with:
206+
github-token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
207+
script: |
208+
const {verifyPublished, isLatest} = require('./.github/workflow-scripts/publishTemplate.js')
209+
const version = "${{ github.ref_name }}"
210+
await verifyPublished(version, isLatest());
234211
- name: Update rn-diff-purge to generate upgrade-support diff
235212
run: |
236213
curl -X POST https://api.github.com/repos/react-native-community/rn-diff-purge/dispatches \

0 commit comments

Comments
 (0)