377 lines
12 KiB
JavaScript
377 lines
12 KiB
JavaScript
const fs = require('fs');
|
||
const path = require('path');
|
||
const { execSync } = require('child_process');
|
||
|
||
function getCurrentVersion() {
|
||
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||
return packageJson.version;
|
||
}
|
||
|
||
function getNextVersion(currentVersion, type = 'patch') {
|
||
const parts = currentVersion.split('.').map(Number);
|
||
|
||
switch (type) {
|
||
case 'major':
|
||
parts[0]++;
|
||
parts[1] = 0;
|
||
parts[2] = 0;
|
||
break;
|
||
case 'minor':
|
||
parts[1]++;
|
||
parts[2] = 0;
|
||
break;
|
||
case 'patch':
|
||
default:
|
||
parts[2]++;
|
||
break;
|
||
}
|
||
|
||
return parts.join('.');
|
||
}
|
||
|
||
function isValidVersion(version) {
|
||
return /^\d+\.\d+\.\d+$/.test(version);
|
||
}
|
||
|
||
function compareVersions(a, b) {
|
||
const aParts = a.split('.').map(Number);
|
||
const bParts = b.split('.').map(Number);
|
||
|
||
for (let i = 0; i < 3; i++) {
|
||
if (aParts[i] > bParts[i]) return 1;
|
||
if (aParts[i] < bParts[i]) return -1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
function tagExists(version) {
|
||
try {
|
||
execSync(`git tag -l | grep -q "^v${version}$"`, { stdio: 'pipe' });
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function findNextAvailableVersion(baseVersion, type) {
|
||
let version = getNextVersion(baseVersion, type);
|
||
|
||
// 如果 tag 已存在,继续递增直到找到可用的版本号
|
||
while (tagExists(version)) {
|
||
console.log(`⚠️ 版本 v${version} 已存在,尝试下一个版本...`);
|
||
version = getNextVersion(version, 'patch'); // 总是递增 patch 版本
|
||
}
|
||
|
||
return version;
|
||
}
|
||
|
||
function updatePackageVersion(newVersion) {
|
||
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||
packageJson.version = newVersion;
|
||
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2) + '\n');
|
||
}
|
||
|
||
// 更新 Service Worker 中的版本号(已废弃,sw.js 现在是构建产物)
|
||
function updateServiceWorkerVersion(version) {
|
||
// sw.js 现在是构建产物,不再需要手动更新
|
||
// 版本信息会通过 version.json 和构建过程自动处理
|
||
console.log(`ℹ️ Service Worker 版本将通过构建过程自动更新`);
|
||
}
|
||
|
||
// 计算两个字符串的相似度 (Jaccard Index based on characters)
|
||
function calculateSimilarity(str1, str2) {
|
||
const set1 = new Set(str1.split(''));
|
||
const set2 = new Set(str2.split(''));
|
||
|
||
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
||
const union = new Set([...set1, ...set2]);
|
||
|
||
return intersection.size / union.size;
|
||
}
|
||
|
||
// 创建版本信息文件
|
||
function createVersionFile(version, commits) {
|
||
let changelog = [];
|
||
if (commits) {
|
||
// 过滤掉用户无感的提交类型 (docs, test, style, refactor, ci, build, revert)
|
||
const irrelevantRegex = /^(docs|test|style|refactor|ci|build|revert|chore)(\(.*?\))?:/i;
|
||
const relevantOthers = commits.others.filter(c => !c.message.match(irrelevantRegex));
|
||
|
||
const allCommits = [
|
||
...commits.features,
|
||
...commits.fixes,
|
||
// ...commits.chores, // 排除 chores
|
||
...relevantOthers
|
||
];
|
||
|
||
// 1. 过滤出有意义的提交消息(汉字 > 5 个 或 英文单词 > 5 个)
|
||
const filteredCommits = allCommits
|
||
.filter(c => {
|
||
const chineseChars = c.message.match(/[\u4e00-\u9fa5]/g) || [];
|
||
// 提取英文单词(排除常见的 commit 前缀如 feat/fix/chore 等)
|
||
const cleanMessage = c.message.replace(/^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]*\))?:\s*/i, '');
|
||
const englishWords = cleanMessage.match(/[a-zA-Z]{2,}/g) || [];
|
||
return chineseChars.length > 5 || englishWords.length > 5;
|
||
})
|
||
.map(c => c.message);
|
||
|
||
// 2. 按长度降序排序(文字多的优先级高)
|
||
filteredCommits.sort((a, b) => b.length - a.length);
|
||
|
||
// 3. 去重和过滤相似消息
|
||
const uniqueChangelog = [];
|
||
for (const msg of filteredCommits) {
|
||
let isSimilar = false;
|
||
for (const existingMsg of uniqueChangelog) {
|
||
// 如果包含关系(忽略大小写和空格)
|
||
if (existingMsg.toLowerCase().replace(/\s/g, '').includes(msg.toLowerCase().replace(/\s/g, ''))) {
|
||
isSimilar = true;
|
||
break;
|
||
}
|
||
|
||
// 如果相似度超过 0.6 (60% 相似)
|
||
if (calculateSimilarity(msg, existingMsg) > 0.6) {
|
||
isSimilar = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!isSimilar) {
|
||
uniqueChangelog.push(msg);
|
||
}
|
||
}
|
||
|
||
changelog = uniqueChangelog;
|
||
}
|
||
|
||
const versionInfo = {
|
||
version: version,
|
||
buildTime: new Date().toISOString(),
|
||
gitCommit: process.env.GITHUB_SHA || 'unknown',
|
||
changelog: changelog
|
||
};
|
||
|
||
const versionPath = path.join(__dirname, '../apps/web/public/version.json');
|
||
fs.writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2));
|
||
console.log(`✅ 版本信息文件已创建: ${version} (包含 ${changelog.length} 条更新日志)`);
|
||
}
|
||
|
||
// 获取上一个版本号
|
||
function getPreviousVersion(currentVersion) {
|
||
try {
|
||
// 尝试获取上一个版本的 tag
|
||
const tags = execSync('git tag --sort=-version:refname', { encoding: 'utf8' })
|
||
.trim()
|
||
.split('\n')
|
||
.filter(tag => tag.startsWith('v'));
|
||
|
||
// 找到当前版本之前的版本
|
||
const currentTag = `v${currentVersion}`;
|
||
const currentIndex = tags.indexOf(currentTag);
|
||
|
||
if (currentIndex > 0 && currentIndex < tags.length) {
|
||
return tags[currentIndex + 1].substring(1); // 移除 'v' 前缀
|
||
}
|
||
|
||
// 如果找不到,返回最新的 tag
|
||
if (tags.length > 0) {
|
||
return tags[0].substring(1);
|
||
}
|
||
} catch (error) {
|
||
console.warn('⚠️ 无法获取上一个版本:', error.message);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// 获取提交记录并分类
|
||
function getCommitsSinceLastVersion(lastVersion) {
|
||
try {
|
||
let gitCommand;
|
||
if (lastVersion) {
|
||
gitCommand = `git log v${lastVersion}..HEAD --pretty=format:"%s|||%h|||%an|||%ae" --no-merges`;
|
||
} else {
|
||
// 如果没有上一个版本,获取最近20条提交
|
||
gitCommand = `git log -20 --pretty=format:"%s|||%h|||%an|||%ae" --no-merges`;
|
||
}
|
||
|
||
const commits = execSync(gitCommand, { encoding: 'utf8' })
|
||
.trim()
|
||
.split('\n')
|
||
.filter(line => line.length > 0);
|
||
|
||
// 分类提交
|
||
const categorized = {
|
||
features: [],
|
||
fixes: [],
|
||
chores: [],
|
||
others: [],
|
||
authors: new Set()
|
||
};
|
||
|
||
commits.forEach(commit => {
|
||
const [message, hash, authorName, authorEmail] = commit.split('|||');
|
||
|
||
// 收集作者信息
|
||
categorized.authors.add(`${authorName} <${authorEmail}>`);
|
||
|
||
// 根据 conventional commits 规范分类
|
||
if (message.match(/^feat(\(.*?\))?:/i)) {
|
||
categorized.features.push({ message: message.replace(/^feat(\(.*?\))?:\s*/i, ''), hash });
|
||
} else if (message.match(/^fix(\(.*?\))?:/i)) {
|
||
categorized.fixes.push({ message: message.replace(/^fix(\(.*?\))?:\s*/i, ''), hash });
|
||
} else if (message.match(/^chore(\(.*?\))?:/i)) {
|
||
categorized.chores.push({ message: message.replace(/^chore(\(.*?\))?:\s*/i, ''), hash });
|
||
} else {
|
||
categorized.others.push({ message, hash });
|
||
}
|
||
});
|
||
|
||
return categorized;
|
||
} catch (error) {
|
||
console.warn('⚠️ 无法获取提交记录:', error.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 更新 CHANGELOG.md
|
||
function updateChangelog(version, commits) {
|
||
const changelogPath = path.join(__dirname, '../CHANGELOG.md');
|
||
const date = new Date().toISOString().split('T')[0];
|
||
|
||
// 构建新的 changelog 条目
|
||
let newEntry = `## ${version} (${date})\n\n`;
|
||
|
||
// 添加功能
|
||
if (commits.features.length > 0) {
|
||
newEntry += `### 🚀 Features\n\n`;
|
||
commits.features.forEach(({ message, hash }) => {
|
||
newEntry += `- ${message} ([${hash}](https://github.com/ljquan/aitu/commit/${hash}))\n`;
|
||
});
|
||
newEntry += '\n';
|
||
}
|
||
|
||
// 添加修复
|
||
if (commits.fixes.length > 0) {
|
||
newEntry += `### 🩹 Fixes\n\n`;
|
||
commits.fixes.forEach(({ message, hash }) => {
|
||
newEntry += `- ${message} ([${hash}](https://github.com/ljquan/aitu/commit/${hash}))\n`;
|
||
});
|
||
newEntry += '\n';
|
||
}
|
||
|
||
// 添加其他更改
|
||
if (commits.chores.length > 0 || commits.others.length > 0) {
|
||
newEntry += `### 🔧 Chores\n\n`;
|
||
[...commits.chores, ...commits.others].forEach(({ message, hash }) => {
|
||
newEntry += `- ${message} ([${hash}](https://github.com/ljquan/aitu/commit/${hash}))\n`;
|
||
});
|
||
newEntry += '\n';
|
||
}
|
||
|
||
// 添加贡献者
|
||
if (commits.authors.size > 0) {
|
||
newEntry += `### ❤️ Thank You\n\n`;
|
||
Array.from(commits.authors).forEach(author => {
|
||
newEntry += `- ${author}\n`;
|
||
});
|
||
newEntry += '\n';
|
||
}
|
||
|
||
// 读取现有 CHANGELOG
|
||
let changelogContent = '';
|
||
if (fs.existsSync(changelogPath)) {
|
||
changelogContent = fs.readFileSync(changelogPath, 'utf8');
|
||
}
|
||
|
||
// 插入新条目到文件开头
|
||
const updatedChangelog = newEntry + changelogContent;
|
||
fs.writeFileSync(changelogPath, updatedChangelog);
|
||
|
||
console.log(`✅ CHANGELOG.md 已更新`);
|
||
}
|
||
|
||
function main() {
|
||
const versionType = process.argv[2] || 'patch';
|
||
const targetArg = process.argv.find(arg => arg.startsWith('--target='));
|
||
const targetVersion = targetArg ? targetArg.slice('--target='.length) : null;
|
||
|
||
try {
|
||
const currentVersion = getCurrentVersion();
|
||
console.log(`📦 当前版本: ${currentVersion}`);
|
||
|
||
if (targetVersion && !isValidVersion(targetVersion)) {
|
||
throw new Error(`目标版本格式无效: ${targetVersion}`);
|
||
}
|
||
if (targetVersion && compareVersions(targetVersion, currentVersion) <= 0) {
|
||
throw new Error(`目标版本必须大于当前版本: ${currentVersion} → ${targetVersion}`);
|
||
}
|
||
|
||
const nextVersion = targetVersion || findNextAvailableVersion(currentVersion, versionType);
|
||
console.log(`🚀 升级到版本: ${nextVersion}`);
|
||
|
||
// 更新 package.json
|
||
updatePackageVersion(nextVersion);
|
||
console.log(`✅ package.json 已更新到 ${nextVersion}`);
|
||
|
||
// 更新 Service Worker 版本
|
||
updateServiceWorkerVersion(nextVersion);
|
||
|
||
// 获取并更新 CHANGELOG
|
||
const previousVersion = getPreviousVersion(currentVersion);
|
||
console.log(`📝 从版本 ${previousVersion || '开始'} 收集提交记录...`);
|
||
|
||
const commits = getCommitsSinceLastVersion(previousVersion);
|
||
|
||
// 创建版本信息文件
|
||
createVersionFile(nextVersion, commits);
|
||
|
||
if (commits && (commits.features.length > 0 || commits.fixes.length > 0 || commits.chores.length > 0 || commits.others.length > 0)) {
|
||
updateChangelog(nextVersion, commits);
|
||
} else {
|
||
console.log(`ℹ️ 没有找到提交记录,跳过 CHANGELOG 更新`);
|
||
}
|
||
|
||
// 同步 CHANGELOG.md → changelog.json
|
||
try {
|
||
require('./sync-changelog')();
|
||
} catch (error) {
|
||
console.warn(`⚠️ 同步 changelog.json 失败:`, error.message);
|
||
}
|
||
|
||
// 提交更改
|
||
try {
|
||
// 检查是否存在 package-lock.json 或 pnpm-lock.yaml
|
||
const filesToAdd = ['package.json', 'apps/web/public/version.json', 'CHANGELOG.md', 'apps/web/public/changelog.json'];
|
||
if (fs.existsSync('package-lock.json')) {
|
||
filesToAdd.push('package-lock.json');
|
||
} else if (fs.existsSync('pnpm-lock.yaml')) {
|
||
filesToAdd.push('pnpm-lock.yaml');
|
||
}
|
||
|
||
execSync(`git add ${filesToAdd.join(' ')}`, { stdio: 'inherit' });
|
||
execSync(`git commit -m "chore: bump version to ${nextVersion}"`, { stdio: 'inherit' });
|
||
console.log(`✅ 版本更改已提交`);
|
||
} catch (error) {
|
||
console.log(`ℹ️ 跳过 git 提交(可能没有更改或不在 git 仓库中)`);
|
||
}
|
||
|
||
// 创建 tag
|
||
try {
|
||
execSync(`git tag -a v${nextVersion} -m "Release ${nextVersion}"`, { stdio: 'inherit' });
|
||
console.log(`✅ 创建 git tag: v${nextVersion}`);
|
||
} catch (error) {
|
||
console.log(`⚠️ 创建 git tag 失败: ${error.message}`);
|
||
}
|
||
|
||
console.log(`\n🎉 版本升级完成: ${currentVersion} → ${nextVersion}`);
|
||
|
||
} catch (error) {
|
||
console.error(`❌ 版本升级失败:`, error.message);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
main();
|