1- name : Auto-rebase submission PRs
1+ name : Rebuild open submission PRs after merge
22
3- # 每当有 PR 合并到 main 时触发
43on :
54 pull_request :
65 types : [closed]
76 branches : [main]
87
98concurrency :
10- group : rebase -submissions
9+ group : rebuild -submissions
1110 cancel-in-progress : false
1211
1312jobs :
14- rebase-open-prs :
15- # 只在 PR 被合并(不是关闭)时运行
13+ rebuild :
1614 if : github.event.pull_request.merged == true
1715 runs-on : ubuntu-latest
1816 permissions :
@@ -23,14 +21,16 @@ jobs:
2321 - uses : actions/checkout@v4
2422 with :
2523 ref : main
26- fetch-depth : 0
24+ fetch-depth : 1
2725 token : ${{ secrets.GITHUB_TOKEN }}
2826
29- - name : Rebase all open submission PRs
27+ - name : Rebuild open PRs onto latest main
3028 uses : actions/github-script@v7
3129 with :
3230 script : |
3331 const { execSync } = require('child_process');
32+ const fs = require('fs');
33+ const path = require('path');
3434
3535 execSync('git config user.name "github-actions[bot]"');
3636 execSync('git config user.email "github-actions[bot]@users.noreply.github.com"');
@@ -46,54 +46,155 @@ jobs:
4646 });
4747
4848 if (openPRs.length === 0) {
49- core.info('No open PRs to rebase .');
49+ core.info('No open PRs to rebuild .');
5050 return;
5151 }
5252
53- core.info(`Found ${openPRs.length} open PR(s) to rebase.`);
54-
55- let succeeded = 0;
56- let failed = 0;
53+ core.info(`Found ${openPRs.length} open PR(s) to rebuild.`);
5754
5855 for (const pr of openPRs) {
5956 const branch = pr.head.ref;
60- core.info(`\nRebasing PR #${pr.number}: ${pr.title} (branch: ${branch})`);
57+ core.info(`\nRebuilding PR #${pr.number}: ${pr.title} (branch: ${branch})`);
6158
6259 try {
63- // 获取远程分支
64- execSync(`git fetch origin ${branch}:${branch}`, { stdio: 'pipe' });
65-
66- // 切到该分支并 rebase 到最新 main
67- execSync(`git checkout ${branch}`, { stdio: 'pipe' });
68- execSync('git rebase origin/main', { stdio: 'pipe' });
69-
70- // force push 更新 PR
71- execSync(`git push origin ${branch} --force-with-lease`, { stdio: 'pipe' });
72-
73- core.info(`✅ PR #${pr.number} rebased successfully.`);
74- succeeded++;
60+ // 1. 获取 PR 的文件变更列表
61+ const { data: files } = await github.rest.pulls.listFiles({
62+ owner: context.repo.owner,
63+ repo: context.repo.repo,
64+ pull_number: pr.number
65+ });
7566
76- // 切回 main 准备下一个
67+ // 2. 找到新增的脚本文件(排除 index.json)
68+ const scriptFiles = files.filter(f =>
69+ f.status === 'added' && f.filename !== 'script_library/index.json'
70+ );
71+
72+ if (scriptFiles.length === 0) {
73+ core.warning(`PR #${pr.number}: No new script files found, skipping.`);
74+ continue;
75+ }
76+
77+ // 3. 从 PR 分支获取每个脚本文件的内容
78+ const scriptContents = [];
79+ for (const sf of scriptFiles) {
80+ try {
81+ const { data: fileData } = await github.rest.repos.getContent({
82+ owner: context.repo.owner,
83+ repo: context.repo.repo,
84+ path: sf.filename,
85+ ref: branch
86+ });
87+ const content = Buffer.from(fileData.content, 'base64').toString('utf8');
88+ scriptContents.push({ path: sf.filename, content });
89+ } catch (e) {
90+ core.warning(`PR #${pr.number}: Failed to read ${sf.filename}: ${e.message}`);
91+ }
92+ }
93+
94+ if (scriptContents.length === 0) {
95+ core.warning(`PR #${pr.number}: Could not read script content, skipping.`);
96+ continue;
97+ }
98+
99+ // 4. 从 PR body 中提取元数据
100+ const body = pr.body || '';
101+ const field = (key) => {
102+ const re = new RegExp(`${key}[::]\\s*(.+)`, 'i');
103+ const m = body.match(re);
104+ return m ? m[1].trim() : '';
105+ };
106+ const author = field('作者') || field('Author') || '社区投稿';
107+ const category = field('分类') || field('Category') || 'basic';
108+ const desc = field('描述') || field('Description') || pr.title;
109+ const scriptName = pr.title.replace(/^\[投稿[::]\s*\w+\]\s*/, '').replace(/^\[投稿\]\s*/, '').trim() || 'untitled';
110+
111+ const catMap = {
112+ '基础脚本': 'basic', 'basic': 'basic',
113+ 'UI 界面': 'ui', 'UI': 'ui', 'ui': 'ui',
114+ '游戏': 'games', 'Games': 'games', 'games': 'games',
115+ '小组件': 'widgets', 'Widgets': 'widgets', 'widgets': 'widgets',
116+ '其他': 'other', 'Other': 'other', 'other': 'other'
117+ };
118+ const catId = catMap[category] || 'basic';
119+
120+ // 5. 基于最新 main 重建分支
77121 execSync('git checkout main', { stdio: 'pipe' });
78- } catch (e) {
79- core.warning(`❌ PR #${pr.number} rebase failed, aborting.`);
80- failed++;
122+ execSync('git pull origin main', { stdio: 'pipe' });
81123
82124 try {
83- execSync(' git rebase --abort' , { stdio: 'pipe' });
125+ execSync(` git branch -D ${branch}` , { stdio: 'pipe' });
84126 } catch (_) {}
85-
127+ execSync(`git checkout -b ${branch}`, { stdio: 'pipe' });
128+
129+ // 6. 写入脚本文件
130+ for (const sc of scriptContents) {
131+ const dir = path.dirname(sc.path);
132+ fs.mkdirSync(dir, { recursive: true });
133+ fs.writeFileSync(sc.path, sc.content, 'utf8');
134+ }
135+
136+ // 7. 读取最新 index.json 并添加条目
137+ const indexPath = 'script_library/index.json';
138+ const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
139+
140+ const safeId = scriptName
141+ .toLowerCase()
142+ .replace(/[\s]+/g, '_')
143+ .replace(/[^a-z0-9_\u4e00-\u9fff]/g, '')
144+ .substring(0, 40) || `submission_${pr.number}`;
145+ const scriptId = `community_${safeId}`;
146+
147+ // 去重:如果已存在则跳过
148+ if (!index.scripts.some(s => s.id === scriptId)) {
149+ const mainScript = scriptContents[0];
150+ const lineCount = mainScript.content.split('\n').length;
151+ const fileType = path.extname(mainScript.path).replace('.', '') || 'py';
152+
153+ index.data_version += 1;
154+ index.updated = new Date().toISOString().replace(/\.\d+Z$/, 'Z');
155+ index.scripts.push({
156+ id: scriptId,
157+ name: scriptName,
158+ name_en: scriptName,
159+ desc: desc,
160+ desc_en: desc,
161+ category: catId,
162+ file: mainScript.path.replace('script_library/', ''),
163+ thumbnail: null,
164+ version: 1,
165+ file_type: fileType,
166+ author: author,
167+ author_en: author,
168+ tags: ['community', catId],
169+ requires: [],
170+ min_app_version: '1.5.0',
171+ added: new Date().toISOString().split('T')[0],
172+ updated: null,
173+ status: 'active',
174+ lines: lineCount
175+ });
176+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + '\n', 'utf8');
177+ }
178+
179+ // 8. 提交并 force push
180+ execSync(`git add -A`, { stdio: 'pipe' });
181+ execSync(`git commit -m "投稿: ${scriptName}" --allow-empty`, { stdio: 'pipe' });
182+ execSync(`git push origin ${branch} --force`, { stdio: 'pipe' });
183+
184+ core.info(`✅ PR #${pr.number} rebuilt successfully on latest main.`);
185+
186+ // 切回 main
86187 execSync('git checkout main', { stdio: 'pipe' });
87188
88- // 在 PR 上留言提示冲突
89- await github.rest.pulls.createReview({
189+ } catch (e) {
190+ core.warning(`❌ PR #${pr.number} rebuild failed: ${e.message}`);
191+ try { execSync('git checkout main', { stdio: 'pipe' }); } catch (_) {}
192+
193+ await github.rest.issues.createComment({
90194 owner: context.repo.owner,
91195 repo: context.repo.repo,
92- pull_number: pr.number,
93- event: 'COMMENT',
94- body: '⚠️ 自动 rebase 失败,此 PR 存在无法自动解决的冲突,需要手动处理。'
196+ issue_number: pr.number,
197+ body: '⚠️ 自动重建失败,可能需要手动处理。'
95198 });
96199 }
97200 }
98-
99- core.info(`\nDone. Succeeded: ${succeeded}, Failed: ${failed}`);
0 commit comments