Skip to content

Commit d6ad6c1

Browse files
committed
Add automated release scripts (#3784)
## Description Implements automated release scripts for the repository. A new nightly release should be triggered on every commit pushed to the `main` branch. Version resolution happens automatically - the `latest` tag is pulled from npm, 1 is added to the minor, and the patch is set to 0. The full version is `$major.$minor$.$patch-$year$month$day-$sha`. To release a stable version, cut a branch with the name matching format of `$major.$minor-stable` and trigger the `publish-stable-release` on that branch. By default, it does a dry run, skipping publishing the package and committing changes. Use that to make sure everything is correct before an actual release. It also uploads the package that would've been published as a GitHub artifact, which could be useful for testing. Version resolution happens automatically: - the major and minor are inferred from the branch name - next patch name is figured out from npm, in the dumbest possible way - iteratively check the patch versions until a not-released one is found - if the minor is exactly +1 compared to the latest and the patch is 0 or the minor matches the latest and the patch is +1, the release is tagged as latest In the future, we may want to run static checks automatically and app builds before releasing, but that would require refactoring our CI to rely more on reusable workflows. ## Test plan Tested on a fork. Release commitly run: https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/18902148546/job/53951686147 (failed on publish due to missing provenance) Release stable run for existing version: https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/18902287389/job/53952137361 (this one succeeded because I commented out `npm publish` and replaced it with `echo`) And here is the created release commit: j-piasecki@211296a. You can notice that the tag was also created successfully. Release stable run for a new version: https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/18903154587/job/53954932559 (this one was a dry run).
1 parent 77be5a1 commit d6ad6c1

File tree

9 files changed

+340
-56
lines changed

9 files changed

+340
-56
lines changed

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"ignorePatterns": [
2121
"packages/react-native-gesture-handler/lib/**/*",
2222
"**/*.config.js",
23-
"scripts/*.js",
23+
"scripts/**/*.js",
2424
"**/node_modules/**/*"
2525
],
2626
"rules": {
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
name: publish-npm-package
2+
description: Build and publish react-native-gesture-handler package to npm
3+
inputs:
4+
is-commitly:
5+
description: Whether the release should be marked as nightly.
6+
default: "true"
7+
dry-run:
8+
description: Whether to perform a dry run of the publish.
9+
default: "true"
10+
11+
runs:
12+
using: composite
13+
steps:
14+
- name: Set up environment
15+
shell: bash
16+
run: |
17+
echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
18+
echo "PACKAGE_NAME=PLACEHOLDER" >> $GITHUB_ENV
19+
20+
- name: Setup Node
21+
uses: actions/setup-node@v6
22+
with:
23+
node-version: 24
24+
cache: 'yarn'
25+
registry-url: https://registry.npmjs.org/
26+
27+
- name: Update package version
28+
id: set-package-version
29+
shell: bash
30+
run: |
31+
VERSION=$(node ./scripts/release/set-package-version.js ${{ inputs.is-commitly == 'true' && '--commitly' || '' }})
32+
echo "Updated package version to $VERSION"
33+
echo "version=$VERSION" >> $GITHUB_OUTPUT
34+
35+
# Ensure npm 11.5.1 or later is installed for OIDC
36+
- name: Update npm
37+
shell: bash
38+
run: npm install -g npm@latest
39+
40+
- name: Install node dependencies
41+
shell: bash
42+
run: yarn install --immutable
43+
44+
- name: Build package
45+
id: build
46+
working-directory: packages/react-native-gesture-handler
47+
shell: bash
48+
run: npm pack
49+
50+
- name: Add package name to env
51+
working-directory: packages/react-native-gesture-handler
52+
shell: bash
53+
run: echo "PACKAGE_NAME=$(ls -l | egrep -o "react-native-gesture-handler-(.*)(=?\.tgz)")" >> $GITHUB_ENV
54+
55+
- name: Assert PACKAGE_NAME
56+
if: ${{ env.PACKAGE_NAME == 'PLACEHOLDER' }}
57+
shell: bash
58+
run: exit 1 # If we end up here, it means that package was not generated.
59+
60+
- name: Upload npm package to GitHub
61+
uses: actions/upload-artifact@v4
62+
with:
63+
name: ${{ env.PACKAGE_NAME }}
64+
path: './packages/react-native-gesture-handler/${{ env.PACKAGE_NAME }}'
65+
66+
- name: Figure out the correct npm tag
67+
id: figure-out-npm-tag
68+
shell: bash
69+
run: |
70+
if [ "${{ inputs.is-commitly }}" = "true" ]; then
71+
TAG_ARGUMENT="--tag nightly"
72+
else
73+
if [ $(node ./scripts/release/should-be-latest.js ${{ steps.set-package-version.outputs.version }}) = "true" ]; then
74+
TAG_ARGUMENT="--tag latest"
75+
else
76+
TAG_ARGUMENT=""
77+
fi
78+
fi
79+
80+
echo "Determined tag argument: $TAG_ARGUMENT"
81+
echo "tag-argument=$TAG_ARGUMENT" >> $GITHUB_OUTPUT
82+
83+
- name: Print outputs
84+
shell: bash
85+
run: |
86+
echo "Version to be published: ${{ steps.set-package-version.outputs.version }}"
87+
echo "NPM tag argument: ${{ steps.figure-out-npm-tag.outputs.tag-argument }}"
88+
89+
- name: Publish npm package
90+
shell: bash
91+
if: inputs.dry-run == 'false'
92+
working-directory: packages/react-native-gesture-handler
93+
run: |
94+
npm publish $PACKAGE_NAME --provenance ${{ steps.figure-out-npm-tag.outputs.tag-argument }}
95+
96+
- name: Don't publish if dry-run is true
97+
shell: bash
98+
if: inputs.dry-run == 'true'
99+
run: |
100+
echo "At this point, the following command would have been run: npm publish $PACKAGE_NAME --provenance ${{ steps.figure-out-npm-tag.outputs.tag-argument }}"
101+
102+
- name: Configure git
103+
if: inputs.is-commitly == 'false'
104+
shell: bash
105+
run: |
106+
git config --local user.email "action@github.com"
107+
git config --local user.name "GitHub Action"
108+
109+
- name: Create release commit
110+
if: inputs.is-commitly == 'false'
111+
shell: bash
112+
run: |
113+
git add packages/react-native-gesture-handler/package.json
114+
git commit -m "Release v${{ steps.set-package-version.outputs.version }}"
115+
116+
if [ "${{ inputs.dry-run }}" = 'false' ]; then
117+
echo "Pushing release commit to origin..."
118+
git push
119+
else
120+
echo "Dry run mode: skipping git push"
121+
fi
122+
123+
- name: Create release tag
124+
if: inputs.is-commitly == 'false'
125+
shell: bash
126+
run: |
127+
git tag -a "v${{ steps.set-package-version.outputs.version }}" -m "Release v${{ steps.set-package-version.outputs.version }}"
128+
129+
if [ "${{ inputs.dry-run }}" = 'false' ]; then
130+
echo "Pushing tag to origin..."
131+
git push origin "v${{ steps.set-package-version.outputs.version }}"
132+
else
133+
echo "Dry run mode: skipping git push"
134+
fi

.github/workflows/npm-gesture-handler-publish.yml

Lines changed: 0 additions & 55 deletions
This file was deleted.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Publish commitly release to npm
2+
on:
3+
push:
4+
branches:
5+
- main
6+
7+
jobs:
8+
npm-build:
9+
if: github.repository == 'software-mansion/react-native-gesture-handler'
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write
13+
id-token: write # for OIDC
14+
steps:
15+
- name: Check out
16+
uses: actions/checkout@v4
17+
18+
- name: Publish commitly release
19+
uses: ./.github/actions/publish-npm-package
20+
with:
21+
is-commitly: true
22+
dry-run: false
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Publish release to npm
2+
on:
3+
# For commitlies
4+
push:
5+
branches:
6+
- main
7+
# For stable releases
8+
workflow_dispatch:
9+
inputs:
10+
dry-run:
11+
description: Whether to perform a dry run of the publish.
12+
type: boolean
13+
default: true
14+
15+
jobs:
16+
npm-build:
17+
if: github.repository == 'software-mansion/react-native-gesture-handler'
18+
runs-on: ubuntu-latest
19+
20+
permissions:
21+
contents: write
22+
id-token: write # for OIDC
23+
24+
concurrency:
25+
group: publish-${{ github.ref }}
26+
cancel-in-progress: false
27+
28+
steps:
29+
- name: Check out
30+
uses: actions/checkout@v4
31+
32+
- name: Publish stable release
33+
if: ${{ github.event_name == 'workflow_dispatch' }}
34+
uses: ./.github/actions/publish-npm-package
35+
with:
36+
is-commitly: false
37+
dry-run: ${{ inputs.dry-run }}
38+
39+
- name: Publish commitly release
40+
if: ${{ github.event_name == 'push' }}
41+
uses: ./.github/actions/publish-npm-package
42+
with:
43+
is-commitly: true
44+
dry-run: false

scripts/release/npm-utils.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const { execSync } = require('child_process');
2+
3+
function getPackageVersionByTag(packageName, tag) {
4+
const npmString =
5+
tag != null
6+
? `npm view ${packageName}@${tag} version`
7+
: `npm view ${packageName} version`;
8+
9+
try {
10+
const result = execSync(npmString, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
11+
return result;
12+
} catch (error) {
13+
throw new Error(`Failed to get package version for ${packageName} by tag: ${tag}`);
14+
}
15+
}
16+
17+
module.exports = {
18+
getPackageVersionByTag,
19+
};
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const fs = require('fs');
2+
const { execSync } = require('child_process');
3+
const { getPackageVersionByTag } = require('./npm-utils');
4+
const { parseVersion, getStableBranchVersion } = require('./version-utils');
5+
6+
const PACKAGE_PATH = './packages/react-native-gesture-handler/package.json';
7+
8+
function getLatestVersion() {
9+
const latestVersion = getPackageVersionByTag('react-native-gesture-handler', 'latest');
10+
11+
try {
12+
return parseVersion(latestVersion);
13+
} catch (error) {
14+
throw new Error(`Failed to parse latest version: ${latestVersion}`);
15+
}
16+
}
17+
18+
function getNextStableVersion() {
19+
const [major, minor] = getStableBranchVersion();
20+
21+
// TODO: We'll worry about 3.x.x later :)
22+
if (major !== 2) {
23+
throw new Error(`Expected major version to be 2, but got ${major}`);
24+
}
25+
26+
let nextPatch = 0;
27+
while (true) {
28+
const version = `${major}.${minor}.${nextPatch}`;
29+
30+
try {
31+
// if the version is already published, increment the patch version and try again
32+
getPackageVersionByTag('react-native-gesture-handler', version);
33+
nextPatch++;
34+
} catch (error) {
35+
return [Number(major), Number(minor), nextPatch];
36+
}
37+
}
38+
}
39+
40+
function getVersion(isCommitly) {
41+
if (isCommitly) {
42+
const [major, minor] = getLatestVersion()
43+
44+
const currentSHA = execSync('git rev-parse HEAD').toString().trim();
45+
const now = new Date();
46+
const year = now.getFullYear();
47+
const month = String(now.getMonth() + 1).padStart(2, '0');
48+
const day = String(now.getDate()).padStart(2, '0');
49+
const currentDate = `${year}${month}${day}`;
50+
51+
const commitlyVersion = `${major}.${minor + 1}.${0}-${currentDate}-${currentSHA.slice(0, 9)}`;
52+
return commitlyVersion;
53+
}
54+
55+
const [major, minor, patch] = getNextStableVersion();
56+
return `${major}.${minor}.${patch}`;
57+
}
58+
59+
function setPackageVersion() {
60+
const isCommitly = process.argv.includes('--commitly');
61+
const version = getVersion(isCommitly);
62+
63+
const packageJson = JSON.parse(fs.readFileSync(PACKAGE_PATH, 'utf8'));
64+
packageJson.version = version;
65+
fs.writeFileSync(PACKAGE_PATH, JSON.stringify(packageJson, null, 2));
66+
67+
// Intentional, this is consumed by the action
68+
console.log(version);
69+
}
70+
71+
setPackageVersion();
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const { getPackageVersionByTag } = require('./npm-utils');
2+
const { parseVersion } = require('./version-utils');
3+
4+
function shouldBeLatest(version) {
5+
const latestVersion = getPackageVersionByTag('react-native-gesture-handler', 'latest');
6+
const [major, minor, patch] = parseVersion(latestVersion);
7+
const [newMajor, newMinor, newPatch] = parseVersion(version);
8+
9+
// TODO: We'll worry about 3.x.x later :)
10+
if (newMajor !== major) {
11+
throw new Error(`Expected major version to be ${major}, but got ${newMajor}`);
12+
}
13+
14+
return (newMajor === major && newMinor === minor && newPatch === patch + 1) ||
15+
(newMajor === major && newMinor === minor + 1 && newPatch === 0);
16+
}
17+
18+
if (require.main === module) {
19+
const version = process.argv[2];
20+
console.log(shouldBeLatest(version));
21+
}
22+
23+
module.exports = {
24+
shouldBeLatest,
25+
};

0 commit comments

Comments
 (0)