Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ name: Publish to npm
# B) Legacy token: add NPM_AUTH_TOKEN (npm granular publish token) to repo secrets
#
# Release:
# 1. Bump version in package.json on master (9.x) or 7.x
# 2. git tag vX.Y.Z && git push origin vX.Y.Z
# Tag must match package.json version (e.g. v9.4.1)
# Or: Actions → Publish to npm → Run workflow (manual dispatch)
# 1. Pull request: bump package.json and CHANGELOG. Merge it to master.
# 2. Tag that merge commit and push the tag:
# git tag vX.Y.Z && git push origin vX.Y.Z
# The tag commit must already be on master. A tag on an open pull
# request fails before npm publish. Tag must match package.json
# (e.g. v9.5.2).
# Or: Actions → Publish to npm → Run workflow from master.
#
# After a 9.x minor (vX.Y.0):
# 3. Add .github/announcements/vX.Y.md (H1 title + `<!-- releases: vX.Y.0 -->`)
Expand All @@ -36,8 +39,27 @@ concurrency:
cancel-in-progress: false

jobs:
on-master:
name: Require commit on master
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0

- name: Fetch master
run: git fetch origin master

- name: Refuse publish unless this commit is on master
run: bash scripts/require-commit-on-master.sh

zip-interop:
name: Zip interop (Node unzipper + Java ZipInputStream)
needs: on-master
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
Expand Down Expand Up @@ -71,7 +93,7 @@ jobs:

publish:
name: Publish to npm
needs: zip-interop
needs: [on-master, zip-interop]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
Expand Down
62 changes: 62 additions & 0 deletions __tests__/require-commit-on-master.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const { execFileSync } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');

const script = path.join(__dirname, '..', 'scripts', 'require-commit-on-master.sh');

function git(cwd, args) {
return execFileSync('git', args, { cwd, encoding: 'utf8' }).trim();
}

function initRepo() {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'require-on-master-'));
git(cwd, ['init', '-b', 'master']);
git(cwd, ['config', 'user.email', 'test@example.com']);
git(cwd, ['config', 'user.name', 'test']);
fs.writeFileSync(path.join(cwd, 'package.json'), '{"version":"1.0.0"}\n');
git(cwd, ['add', 'package.json']);
git(cwd, ['commit', '-m', 'init']);
return cwd;
}

function run(cwd) {
try {
return execFileSync('bash', [script], {
cwd,
encoding: 'utf8',
env: { ...process.env, MASTER_REF: 'master' },
});
} catch (error) {
const output = `${error.stdout || ''}\n${error.stderr || ''}`;
error.message = `${error.message}\n${output}`;
throw error;
}
}

describe('require-commit-on-master', () => {
test('accepts a commit that is already on master', () => {
const cwd = initRepo();
const output = run(cwd);
expect(output).toMatch(/is on master/);
});

test('rejects a commit that exists only on an open branch', () => {
const cwd = initRepo();
git(cwd, ['checkout', '-b', 'release']);
fs.writeFileSync(path.join(cwd, 'package.json'), '{"version":"1.0.1"}\n');
git(cwd, ['commit', '-am', 'bump']);
expect(() => run(cwd)).toThrow(/not on master/);
});

test('accepts the merge commit after the branch lands on master', () => {
const cwd = initRepo();
git(cwd, ['checkout', '-b', 'release']);
fs.writeFileSync(path.join(cwd, 'package.json'), '{"version":"1.0.1"}\n');
git(cwd, ['commit', '-am', 'bump']);
git(cwd, ['checkout', 'master']);
git(cwd, ['merge', '--ff-only', 'release']);
const output = run(cwd);
expect(output).toMatch(/is on master/);
});
});
3 changes: 2 additions & 1 deletion __tests__/zip-interop.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ describe('zip interop gate (RNZA-16)', () => {
path.join(__dirname, '..', '.github', 'workflows', 'publish.yml'),
'utf8'
);
expect(yml).toMatch(/needs:\s*zip-interop/);
expect(yml).toMatch(/needs:\s*\[on-master,\s*zip-interop\]/);
expect(yml).toMatch(/needs:\s*on-master/);
expect(yml).toMatch(/verify-zip-interop\.js --fixtures/);
});

Expand Down
20 changes: 20 additions & 0 deletions scripts/require-commit-on-master.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Refuse to publish unless HEAD is already contained in master.
# Tag pushes check out the tag, so this must fetch origin/master first.
set -euo pipefail

REF="${MASTER_REF:-origin/master}"

if ! git rev-parse --verify --quiet "$REF" >/dev/null; then
echo "::error::Missing ref ${REF}. Fetch master before this check."
exit 1
fi

HEAD_SHA="$(git rev-parse HEAD)"
if git merge-base --is-ancestor HEAD "$REF"; then
echo "Commit ${HEAD_SHA} is on ${REF}."
exit 0
fi

echo "::error::Commit ${HEAD_SHA} is not on ${REF}. Merge the release pull request, then tag that merge commit. npm publish does not run from an open pull request."
exit 1
Loading