Skip to content

Commit ff312a7

Browse files
committed
ci: Add changelog automation workflow with backfill support
1 parent b76cd84 commit ff312a7

File tree

4 files changed

+378
-0
lines changed

4 files changed

+378
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/env node
2+
/**
3+
* backfill-changelog.js
4+
* 一次性回填历史版本到 CHANGELOG.md
5+
* 用法: node .github/scripts/backfill-changelog.js
6+
*/
7+
8+
const { execSync } = require('child_process');
9+
const fs = require('fs');
10+
const path = require('path');
11+
12+
const CHANGELOG_PATH = path.resolve('CHANGELOG.md');
13+
const TARGET_FILE = 'FFA-Omnibar.js'; // 如有多个脚本,改成数组后循环
14+
15+
// ── 1. 获取该文件所有 commit(从旧到新) ──────────────────────────────────
16+
const rawLog = execSync(
17+
`git log --reverse --pretty=format:"%H|%ad|%s" --date=short -- ${TARGET_FILE}`,
18+
{ encoding: 'utf8', shell: true }
19+
).toString().trim();
20+
21+
if (!rawLog) {
22+
console.error('没有找到该文件的 commit 历史');
23+
process.exit(1);
24+
}
25+
26+
const commits = rawLog.split('\n').map(line => {
27+
const [hash, date, ...msgParts] = line.split('|');
28+
return { hash, date, msg: msgParts.join('|') };
29+
});
30+
31+
// ── 2. 找出每个 commit 对应的 @version ───────────────────────────────────
32+
function getVersionAtCommit(hash, file) {
33+
try {
34+
const content = execSync(`git show "${hash}:${file}"`, {
35+
encoding: 'utf8',
36+
stdio: ['pipe', 'pipe', 'ignore'], // 忽略 stderr,避免 Windows 报错污染
37+
shell: true,
38+
});
39+
const match = content.match(/\/\/ @version\s+(\S+)/);
40+
return match ? match[1] : null;
41+
} catch {
42+
return null;
43+
}
44+
}
45+
46+
// 构建每个 commit 的版本信息
47+
const commitInfos = commits.map(c => ({
48+
...c,
49+
version: getVersionAtCommit(c.hash, TARGET_FILE),
50+
}));
51+
52+
// ── 3. 按版本分组(版本号第一次出现的 commit 作为该版本的"发布节点") ────
53+
const versionMap = new Map(); // version -> { date, commits[] }
54+
55+
let currentVersion = null;
56+
for (const info of commitInfos) {
57+
if (info.version && info.version !== currentVersion) {
58+
// 版本发生变化,开启新版本分组
59+
currentVersion = info.version;
60+
if (!versionMap.has(currentVersion)) {
61+
versionMap.set(currentVersion, { date: info.date, commits: [] });
62+
}
63+
}
64+
if (currentVersion) {
65+
versionMap.get(currentVersion).commits.push(info);
66+
}
67+
}
68+
69+
// ── 4. 分类函数(复用 workflow 里的逻辑) ─────────────────────────────────
70+
const SKIP_PATTERNS = [/^\[skip ci\]/i, /^docs: update changelog/i, /^chore:/i];
71+
72+
function categorize(commitList) {
73+
const cats = { Added: [], Changed: [], Fixed: [], Removed: [], Security: [], Other: [] };
74+
for (const { msg } of commitList) {
75+
if (SKIP_PATTERNS.some(p => p.test(msg))) continue;
76+
const clean = msg.replace(/^(feat|fix|refactor|perf|style|test|build|ci|chore|docs)(\(.+?\))?!?:\s*/i, '').trim();
77+
if (/^feat/i.test(msg)) cats.Added.push(clean);
78+
else if (/^fix/i.test(msg)) cats.Fixed.push(clean);
79+
else if (/^refactor|^perf/i.test(msg))cats.Changed.push(clean);
80+
else if (/^remove|^drop/i.test(msg)) cats.Removed.push(clean);
81+
else if (/security/i.test(msg)) cats.Security.push(clean);
82+
else cats.Other.push(clean);
83+
}
84+
return cats;
85+
}
86+
87+
// ── 5. 生成所有版本的 Markdown 段落(从新到旧) ──────────────────────────
88+
const ORDER = ['Added', 'Changed', 'Fixed', 'Removed', 'Security', 'Other'];
89+
const sections = [];
90+
91+
// 倒序:最新版本在最前
92+
const versions = [...versionMap.entries()].reverse();
93+
94+
for (const [version, { date, commits: vCommits }] of versions) {
95+
const cats = categorize(vCommits);
96+
const hasContent = Object.values(cats).some(a => a.length > 0);
97+
98+
const lines = [`## [${version}] - ${date}`];
99+
if (hasContent) {
100+
for (const cat of ORDER) {
101+
if (cats[cat].length === 0) continue;
102+
lines.push(`\n### ${cat}`);
103+
cats[cat].forEach(item => lines.push(`- ${item}`));
104+
}
105+
} else {
106+
lines.push('\n_No significant changes recorded._');
107+
}
108+
sections.push(lines.join('\n'));
109+
}
110+
111+
// ── 6. 写入 CHANGELOG.md ──────────────────────────────────────────────────
112+
const header = `# Changelog
113+
114+
All notable changes to this project will be documented in this file.
115+
116+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
117+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
118+
`;
119+
120+
const body = header + '\n' + sections.join('\n\n') + '\n';
121+
fs.writeFileSync(CHANGELOG_PATH, body, 'utf8');
122+
123+
console.log(`✅ 回填完成,共写入 ${versionMap.size} 个版本:`);
124+
for (const [v, { date }] of [...versionMap.entries()].reverse()) {
125+
console.log(` ${v} (${date})`);
126+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/usr/bin/env node
2+
/**
3+
* update-changelog.js
4+
* 读取最近的 commit messages,按 Keep a Changelog 规范
5+
* 将新版本段落插入 CHANGELOG.md 的顶部(## [Unreleased] 之后)
6+
*/
7+
8+
const { execSync } = require('child_process');
9+
const fs = require('fs');
10+
const path = require('path');
11+
12+
const CHANGELOG_PATH = path.resolve('CHANGELOG.md');
13+
const NEW_VERSION = process.env.NEW_VERSION;
14+
const OLD_VERSION = process.env.OLD_VERSION;
15+
const BUMPED_FILE = process.env.BUMPED_FILE;
16+
17+
if (!NEW_VERSION) {
18+
console.error('NEW_VERSION env var is required');
19+
process.exit(1);
20+
}
21+
22+
// ── 1. 拉取自上个版本以来的 commit messages ──────────────────────────────
23+
let commitLog = '';
24+
try {
25+
// 如果有旧版本 tag,从 tag 开始;否则取最近 20 条
26+
const tagExists = execSync(`git tag -l "v${OLD_VERSION}"`).toString().trim();
27+
const range = tagExists ? `v${OLD_VERSION}..HEAD` : '-20';
28+
commitLog = execSync(
29+
`git log ${range} --pretty=format:"%s" --no-merges`
30+
).toString().trim();
31+
} catch {
32+
commitLog = execSync('git log -10 --pretty=format:"%s" --no-merges').toString().trim();
33+
}
34+
35+
const commits = commitLog.split('\n').filter(Boolean);
36+
37+
// ── 2. 按 Conventional Commits 关键词分类 ────────────────────────────────
38+
const categories = {
39+
Added: [],
40+
Changed: [],
41+
Fixed: [],
42+
Removed: [],
43+
Security: [],
44+
Other: [],
45+
};
46+
47+
const SKIP_PATTERNS = [/^\[skip ci\]/i, /^docs: update changelog/i, /^chore: /i];
48+
49+
for (const msg of commits) {
50+
if (SKIP_PATTERNS.some(p => p.test(msg))) continue;
51+
52+
const clean = msg.replace(/^(feat|fix|refactor|perf|style|test|build|ci|chore|docs)(\(.+?\))?!?:\s*/i, '').trim();
53+
54+
if (/^feat/i.test(msg)) categories.Added.push(clean);
55+
else if (/^fix/i.test(msg)) categories.Fixed.push(clean);
56+
else if (/^refactor|perf/i.test(msg)) categories.Changed.push(clean);
57+
else if (/^remove|^drop/i.test(msg)) categories.Removed.push(clean);
58+
else if (/security/i.test(msg)) categories.Security.push(clean);
59+
else categories.Other.push(clean);
60+
}
61+
62+
// 如果所有分类都是空的,把所有提交放进 Other
63+
const hasAny = Object.values(categories).some(a => a.length > 0);
64+
if (!hasAny) {
65+
commits.forEach(m => {
66+
if (!SKIP_PATTERNS.some(p => p.test(m))) categories.Other.push(m);
67+
});
68+
}
69+
70+
// ── 3. 拼装新版本段落 ─────────────────────────────────────────────────────
71+
const today = new Date().toISOString().slice(0, 10);
72+
const lines = [`## [${NEW_VERSION}] - ${today}`];
73+
74+
if (BUMPED_FILE) {
75+
lines.push(`\n> 📦 \`${BUMPED_FILE}\``);
76+
}
77+
78+
const ORDER = ['Added', 'Changed', 'Fixed', 'Removed', 'Security', 'Other'];
79+
for (const cat of ORDER) {
80+
if (categories[cat].length === 0) continue;
81+
lines.push(`\n### ${cat}`);
82+
categories[cat].forEach(item => lines.push(`- ${item}`));
83+
}
84+
85+
const newSection = lines.join('\n');
86+
87+
// ── 4. 插入到 CHANGELOG.md ────────────────────────────────────────────────
88+
let content = '';
89+
if (fs.existsSync(CHANGELOG_PATH)) {
90+
content = fs.readFileSync(CHANGELOG_PATH, 'utf8');
91+
} else {
92+
content = `# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).\n`;
93+
}
94+
95+
// 找到第一个 ## 版本段落的位置,在它之前插入
96+
const insertAfterHeader = content.indexOf('\n## ');
97+
if (insertAfterHeader === -1) {
98+
// 没有已有版本段落,直接追加
99+
content = content.trimEnd() + '\n\n' + newSection + '\n';
100+
} else {
101+
content = content.slice(0, insertAfterHeader) + '\n\n' + newSection + '\n' + content.slice(insertAfterHeader);
102+
}
103+
104+
fs.writeFileSync(CHANGELOG_PATH, content, 'utf8');
105+
console.log(`✅ CHANGELOG.md updated → v${NEW_VERSION}`);

.github/workflows/changelog.yml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Auto Update Changelog
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
paths:
8+
- '**.js' # 只有 .js 文件变动才触发
9+
10+
jobs:
11+
update-changelog:
12+
runs-on: ubuntu-latest
13+
# 防止 changelog commit 本身再次触发 workflow
14+
if: "!contains(github.event.head_commit.message, '[skip ci]')"
15+
16+
steps:
17+
- name: Checkout repository
18+
uses: actions/checkout@v4
19+
with:
20+
fetch-depth: 2 # 需要拿到上一个 commit 来做版本对比
21+
22+
- name: Setup Node.js
23+
uses: actions/setup-node@v4
24+
with:
25+
node-version: '20'
26+
27+
- name: Detect version bumps in JS files
28+
id: detect
29+
run: |
30+
# 获取本次 push 中变动的 .js 文件列表
31+
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD -- '*.js')
32+
echo "Changed JS files: $CHANGED_FILES"
33+
34+
VERSION_BUMPED=""
35+
BUMPED_FILE=""
36+
NEW_VERSION=""
37+
OLD_VERSION=""
38+
39+
for FILE in $CHANGED_FILES; do
40+
if [ ! -f "$FILE" ]; then continue; fi
41+
42+
# 提取当前版本
43+
CUR=$(grep -oP '(?<=// @version\s{1,10})\S+' "$FILE" || true)
44+
# 提取上个 commit 的版本
45+
PREV=$(git show HEAD~1:"$FILE" 2>/dev/null | grep -oP '(?<=// @version\s{1,10})\S+' || true)
46+
47+
echo "File: $FILE | prev: $PREV | current: $CUR"
48+
49+
if [ -n "$CUR" ] && [ "$CUR" != "$PREV" ]; then
50+
VERSION_BUMPED="true"
51+
BUMPED_FILE="$FILE"
52+
NEW_VERSION="$CUR"
53+
OLD_VERSION="$PREV"
54+
break
55+
fi
56+
done
57+
58+
echo "version_bumped=$VERSION_BUMPED" >> $GITHUB_OUTPUT
59+
echo "bumped_file=$BUMPED_FILE" >> $GITHUB_OUTPUT
60+
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
61+
echo "old_version=$OLD_VERSION" >> $GITHUB_OUTPUT
62+
63+
- name: Update CHANGELOG.md
64+
if: steps.detect.outputs.version_bumped == 'true'
65+
env:
66+
NEW_VERSION: ${{ steps.detect.outputs.new_version }}
67+
OLD_VERSION: ${{ steps.detect.outputs.old_version }}
68+
BUMPED_FILE: ${{ steps.detect.outputs.bumped_file }}
69+
run: node .github/scripts/update-changelog.js
70+
71+
- name: Commit and push CHANGELOG
72+
if: steps.detect.outputs.version_bumped == 'true'
73+
run: |
74+
git config user.name "github-actions[bot]"
75+
git config user.email "github-actions[bot]@users.noreply.github.com"
76+
git add CHANGELOG.md
77+
git diff --staged --quiet || \
78+
git commit -m "docs: update CHANGELOG for v${{ steps.detect.outputs.new_version }} [skip ci]"
79+
git push

