|
|
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'); // 用于语义化版本号处理
|
|
|
|
|
|
// 映射包名到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(); // 当前工作目录
|
|
|
|
|
|
// 读取当前项目的package.json路径和内容
|
|
|
const packageJsonPath = path.join(CURRENT_DIR, 'package.json');
|
|
|
const packageJson = require(packageJsonPath);
|
|
|
|
|
|
// 从package.json中提取应用名称和当前版本
|
|
|
const APP_NAME = packageJson.name;
|
|
|
const APP_VERSION = packageJson.version;
|
|
|
|
|
|
/**
|
|
|
* 安全执行命令的工具函数
|
|
|
* 捕获错误并返回包含stdout和stderr的对象(避免进程直接崩溃)
|
|
|
*/
|
|
|
async function safeExec(command) {
|
|
|
try {
|
|
|
return await exec(command);
|
|
|
} catch (err) {
|
|
|
return {
|
|
|
stdout: err.stdout,
|
|
|
stderr: err.stderr
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 确保当前应用在允许发布的列表中
|
|
|
* 检查APP_NAME是否存在于CONFIG_KEYS的键名中
|
|
|
*/
|
|
|
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); // 非允许的应用,退出进程
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 确保不在main分支上执行发布
|
|
|
* 防止直接在主分支上修改代码
|
|
|
*/
|
|
|
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); // 在main分支上,退出进程
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 确保当前git工作区干净(无未提交的更改)
|
|
|
* 避免发布时包含未跟踪的修改
|
|
|
*/
|
|
|
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); // 有未提交的更改,退出进程
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取用户输入的版本更新类型(patch/minor/major)并计算新版本号
|
|
|
* 默认使用patch更新
|
|
|
*/
|
|
|
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); // 使用semver计算新版本号
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 更新全局配置文件中的应用版本
|
|
|
* 只保留主版本号和次版本号(如1.2.3 → 1.2)
|
|
|
*/
|
|
|
async function updateConfig(newVersion) {
|
|
|
// 定位全局配置文件defaults.json
|
|
|
const defaultConfigPath = path.resolve(__dirname, '../../ghost/core/core/shared/config/defaults.json');
|
|
|
const defaultConfig = require(defaultConfigPath);
|
|
|
|
|
|
// 获取当前应用在配置文件中的键名
|
|
|
const configKey = CONFIG_KEYS[APP_NAME];
|
|
|
|
|
|
// 更新配置中的版本(只保留major和minor)
|
|
|
defaultConfig[configKey].version = `${semver.major(newVersion)}.${semver.minor(newVersion)}`;
|
|
|
|
|
|
// 写回配置文件(保留4空格缩进)
|
|
|
await fs.writeFile(defaultConfigPath, JSON.stringify(defaultConfig, null, 4) + '\n');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 更新当前应用的package.json版本号
|
|
|
*/
|
|
|
async function updatePackageJson(newVersion) {
|
|
|
const newPackageJson = Object.assign({}, packageJson, {
|
|
|
version: newVersion
|
|
|
});
|
|
|
|
|
|
// 写回package.json(保留2空格缩进)
|
|
|
await fs.writeFile(packageJsonPath, JSON.stringify(newPackageJson, null, 2) + '\n');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 生成更新日志(Changelog)
|
|
|
* 包含i18n更新信息和相关的提交记录
|
|
|
*/
|
|
|
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 = [];
|
|
|
|
|
|
// 如果有i18n更新,添加到更新日志
|
|
|
if (i18nChanges) {
|
|
|
changelogItems.push('Updated i18n translations');
|
|
|
}
|
|
|
|
|
|
// 获取当前应用目录下最近50条提交记录(限制在当前目录)
|
|
|
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); // 移除哈希前缀
|
|
|
return releaseRegex.test(commitMessage);
|
|
|
});
|
|
|
|
|
|
if (indexOfLastRelease === -1) {
|
|
|
// 未找到上一次发布记录,使用最近20条相关提交作为 fallback
|
|
|
console.warn(`Could not find commit for previous release. Will include recent commits affecting this app.`);
|
|
|
|
|
|
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__');
|
|
|
|
|
|
// 筛选包含Linear链接的提交(通常是已处理的任务)
|
|
|
const recentCommitsWhichMentionLinear = recentCommitsList.filter((commitBlock) => {
|
|
|
return commitBlock.includes('https://linear.app/ghost');
|
|
|
});
|
|
|
|
|
|
// 生成提交记录的GitHub链接
|
|
|
const commitChangelogItems = recentCommitsWhichMentionLinear.map((commitBlock) => {
|
|
|
const lines = commitBlock.split('\n');
|
|
|
if (!lines.length || !lines[0].trim()) {
|
|
|
return null; // 跳过无效条目
|
|
|
}
|
|
|
const hash = lines[0].trim();
|
|
|
return `https://github.com/TryGhost/Ghost/commit/${hash}`;
|
|
|
}).filter(Boolean); // 过滤空值
|
|
|
|
|
|
changelogItems.push(...commitChangelogItems);
|
|
|
} else {
|
|
|
// 找到上一次发布记录,获取两次发布之间的提交
|
|
|
const lastReleaseCommit = lastFiftyCommitsList[indexOfLastRelease];
|
|
|
const lastReleaseCommitHash = lastReleaseCommit.slice(0, 10); // 提取提交哈希
|
|
|
|
|
|
// 获取从上次发布到现在的提交(限制在当前目录)
|
|
|
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__');
|
|
|
|
|
|
// 筛选包含Linear链接的提交
|
|
|
const commitsSinceLastReleaseWhichMentionLinear = commitsSinceLastReleaseList.filter((commitBlock) => {
|
|
|
return commitBlock.includes('https://linear.app/ghost');
|
|
|
});
|
|
|
|
|
|
// 生成提交记录的GitHub链接
|
|
|
const commitChangelogItems = commitsSinceLastReleaseWhichMentionLinear.map((commitBlock) => {
|
|
|
const lines = commitBlock.split('\n');
|
|
|
if (!lines.length || !lines[0].trim()) {
|
|
|
return null; // 跳过无效条目
|
|
|
}
|
|
|
const hash = lines[0].trim();
|
|
|
return `https://github.com/TryGhost/Ghost/commit/${hash}`;
|
|
|
}).filter(Boolean); // 过滤空值
|
|
|
|
|
|
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(); // 检查是否不在main分支
|
|
|
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);
|
|
|
|
|
|
// 更新package.json并提交
|
|
|
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}'`);
|
|
|
|
|
|
// 提示用户检查并提交PR
|
|
|
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(); |