feat(ci): add automated dynamic release notes generation with git changelog and checksums
This commit is contained in:
+145
-19
@@ -1,5 +1,107 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
/**
|
||||
* 动态提取 Git Commit 日志并生成规范化 Release Notes
|
||||
*/
|
||||
function generateReleaseNotes({ tag, files, distDir, repoUrl }) {
|
||||
let prevTag = '';
|
||||
try {
|
||||
const tags = execSync('git tag --sort=-creatordate', { encoding: 'utf8' })
|
||||
.split('\n')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
const currIdx = tags.indexOf(tag);
|
||||
if (currIdx !== -1 && currIdx < tags.length - 1) {
|
||||
prevTag = tags[currIdx + 1];
|
||||
} else if (tags.length > 0 && currIdx === -1) {
|
||||
prevTag = tags[0];
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Release] Unable to determine previous git tag:', e.message);
|
||||
}
|
||||
|
||||
let commits = [];
|
||||
try {
|
||||
const range = prevTag ? `${prevTag}..HEAD` : 'HEAD';
|
||||
const logOutput = execSync(`git log ${range} --pretty=format:"%h%x09%s"`, { encoding: 'utf8' });
|
||||
commits = logOutput
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [hash, ...rest] = line.split('\t');
|
||||
return { hash, subject: rest.join('\t') };
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[Release] Unable to retrieve git log:', e.message);
|
||||
}
|
||||
|
||||
// 按照 Conventional Commits 规范分类
|
||||
const categories = {
|
||||
features: { title: '🚀 新功能 (Features)', items: [] },
|
||||
ui: { title: '🎨 界面与主题 (UI & Themes)', items: [] },
|
||||
fixes: { title: '🐛 问题修复 (Bug Fixes)', items: [] },
|
||||
docs: { title: '📖 文档与说明 (Documentation)', items: [] },
|
||||
ci: { title: '🔧 构建与 CI (Build & CI)', items: [] },
|
||||
other: { title: '📝 其他变动 (Other Changes)', items: [] },
|
||||
};
|
||||
|
||||
for (const c of commits) {
|
||||
const s = c.subject;
|
||||
const item = repoUrl
|
||||
? `* [${c.hash}](${repoUrl}/commit/${c.hash}) - ${s}`
|
||||
: `* ${c.hash} - ${s}`;
|
||||
|
||||
if (/^(style|theme)(\(.*\))?:/i.test(s) || /theme|主题|配色/i.test(s)) {
|
||||
categories.ui.items.push(item);
|
||||
} else if (/^feat(\(.*\))?:/i.test(s)) {
|
||||
categories.features.items.push(item);
|
||||
} else if (/^fix(\(.*\))?:/i.test(s)) {
|
||||
categories.fixes.items.push(item);
|
||||
} else if (/^docs?(\(.*\))?:/i.test(s)) {
|
||||
categories.docs.items.push(item);
|
||||
} else if (/^(ci|chore|build|refactor|perf)(\(.*\))?:/i.test(s)) {
|
||||
categories.ci.items.push(item);
|
||||
} else {
|
||||
categories.other.items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
let changelogSection = '';
|
||||
let hasItems = false;
|
||||
for (const key of Object.keys(categories)) {
|
||||
const cat = categories[key];
|
||||
if (cat.items.length > 0) {
|
||||
hasItems = true;
|
||||
changelogSection += `#### ${cat.title}\n\n${cat.items.join('\n')}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasItems) {
|
||||
changelogSection = '暂无详细更新记录。\n\n';
|
||||
}
|
||||
|
||||
// 产物文件大小与 SHA-256 校验和表格
|
||||
let checksumSection = '';
|
||||
if (files && files.length > 0 && distDir) {
|
||||
checksumSection = '### 📦 安装包校验信息 (Checksums)\n\n| 文件名 | 大小 | SHA-256 校验和 |\n| :--- | :--- | :--- |\n';
|
||||
for (const file of files) {
|
||||
const filePath = path.join(distDir, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const stat = fs.statSync(filePath);
|
||||
const sizeStr = (stat.size / (1024 * 1024)).toFixed(2) + ' MB';
|
||||
const hash = crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
||||
checksumSection += `| \`${file}\` | ${sizeStr} | \`${hash}\` |\n`;
|
||||
}
|
||||
}
|
||||
checksumSection += '\n';
|
||||
}
|
||||
|
||||
return `## ⚡ hatty ${tag} 发布说明\n\n### 🌟 更新日志 (What's Changed)\n\n${prevTag && repoUrl ? `> 对比基准版本:[${prevTag}](${repoUrl}/src/tag/${prevTag}) → **${tag}**\n\n` : ''}${changelogSection}### 💻 安装方法 (Installation)\n\n下载下方附件中的 \`.deb\` 安装包,并在终端执行安装:\n\n\`\`\`bash\n# 安装 deb 安装包\nsudo dpkg -i hatty_*.deb\n\n# 如遇到缺少系统依赖,可执行自动修复:\nsudo apt-get install -f -y\n\`\`\`\n\n${checksumSection}---\n*由 Gitea Actions 自动化流水线构建并发布。*`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const serverUrl = process.env.GITHUB_SERVER_URL || process.env.GITEA_SERVER_URL || 'https://git.zzzjy.cloud';
|
||||
@@ -16,11 +118,31 @@ async function main() {
|
||||
}
|
||||
|
||||
const [owner, repoName] = repo.split('/');
|
||||
const apiBase = `${serverUrl.replace(/\/+$/, '')}/api/v1/repos/${owner}/${repoName}`;
|
||||
const normalizedServer = serverUrl.replace(/\/+$/, '');
|
||||
const repoUrl = `${normalizedServer}/${owner}/${repoName}`;
|
||||
const apiBase = `${normalizedServer}/api/v1/repos/${owner}/${repoName}`;
|
||||
|
||||
console.log(`Connecting to Gitea at ${serverUrl} for repository ${repo}...`);
|
||||
|
||||
// 1. 检查或创建 Release
|
||||
// 1. 查找 dist/ 下的 .deb / .AppImage 安装包并生成 Release Notes
|
||||
const distDir = path.resolve('dist');
|
||||
if (!fs.existsSync(distDir)) {
|
||||
console.error('dist directory does not exist.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(distDir).filter((f) => f.endsWith('.deb') || f.endsWith('.AppImage'));
|
||||
if (files.length === 0) {
|
||||
console.error('No .deb or .AppImage files found in dist directory.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const releaseNotes = generateReleaseNotes({ tag, files, distDir, repoUrl });
|
||||
console.log('--- Generated Release Notes ---');
|
||||
console.log(releaseNotes);
|
||||
console.log('-------------------------------');
|
||||
|
||||
// 2. 检查或创建 / 更新 Release
|
||||
let releaseId = null;
|
||||
const tagCheckRes = await fetch(`${apiBase}/releases/tags/${encodeURIComponent(tag)}`, {
|
||||
headers: {
|
||||
@@ -32,7 +154,24 @@ async function main() {
|
||||
if (tagCheckRes.ok) {
|
||||
const existing = await tagCheckRes.json();
|
||||
releaseId = existing.id;
|
||||
console.log(`Found existing release for tag ${tag} (id: ${releaseId})`);
|
||||
console.log(`Found existing release for tag ${tag} (id: ${releaseId}), updating release notes...`);
|
||||
const updateRes = await fetch(`${apiBase}/releases/${releaseId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: `hatty ${tag}`,
|
||||
body: releaseNotes,
|
||||
}),
|
||||
});
|
||||
if (!updateRes.ok) {
|
||||
console.warn(`Warning: Failed to update release body: ${updateRes.status}`);
|
||||
} else {
|
||||
console.log(`Release notes updated successfully for id ${releaseId}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`Creating new release for tag ${tag}...`);
|
||||
const createRes = await fetch(`${apiBase}/releases`, {
|
||||
@@ -45,7 +184,7 @@ async function main() {
|
||||
body: JSON.stringify({
|
||||
tag_name: tag,
|
||||
name: `hatty ${tag}`,
|
||||
body: `### ⚡ hatty ${tag} 现代化 Linux 终端新版本发布\n\n- **架构**:Electron + xterm.js (WebGL 硬件加速) + Linux PTY\n- **特性**:无边框磨砂毛玻璃、5 套现代暗黑主题、多标签页、灵活多窗格分屏\n- **安装说明**:下载下方附件 \`.deb\` 文件,在终端运行 \`sudo dpkg -i hatty_*.deb\` 即可完成安装。`,
|
||||
body: releaseNotes,
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
}),
|
||||
@@ -62,20 +201,7 @@ async function main() {
|
||||
console.log(`Release created successfully with id ${releaseId}`);
|
||||
}
|
||||
|
||||
// 2. 查找 dist/ 下的 .deb 安装包
|
||||
const distDir = path.resolve('dist');
|
||||
if (!fs.existsSync(distDir)) {
|
||||
console.error('dist directory does not exist.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(distDir).filter((f) => f.endsWith('.deb') || f.endsWith('.AppImage'));
|
||||
if (files.length === 0) {
|
||||
console.error('No .deb or .AppImage files found in dist directory.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 检查已存在的附件,若同名则先删除以支持重试
|
||||
// 3. 检查已存在的附件,若同名则先删除以支持重试
|
||||
try {
|
||||
const assetsRes = await fetch(`${apiBase}/releases/${releaseId}/assets`, {
|
||||
headers: {
|
||||
@@ -99,7 +225,7 @@ async function main() {
|
||||
console.warn('Checking existing assets notice:', e.message);
|
||||
}
|
||||
|
||||
// 3. 上传安装包到 Release 附件
|
||||
// 4. 上传安装包到 Release 附件
|
||||
for (const fileName of files) {
|
||||
const filePath = path.join(distDir, fileName);
|
||||
const fileStat = fs.statSync(filePath);
|
||||
|
||||
Reference in New Issue
Block a user