CHANGELOG.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [3.0.2] - 2026-03-16
9+
10+
### Added
11+
- Refine engine button animations with advanced easing and micro-interactions
12+
13+
## [3.0.1] - 2026-03-15
14+
15+
### Added
16+
- Enhance search input with expandable design and search button
17+
- Update metadata and enhance visual consistency
18+
19+
### Changed
20+
- Remove redundant @connect directive and iframe check
21+
22+
## [3.0.0] - 2026-03-15
23+
24+
### Added
25+
- Major visual overhaul with brand icons and enhanced theme system
26+
27+
## [2.1.7] - 2026-03-14
28+
29+
### Added
30+
- Add smart engine matching and improved button behavior
31+
32+
## [2.1.6] - 2026-03-13
33+
34+
### Fixed
35+
- Prevent duplicate toolbar in iframes with @noframes and runtime checks
36+
37+
## [2.1.5] - 2026-03-13
38+
39+
### Fixed
40+
- Improve SuggestModule cache mechanism to prevent memory leak
41+
42+
## [2.1.4] - 2026-03-13
43+
44+
### Added
45+
- Add separate blur controls for toolbar and panel
46+
47+
## [2.1.2] - 2026-03-13
48+
49+
### Fixed
50+
- Apply openInNewTab setting to all search actions
51+
52+
## [2.1.1] - 2026-03-13
53+
54+
### Added
55+
- Optimize search UI with single global input box
56+
57+
## [2.1.0] - 2026-03-13
58+
59+
### Added
60+
- Transform FFA Omnibar into global search assistant
61+
62+
## [2.0.0] - 2026-03-13
63+
64+
### Added
65+
- Add search behavior setting for opening in new tab
66+
67+
### Other
68+
- Initial commit: FFA-Omnibar script

0 commit comments

Comments
 (0)