Skip to content

Commit

Permalink
Replace sh scripts with tested JS scripts to release template (#46363)
Browse files Browse the repository at this point in the history
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
blakef committed Oct 15, 2024
1 parent 02b879b commit 45ae0b4
Show file tree
Hide file tree
Showing 4 changed files with 305 additions and 38 deletions.
158 changes: 158 additions & 0 deletions .github/workflow-scripts/__tests__/publishTemplate-test.js
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);
});
});
});
103 changes: 103 additions & 0 deletions .github/workflow-scripts/publishTemplate.js
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);
};
29 changes: 29 additions & 0 deletions .github/workflow-scripts/utils.js
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);
53 changes: 15 additions & 38 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,46 +191,23 @@ jobs:
gha-npm-token: ${{ env.GHA_NPM_TOKEN }}
- name: Publish @react-native-community/template
id: publish-template-to-npm
shell: bash
run: |
COMMIT_MSG=$(git log -n1 --pretty=%B);
if grep -q '#publish-packages-to-npm&latest' <<< "$COMMIT_MSG"; then
echo "TAG=latest" >> $GITHUB_OUTPUT
IS_LATEST=true
else
IS_LATEST=false
fi
# Go from v0.75.0-rc.4 -> 0.75-stable, which is the template's branching scheme
VERSION=$(grep -oE '\d+\.\d+' <<< "${{ github.ref_name }}" | { read version; echo "$version-stable"; })
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
curl -L https://api.github.com/repos/react-native-community/template/actions/workflows/release.yaml/dispatches
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: Bearer $REACT_NATIVE_BOT_GITHUB_TOKEN" \
-d "{\"ref\":\"$VERSION\",\"inputs\":{\"version\":\"${{ github.ref_name }}\",\"is_latest_on_npm\":\"$IS_LATEST\"}}"
uses: actions/github-script@v6
with:
github-token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
script: |
const {publishTemplate} = require('./.github/workflow-scripts/publishTemplate.js')
const version = "${{ github.ref_name }}"
const isDryRun = false
await publishTemplate(github, version, isDryRun);
- name: Wait for template to be published
timeout-minutes: 3
env:
VERSION: ${{ steps.publish-template-to-npm.outputs.VERSION }}
TAG: ${{ steps.publish-template-to-npm.outputs.TAG }}
shell: bash
run: |
echo "Waiting until @react-native-community/template is published to npm"
while true; do
if curl -o /dev/null -s -f "https://registry.npmjs.org/@react-native-community/template/$VERSION"; then
echo "Confirm that @react-native-community/template@$VERSION is published on npm"
break
fi
sleep 10
done
while [ "$TAG" == "latest" ]; do
CURRENT=$(curl -s "https://registry.npmjs.org/react-native/latest" | jq -r '.version');
if [ "$CURRENT" == "$VERSION" ]; then
echo "Confirm that @react-native-community/template@latest == $VERSION on npm"
break
fi
sleep 10
done
uses: actions/github-script@v6
with:
github-token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
script: |
const {verifyPublished, isLatest} = require('./.github/workflow-scripts/publishTemplate.js')
const version = "${{ github.ref_name }}"
await verifyPublished(version, isLatest());
- name: Update rn-diff-purge to generate upgrade-support diff
run: |
curl -X POST https://api.github.com/repos/react-native-community/rn-diff-purge/dispatches \
Expand Down

0 comments on commit 45ae0b4

Please sign in to comment.