raven-pensieve_obsidian-ace.../scripts/version-bump.mjs
RavenHogWarts 358da40a8d
ci: 更新贡献和发版流程 (#29)
* feat(ci): 添加 PR 专用构工作流并增加安全检查

新增 .github/workflows/pr-build.yml,提供 PR 提交时的专用构建流程,
并在工作流中加入严格的安全检查逻辑以防止来自外部仓库的任意构建。

主要变更:
- 新增 PR Build 工作流(name: PR Build),在 pull_request_target 触发
 (types: opened, synchronize, reopened, labeled)。
- 增加权限配置(contents/read, pull-requests/write, checks/write,
  statuses/write)并设置 PLUGIN_ID 环境变量。
- 添加 security-check 作业:
  - 判断事件类型并检测 PR 来源(内部仓库或外部仓库)。
  - 对 labeled 事件仅在加入 safe-to-build 标签时允许构建。
  - 输出两个标志:is-safe 和 should-build,控制后续是否构建。
- 添加 build 作业(依赖 security-check):
  - 仅在 should-build == 'true' 时运行。
  - 检出 PR 分支代码(ref 为 PR head sha),设置 Node.js,安装依赖并执行构建。
  - 打包构建产物为 zip 并上传为 artifact(保留 7 天)。
  - 在 PR 上发表评论,包含构建信息、触发原因及下载链接(根据事件类型区分
    labeled 与其他触发)。

变更原因:
- 避免直接对外部 PR 自动构建可能带来的安全风险,通过 safe-to-build
  标签和来源检测来显式授权构建。
- 为 PR 提供可下载的构建产物与构建通知,方便审查与测试。

* feat(ci):除自动生成 Changelog 的Hub Action 步骤删除 release workflow 中 "Generate Changelog 步骤,除针对

中两版 CHANGE 的提取、标题级别判断awk 提取逻以及安装
的拼接。这样做是因为该步骤过于复杂且在当前流程中不再需要,
减少 CI 配置复杂度并避免在发布流程中因 changelog 提取失败导致
构建中断。

* chore(scripts): 移除中文 changelog 脚本调用并同步脚本路径

- 从 package.json 中移除对 CHANGELOG-zh.md 的 conventional-changelog
  命令调用,合并只生成英文 CHANGELOG。
- 更新 changelog:u 与 changelog:all 脚本,去掉对中文选项脚本的依赖,
  避免在无中文文件或脚本时导致发布流程失败。
- 同步变更记录语言为简体中文格式(仅修改 CHANGELOG.md 内容为中文翻译),
  以便发布说明更适配中文用户阅读。

这样做可以简化发布脚本依赖,减少发布时因缺少中文文件或脚本
引发的错误,同时保证主要 changelog 正常生成。

* feat(release): 增强发布脚本的健壮性与交互选项

- scripts/release-tag.mjs:
  - 添加 safeExec 封装执行命令并提供重试/跳过逻辑,
    避免重复执行导致脚本中断。
  - 添加 hasUncommittedChanges、tagExists、remoteTagExists
    等辅助函数,用于在提交、打标签和推送前做检查。
  - 在无变更时跳过 commit;在本地/远程已存在标签时跳过
    创建或推送标签;对 push 操作允许可跳过错误。
  - 发布失败时打印重试提示,整体输出更友好并更具容错性。
- scripts/version-bump.mjs:
  - 修复 parseVersion 返回对象的格式(补上逗号)并去掉多余空白。
  - 添加对“第一次发布 (1.0.0)”的特殊处理,调整交互选项
    显示顺序与编号;根据是否为第一次发布或 beta 状态
    动态显示可选项数量(1-5 或 1-6),并正确显示 beta
    的下一个编号。

这些改动提升了发布流程的安全性与可用性,减少手动
干预并避免重复操作带来的失败。
2025-08-13 00:11:24 +08:00

328 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
import { existsSync, readFileSync, writeFileSync } from "fs";
import readline from "readline";
// 检查是否为直接调用模式
const directVersion = process.argv[2];
const isDirectMode = !!directVersion;
// 如果是直接调用模式,直接更新版本
if (isDirectMode) {
updateAllFiles(directVersion);
process.exit(0);
}
// 否则,启动交互式模式
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// 获取当前版本
const packageJson = JSON.parse(readFileSync("package.json", "utf8"));
const currentVersion = packageJson.version;
console.log(`当前版本: ${currentVersion}`);
// 解析版本号 - 支持beta版本
function parseVersion(version) {
const betaRegex = /^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?$/;
const match = version.match(betaRegex);
if (!match) {
throw new Error(`无效的版本格式: ${version}`);
}
return {
major: parseInt(match[1], 10),
minor: parseInt(match[2], 10),
patch: parseInt(match[3], 10),
betaNumber: match[4] ? parseInt(match[4], 10) : null,
isBeta: !!match[4],
};
}
const versionInfo = parseVersion(currentVersion);
const { major, minor, patch, betaNumber, isBeta } = versionInfo;
// 构建基础版本号不包含beta后缀
const baseVersion = `${major}.${minor}.${patch}`;
// 检查是否为第一次发布版本 (1.0.0)
const isFirstRelease = currentVersion === "1.0.0";
// 显示选项
console.log("\n请选择版本更新类型:");
if (isFirstRelease) {
// 第一次发布版本的特殊选项
console.log(`1. 第一次发布 (保持 ${currentVersion})`);
console.log(`2. 主版本 (${major + 1}.0.0)`);
console.log(`3. 次版本 (${major}.${minor + 1}.0)`);
console.log(`4. 补丁版本 (${major}.${minor}.${patch + 1})`);
console.log(`5. 自定义版本`);
console.log(`6. Beta 版本 (${baseVersion}-beta.1)`);
} else {
// 正常的版本选项
console.log(`1. 主版本 (${major + 1}.0.0)`);
console.log(`2. 次版本 (${major}.${minor + 1}.0)`);
console.log(`3. 补丁版本 (${major}.${minor}.${patch + 1})`);
console.log(`4. 自定义版本`);
// Beta版本选项显示逻辑
if (isBeta) {
// 当前是beta版本显示下一个beta版本
console.log(`5. Beta 版本 (${baseVersion}-beta.${betaNumber + 1})`);
} else {
// 当前是正式版本显示第一个beta版本
console.log(`5. Beta 版本 (${baseVersion}-beta.1)`);
}
}
rl.question(`\n请输入选项 (1-${isFirstRelease ? "6" : "5"}): `, (answer) => {
let newVersion;
let isNewBeta = false;
if (isFirstRelease) {
// 第一次发布版本的选项处理
switch (answer) {
case "1":
// 第一次发布,保持当前版本
newVersion = currentVersion;
console.log(`\n选择第一次发布,保持版本 ${currentVersion}`);
break;
case "2":
newVersion = `${major + 1}.0.0`;
break;
case "3":
newVersion = `${major}.${minor + 1}.0`;
break;
case "4":
newVersion = `${major}.${minor}.${patch + 1}`;
break;
case "5":
rl.question(
"请输入自定义版本号 (x.y.z 或 x.y.z-beta.n): ",
(customVersion) => {
try {
const customVersionInfo =
parseVersion(customVersion);
isNewBeta = customVersionInfo.isBeta;
updateAllFiles(customVersion, isNewBeta);
} catch (error) {
console.error("版本格式错误:", error.message);
process.exit(1);
}
rl.close();
}
);
return;
case "6":
newVersion = `${baseVersion}-beta.1`;
isNewBeta = true;
break;
default:
console.log("无效选项,使用第一次发布选项");
newVersion = currentVersion;
}
} else {
// 正常版本的选项处理
switch (answer) {
case "1":
newVersion = `${major + 1}.0.0`;
break;
case "2":
newVersion = `${major}.${minor + 1}.0`;
break;
case "3":
newVersion = `${major}.${minor}.${patch + 1}`;
break;
case "4":
rl.question(
"请输入自定义版本号 (x.y.z 或 x.y.z-beta.n): ",
(customVersion) => {
try {
const customVersionInfo =
parseVersion(customVersion);
isNewBeta = customVersionInfo.isBeta;
updateAllFiles(customVersion, isNewBeta);
} catch (error) {
console.error("版本格式错误:", error.message);
process.exit(1);
}
rl.close();
}
);
return;
case "5":
if (isBeta) {
// 当前是beta版本只增加beta数字
newVersion = `${baseVersion}-beta.${betaNumber + 1}`;
} else {
// 当前是正式版本创建第一个beta版本
newVersion = `${baseVersion}-beta.1`;
}
isNewBeta = true;
break;
default:
console.log("无效选项,使用补丁版本更新");
newVersion = `${major}.${minor}.${patch + 1}`;
}
}
updateAllFiles(newVersion, isNewBeta);
rl.close();
});
/**
* 更新所有相关文件的版本号
* @param {string} version 新版本号
* @param {boolean} isBeta 是否为Beta版本
*/
function updateAllFiles(version, isBeta = false) {
try {
const isFirstRelease = version === "1.0.0";
if (isFirstRelease) {
console.log(`\n正在准备第一次发布版本 ${version}...`);
} else {
console.log(`\n正在更新至版本 ${version}...`);
}
// 1. 更新 package.json
updatePackageJson(version);
// 2. 更新 manifest.json 或 manifest-beta.json
const minAppVersion = updateManifestJson(version, isBeta);
// 3. 更新 versions.json
updateVersionsJson(version, minAppVersion);
// 提示提交更改
if (isFirstRelease) {
console.log("\n🎉 第一次发布准备完成!建议执行以下命令:");
} else {
console.log("\n版本已更新。建议执行以下命令:");
}
if (isBeta) {
console.log(
`git add package.json manifest-beta.json versions.json`
);
} else {
console.log(`git add package.json manifest.json versions.json`);
}
if (isFirstRelease) {
console.log(`git commit -m "feat: first release ${version}"`);
} else {
console.log(`git commit -m "build: bump version to ${version}"`);
}
console.log(`git tag ${version}`);
if (isFirstRelease) {
console.log("\n🎊 第一次发布版本准备完成!");
} else {
console.log("\n版本更新完成");
}
} catch (error) {
console.error("更新版本时出错:", error);
process.exit(1);
}
}
/**
* 更新 package.json 文件
* @param {string} version 新版本号
*/
function updatePackageJson(version) {
try {
const packageJson = JSON.parse(readFileSync("package.json", "utf8"));
packageJson.version = version;
writeFileSync(
"package.json",
JSON.stringify(packageJson, null, "\t") + "\n"
);
console.log(`已更新 package.json 版本至 ${version}`);
} catch (error) {
console.error("更新 package.json 时出错:", error);
throw error;
}
}
/**
* 更新 manifest.json 或 manifest-beta.json 文件
* @param {string} version 新版本号
* @param {boolean} isBeta 是否为Beta版本
* @returns {string} 最低应用版本
*/
function updateManifestJson(version, isBeta = false) {
try {
const manifestFile = isBeta ? "manifest-beta.json" : "manifest.json";
// 检查文件是否存在
if (!existsSync(manifestFile) && isBeta) {
// 如果manifest-beta.json不存在则从manifest.json复制一份
if (existsSync("manifest.json")) {
const manifest = JSON.parse(
readFileSync("manifest.json", "utf8")
);
manifest.version = version;
writeFileSync(
manifestFile,
JSON.stringify(manifest, null, "\t") + "\n"
);
console.log(`已创建并更新 ${manifestFile} 版本至 ${version}`);
return manifest.minAppVersion;
}
}
const manifest = JSON.parse(readFileSync(manifestFile, "utf8"));
const { minAppVersion } = manifest;
manifest.version = version;
writeFileSync(
manifestFile,
JSON.stringify(manifest, null, "\t") + "\n"
);
console.log(`已更新 ${manifestFile} 版本至 ${version}`);
return minAppVersion;
} catch (error) {
console.error(`更新 manifest 文件时出错:`, error);
throw error;
}
}
/**
* 更新 versions.json 文件
* @param {string} version 新版本号
* @param {string} minAppVersion 最低应用版本
*/
function updateVersionsJson(version, minAppVersion) {
try {
// 读取或创建 versions.json
let versions = {};
try {
if (existsSync("versions.json")) {
const versionsContent = readFileSync("versions.json", "utf8");
if (versionsContent.trim()) {
versions = JSON.parse(versionsContent);
}
}
} catch (error) {
console.log("创建新的 versions.json 文件");
}
// 更新版本信息
versions[version] = minAppVersion;
writeFileSync(
"versions.json",
JSON.stringify(versions, null, "\t") + "\n"
);
console.log(
`已更新 versions.json添加版本 ${version} -> ${minAppVersion}`
);
} catch (error) {
console.error("更新 versions.json 时出错:", error);
throw error;
}
}