You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
216 lines
7.8 KiB
216 lines
7.8 KiB
const path = require('path');
|
|
const fs = require('fs/promises');
|
|
const exec = require('util').promisify(require('child_process').exec);
|
|
const readline = require('readline/promises');
|
|
|
|
const semver = require('semver');
|
|
|
|
// Maps a package name to the config key in defaults.json
|
|
const CONFIG_KEYS = {
|
|
'@tryghost/portal': 'portal',
|
|
'@tryghost/sodo-search': 'sodoSearch',
|
|
'@tryghost/comments-ui': 'comments',
|
|
'@tryghost/announcement-bar': 'announcementBar',
|
|
'@tryghost/signup-form': 'signupForm'
|
|
};
|
|
|
|
const CURRENT_DIR = process.cwd();
|
|
|
|
const packageJsonPath = path.join(CURRENT_DIR, 'package.json');
|
|
const packageJson = require(packageJsonPath);
|
|
|
|
const APP_NAME = packageJson.name;
|
|
const APP_VERSION = packageJson.version;
|
|
|
|
async function safeExec(command) {
|
|
try {
|
|
return await exec(command);
|
|
} catch (err) {
|
|
return {
|
|
stdout: err.stdout,
|
|
stderr: err.stderr
|
|
};
|
|
}
|
|
}
|
|
|
|
async function ensureEnabledApp() {
|
|
const ENABLED_APPS = Object.keys(CONFIG_KEYS);
|
|
if (!ENABLED_APPS.includes(APP_NAME)) {
|
|
console.error(`${APP_NAME} is not enabled, please modify ${__filename}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function ensureNotOnMain() {
|
|
const currentGitBranch = await safeExec(`git branch --show-current`);
|
|
if (currentGitBranch.stderr) {
|
|
console.error(`There was an error checking the current git branch`)
|
|
console.error(`${currentGitBranch.stderr}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (currentGitBranch.stdout.trim() === 'main') {
|
|
console.error(`The release can not be done on the "main" branch`)
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function ensureCleanGit() {
|
|
const localGitChanges = await safeExec(`git status --porcelain`);
|
|
if (localGitChanges.stderr) {
|
|
console.error(`There was an error checking the local git status`)
|
|
console.error(`${localGitChanges.stderr}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (localGitChanges.stdout) {
|
|
console.error(`You have local git changes - are you sure you're ready to release?`)
|
|
console.error(`${localGitChanges.stdout}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function getNewVersion() {
|
|
const rl = readline.createInterface({input: process.stdin, output: process.stdout});
|
|
const bumpTypeInput = await rl.question('Is this a patch, minor or major (patch)? ');
|
|
rl.close();
|
|
const bumpType = bumpTypeInput.trim().toLowerCase() || 'patch';
|
|
if (!['patch', 'minor', 'major'].includes(bumpType)) {
|
|
console.error(`Unknown bump type ${bumpTypeInput} - expected one of "patch", "minor, "major"`)
|
|
process.exit(1);
|
|
}
|
|
return semver.inc(APP_VERSION, bumpType);
|
|
}
|
|
|
|
async function updateConfig(newVersion) {
|
|
const defaultConfigPath = path.resolve(__dirname, '../../ghost/core/core/shared/config/defaults.json');
|
|
const defaultConfig = require(defaultConfigPath);
|
|
|
|
const configKey = CONFIG_KEYS[APP_NAME];
|
|
|
|
defaultConfig[configKey].version = `${semver.major(newVersion)}.${semver.minor(newVersion)}`;
|
|
|
|
await fs.writeFile(defaultConfigPath, JSON.stringify(defaultConfig, null, 4) + '\n');
|
|
}
|
|
|
|
async function updatePackageJson(newVersion) {
|
|
const newPackageJson = Object.assign({}, packageJson, {
|
|
version: newVersion
|
|
});
|
|
|
|
await fs.writeFile(packageJsonPath, JSON.stringify(newPackageJson, null, 2) + '\n');
|
|
}
|
|
|
|
async function getChangelog(newVersion) {
|
|
const rl = readline.createInterface({input: process.stdin, output: process.stdout});
|
|
const i18nChangesInput = await rl.question('Does this release contain i18n updates (Y/n)? ');
|
|
rl.close();
|
|
|
|
const i18nChanges = i18nChangesInput.trim().toLowerCase() !== 'n';
|
|
|
|
let changelogItems = [];
|
|
|
|
if (i18nChanges) {
|
|
changelogItems.push('Updated i18n translations');
|
|
}
|
|
|
|
// Restrict git log to only the current directory (the specific app)
|
|
const lastFiftyCommits = await safeExec(`git log -n 50 --oneline -- .`);
|
|
|
|
if (lastFiftyCommits.stderr) {
|
|
console.error(`There was an error getting the last 50 commits`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const lastFiftyCommitsList = lastFiftyCommits.stdout.split('\n');
|
|
const releaseRegex = new RegExp(`Released ${APP_NAME} v${APP_VERSION}`);
|
|
const indexOfLastRelease = lastFiftyCommitsList.findIndex((commitLine) => {
|
|
const commitMessage = commitLine.slice(11); // Take the hash off the front
|
|
return releaseRegex.test(commitMessage);
|
|
});
|
|
|
|
if (indexOfLastRelease === -1) {
|
|
console.warn(`Could not find commit for previous release. Will include recent commits affecting this app.`);
|
|
|
|
// Fallback: get recent commits for this app (last 20)
|
|
const recentCommits = await safeExec(`git log -n 20 --pretty=format:"%h%n%B__SPLIT__" -- .`);
|
|
if (recentCommits.stderr) {
|
|
console.error(`There was an error getting recent commits`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const recentCommitsList = recentCommits.stdout.split('__SPLIT__');
|
|
|
|
const recentCommitsWhichMentionLinear = recentCommitsList.filter((commitBlock) => {
|
|
return commitBlock.includes('https://linear.app/ghost');
|
|
});
|
|
|
|
const commitChangelogItems = recentCommitsWhichMentionLinear.map((commitBlock) => {
|
|
const lines = commitBlock.split('\n');
|
|
if (!lines.length || !lines[0].trim()) {
|
|
return null; // Skip entries with no hash
|
|
}
|
|
const hash = lines[0].trim();
|
|
return `https://github.com/TryGhost/Ghost/commit/${hash}`;
|
|
}).filter(Boolean); // Filter out any null entries
|
|
|
|
changelogItems.push(...commitChangelogItems);
|
|
} else {
|
|
const lastReleaseCommit = lastFiftyCommitsList[indexOfLastRelease];
|
|
const lastReleaseCommitHash = lastReleaseCommit.slice(0, 10);
|
|
|
|
// Also restrict this git log to only the current directory (the specific app)
|
|
const commitsSinceLastRelease = await safeExec(`git log ${lastReleaseCommitHash}..HEAD --pretty=format:"%h%n%B__SPLIT__" -- .`);
|
|
if (commitsSinceLastRelease.stderr) {
|
|
console.error(`There was an error getting commits since the last release`);
|
|
process.exit(1);
|
|
}
|
|
const commitsSinceLastReleaseList = commitsSinceLastRelease.stdout.split('__SPLIT__');
|
|
|
|
const commitsSinceLastReleaseWhichMentionLinear = commitsSinceLastReleaseList.filter((commitBlock) => {
|
|
return commitBlock.includes('https://linear.app/ghost');
|
|
});
|
|
|
|
const commitChangelogItems = commitsSinceLastReleaseWhichMentionLinear.map((commitBlock) => {
|
|
const lines = commitBlock.split('\n');
|
|
if (!lines.length || !lines[0].trim()) {
|
|
return null; // Skip entries with no hash
|
|
}
|
|
const hash = lines[0].trim();
|
|
return `https://github.com/TryGhost/Ghost/commit/${hash}`;
|
|
}).filter(Boolean); // Filter out any null entries
|
|
|
|
changelogItems.push(...commitChangelogItems);
|
|
}
|
|
|
|
const changelogList = changelogItems.map(item => ` - ${item}`).join('\n');
|
|
return `Changelog for v${APP_VERSION} -> ${newVersion}: \n${changelogList}`;
|
|
}
|
|
|
|
async function main() {
|
|
await ensureEnabledApp();
|
|
await ensureNotOnMain();
|
|
await ensureCleanGit();
|
|
|
|
console.log(`Running release for ${APP_NAME}`);
|
|
console.log(`Current version is ${APP_VERSION}`);
|
|
|
|
const newVersion = await getNewVersion();
|
|
|
|
console.log(`Bumping to version ${newVersion}`);
|
|
|
|
const changelog = await getChangelog(newVersion);
|
|
|
|
await updatePackageJson(newVersion);
|
|
await exec(`git add package.json`);
|
|
|
|
await updateConfig(newVersion);
|
|
await exec(`git add ../../ghost/core/core/shared/config/defaults.json`);
|
|
|
|
await exec(`git commit -m 'Released ${APP_NAME} v${newVersion}\n\n${changelog}'`);
|
|
|
|
console.log(`Release commit created - please double check it and use "git commit --amend" to make any changes before opening a PR to merge into main`)
|
|
}
|
|
|
|
main();
|