Files
orbit/scripts/release.mjs
Gabriel Donadel Dall'Agnol bed8c3039d Use EAS custom builds for macOS releases (#330)
* [eas] Update configureMacOSCredentials to install provisioning profile on setup

* [eas] Manually notarize app

* Update release script

* Add changelog entry
2026-04-06 16:55:11 +02:00

184 lines
5.4 KiB
JavaScript

#!/usr/bin/env node
import { readFileSync } from 'fs';
import { execSync } from 'child_process';
import { createInterface } from 'readline';
import { ROOT, MENU_BAR_PKG, CHANGELOG } from './constants.mjs';
function exec(cmd, opts = {}) {
return execSync(cmd, { cwd: ROOT, encoding: 'utf8', ...opts }).trim();
}
function ask(question) {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
function getCurrentVersion() {
const pkg = JSON.parse(readFileSync(MENU_BAR_PKG, 'utf8'));
return pkg.version;
}
function incrementVersion(current, bump) {
const [major, minor, patch] = current.split('.').map(Number);
switch (bump) {
case 'major':
return `${major + 1}.0.0`;
case 'minor':
return `${major}.${minor + 1}.0`;
case 'patch':
return `${major}.${minor}.${patch + 1}`;
default:
return null;
}
}
function validatePreconditions() {
const errors = [];
// Check branch
const branch = exec('git rev-parse --abbrev-ref HEAD');
if (branch !== 'main') {
errors.push(`Must be on 'main' branch (currently on '${branch}')`);
}
// Check clean working tree
const status = exec('git status --porcelain');
if (status) {
errors.push('Working tree is not clean. Commit or stash your changes first.');
}
// Check up to date with remote
try {
exec('git fetch origin main');
const local = exec('git rev-parse HEAD');
const remote = exec('git rev-parse origin/main');
if (local !== remote) {
errors.push('Local branch is not up to date with origin/main. Pull or push first.');
}
} catch {
errors.push('Could not fetch from origin. Check your network connection.');
}
// Check changelog has content
const changelog = readFileSync(CHANGELOG, 'utf8');
const unpublishedIdx = changelog.indexOf('## Unpublished');
if (unpublishedIdx === -1) {
errors.push('Could not find "## Unpublished" section in CHANGELOG.md');
} else {
const nextSection = changelog.indexOf('\n## ', unpublishedIdx + '## Unpublished'.length);
const unpublishedContent =
nextSection === -1
? changelog.slice(unpublishedIdx + '## Unpublished'.length)
: changelog.slice(unpublishedIdx + '## Unpublished'.length, nextSection);
const hasBullets = unpublishedContent.includes('\n- ');
if (!hasBullets) {
errors.push('No changes found in the Unpublished section of CHANGELOG.md');
}
}
return errors;
}
async function main() {
console.log('Expo Orbit Release\n');
const currentVersion = getCurrentVersion();
console.log(`Current version: ${currentVersion}\n`);
// Prompt for version
const input = await ask('New version (X.Y.Z, or patch/minor/major): ');
let newVersion = incrementVersion(currentVersion, input) || input;
if (!/^\d+\.\d+\.\d+$/.test(newVersion)) {
console.error(`Invalid version: ${newVersion}`);
process.exit(1);
}
console.log(`\nWill release: ${currentVersion} -> ${newVersion}\n`);
// Validate preconditions
console.log('Checking preconditions...');
const errors = validatePreconditions();
if (errors.length > 0) {
console.error('\nPrecondition checks failed:');
for (const err of errors) {
console.error(` - ${err}`);
}
process.exit(1);
}
console.log(' All checks passed.\n');
// Check tag doesn't already exist
const tag = `expo-orbit-v${newVersion}`;
try {
exec(`git rev-parse ${tag}`, { stdio: ['pipe', 'pipe', 'ignore'] });
console.error(`Tag ${tag} already exists.`);
process.exit(1);
} catch {
// Tag doesn't exist, good
}
// Bump version
console.log('Bumping versions...');
const bumpOutput = exec(`node scripts/bump-version.mjs --version ${newVersion}`);
console.log(bumpOutput);
// Show diff
console.log('\nChanges:');
console.log(exec('git diff --stat'));
console.log();
const confirm = await ask('Proceed with commit, tag, and push? (y/N): ');
if (confirm.toLowerCase() !== 'y') {
console.log('\nAborted. Reverting changes...');
exec('git checkout -- .');
process.exit(0);
}
// Stage and commit
const filesToStage = [
'apps/menu-bar/package.json',
'apps/menu-bar/electron/package.json',
'apps/menu-bar/macos/ExpoMenuBar-macOS/Info.plist',
'CHANGELOG.md',
];
exec(`git add ${filesToStage.join(' ')}`);
exec(`git commit -m "[menu-bar] Bump version to ${newVersion}"`);
console.log(`\nCommitted: [menu-bar] Bump version to ${newVersion}`);
// Tag
exec(`git tag ${tag}`);
console.log(`Tagged: ${tag}`);
// Push
console.log('\nPushing to origin...');
exec('git push origin main');
exec(`git push origin ${tag}`);
console.log('Pushed branch and tag.\n');
// Summary
console.log('='.repeat(60));
console.log('Release started successfully!\n');
console.log('CI will now:');
console.log(' - Build Linux/Windows artifacts');
console.log(' - Build and notarize macOS via EAS');
console.log(' - Create a draft GitHub Release\n');
console.log('Next steps:');
console.log(' 1. Download the notarized macOS zip from EAS and upload to the draft GitHub Release');
console.log(' 2. Publish the GitHub Release');
console.log(` 3. Run: yarn release:metadata`);
console.log('='.repeat(60));
}
main().catch((err) => {
console.error(err);
process.exit(1);
});