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.
ghost/.github/scripts/release-apps.js

275 lines
10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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();