|
1 | | -name: Auto-merge approved submission |
| 1 | +name: Auto-rebase submission PRs |
2 | 2 |
|
| 3 | +# 每当有 PR 合并到 main 时触发 |
3 | 4 | on: |
4 | | - issues: |
5 | | - types: [labeled] |
| 5 | + pull_request: |
| 6 | + types: [closed] |
| 7 | + branches: [main] |
6 | 8 |
|
7 | 9 | concurrency: |
8 | | - group: approve-submission |
9 | | - cancel-in-progress: false # 不取消排队中的,按顺序跑 |
| 10 | + group: rebase-submissions |
| 11 | + cancel-in-progress: false |
10 | 12 |
|
11 | 13 | jobs: |
12 | | - merge: |
13 | | - if: github.event.label.name == 'approved' |
| 14 | + rebase-open-prs: |
| 15 | + # 只在 PR 被合并(不是关闭)时运行 |
| 16 | + if: github.event.pull_request.merged == true |
14 | 17 | runs-on: ubuntu-latest |
15 | 18 | permissions: |
16 | 19 | contents: write |
17 | | - issues: write |
| 20 | + pull-requests: write |
18 | 21 |
|
19 | 22 | steps: |
20 | 23 | - uses: actions/checkout@v4 |
21 | 24 | with: |
22 | 25 | ref: main |
23 | | - fetch-depth: 1 |
| 26 | + fetch-depth: 0 |
| 27 | + token: ${{ secrets.GITHUB_TOKEN }} |
24 | 28 |
|
25 | | - - name: Parse issue and merge script |
| 29 | + - name: Rebase all open submission PRs |
26 | 30 | uses: actions/github-script@v7 |
27 | 31 | with: |
28 | 32 | script: | |
29 | | - const issue = context.payload.issue; |
30 | | - const body = issue.body || ''; |
31 | | -
|
32 | | - // ── 1. 解析 Issue body ── |
33 | | - const field = (key) => { |
34 | | - const re = new RegExp(`${key}:\\s*(.+)`, 'i'); |
35 | | - const m = body.match(re); |
36 | | - return m ? m[1].trim() : ''; |
37 | | - }; |
38 | | -
|
39 | | - const author = field('作者') || field('Author'); |
40 | | - const category = field('分类') || field('Category'); |
41 | | - const name = field('描述') || issue.title.replace(/^\[投稿\]\s*/, ''); |
42 | | - const desc = field('描述') || ''; |
43 | | - const scriptName = issue.title.replace(/^\[投稿\]\s*/, '').trim(); |
44 | | -
|
45 | | - // 提取代码块 |
46 | | - const codeMatch = body.match(/---\s*代码\s*---\s*\n([\s\S]+)$/i) |
47 | | - || body.match(/```(?:python)?\n([\s\S]+?)```/); |
48 | | - if (!codeMatch) { |
49 | | - await github.rest.issues.createComment({ |
50 | | - owner: context.repo.owner, |
51 | | - repo: context.repo.repo, |
52 | | - issue_number: issue.number, |
53 | | - body: '❌ 无法从 Issue 中提取代码,请检查格式。' |
54 | | - }); |
55 | | - return; |
56 | | - } |
57 | | - const code = codeMatch[1].trimEnd(); |
58 | | -
|
59 | | - // 映射分类 |
60 | | - const catMap = { |
61 | | - '基础脚本': 'basic', 'basic': 'basic', 'Basics': 'basic', |
62 | | - 'UI 界面': 'ui', 'UI': 'ui', 'ui': 'ui', |
63 | | - '游戏': 'games', 'Games': 'games', 'games': 'games', |
64 | | - '小组件': 'widgets', 'Widgets': 'widgets', 'widgets': 'widgets', |
65 | | - '其他': 'other', 'Other': 'other', 'other': 'other' |
66 | | - }; |
67 | | - const catId = catMap[category] || 'basic'; |
68 | | -
|
69 | | - // 生成安全的文件 ID |
70 | | - const safeId = scriptName |
71 | | - .toLowerCase() |
72 | | - .replace(/[\s]+/g, '_') |
73 | | - .replace(/[^a-z0-9_\u4e00-\u9fff]/g, '') |
74 | | - .substring(0, 40) || `submission_${issue.number}`; |
75 | | - const scriptId = `community_${safeId}`; |
76 | | -
|
77 | | - const catDir = { basic: 'basic', ui: 'ui', games: 'games', widgets: 'widgets', other: 'other' }; |
78 | | - const dir = catDir[catId] || 'basic'; |
79 | | -
|
80 | | - // ── 2. 读取现有 index.json ── |
81 | | - const fs = require('fs'); |
82 | | - const indexPath = 'script_library/index.json'; |
83 | | - const indexRaw = fs.readFileSync(indexPath, 'utf8'); |
84 | | - const index = JSON.parse(indexRaw); |
85 | | -
|
86 | | - // 检查重复 |
87 | | - if (index.scripts.some(s => s.id === scriptId)) { |
88 | | - await github.rest.issues.createComment({ |
89 | | - owner: context.repo.owner, |
90 | | - repo: context.repo.repo, |
91 | | - issue_number: issue.number, |
92 | | - body: `⚠️ 脚本 ID \`${scriptId}\` 已存在,跳过。` |
93 | | - }); |
| 33 | + const { execSync } = require('child_process'); |
| 34 | +
|
| 35 | + execSync('git config user.name "github-actions[bot]"'); |
| 36 | + execSync('git config user.email "github-actions[bot]@users.noreply.github.com"'); |
| 37 | +
|
| 38 | + // 获取所有 open 的 PR |
| 39 | + const { data: openPRs } = await github.rest.pulls.list({ |
| 40 | + owner: context.repo.owner, |
| 41 | + repo: context.repo.repo, |
| 42 | + state: 'open', |
| 43 | + base: 'main', |
| 44 | + sort: 'created', |
| 45 | + direction: 'asc' |
| 46 | + }); |
| 47 | +
|
| 48 | + if (openPRs.length === 0) { |
| 49 | + core.info('No open PRs to rebase.'); |
94 | 50 | return; |
95 | 51 | } |
96 | 52 |
|
97 | | - // ── 3. 写入脚本文件 ── |
98 | | - const scriptRelPath = `scripts/${dir}/${scriptId}.py`; |
99 | | - const scriptFullPath = `script_library/${scriptRelPath}`; |
100 | | - const scriptDir = require('path').dirname(scriptFullPath); |
101 | | - fs.mkdirSync(scriptDir, { recursive: true }); |
102 | | - fs.writeFileSync(scriptFullPath, code + '\n', 'utf8'); |
103 | | -
|
104 | | - const lineCount = code.split('\n').length; |
105 | | -
|
106 | | - // ── 4. 更新 index.json ── |
107 | | - index.data_version += 1; |
108 | | - index.updated = new Date().toISOString().replace(/\.\d+Z$/, 'Z'); |
109 | | -
|
110 | | - index.scripts.push({ |
111 | | - id: scriptId, |
112 | | - name: scriptName, |
113 | | - name_en: scriptName, |
114 | | - desc: desc || scriptName, |
115 | | - desc_en: desc || scriptName, |
116 | | - category: catId, |
117 | | - file: scriptRelPath, |
118 | | - thumbnail: null, |
119 | | - version: 1, |
120 | | - file_type: 'py', |
121 | | - author: author || '社区投稿', |
122 | | - author_en: author || 'Community', |
123 | | - tags: ['community', catId], |
124 | | - requires: [], |
125 | | - min_app_version: '1.5.0', |
126 | | - added: new Date().toISOString().split('T')[0], |
127 | | - updated: null, |
128 | | - status: 'active', |
129 | | - lines: lineCount |
130 | | - }); |
| 53 | + core.info(`Found ${openPRs.length} open PR(s) to rebase.`); |
131 | 54 |
|
132 | | - fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + '\n', 'utf8'); |
| 55 | + let succeeded = 0; |
| 56 | + let failed = 0; |
133 | 57 |
|
134 | | - // ── 5. Git commit & push ── |
135 | | - const { execSync } = require('child_process'); |
136 | | - execSync('git config user.name "github-actions[bot]"'); |
137 | | - execSync('git config user.email "github-actions[bot]@users.noreply.github.com"'); |
138 | | - execSync(`git add "${scriptFullPath}" "${indexPath}"`); |
139 | | - execSync(`git commit -m "✅ 收录社区投稿: ${scriptName} (#${issue.number})"`); |
| 58 | + for (const pr of openPRs) { |
| 59 | + const branch = pr.head.ref; |
| 60 | + core.info(`\nRebasing PR #${pr.number}: ${pr.title} (branch: ${branch})`); |
140 | 61 |
|
141 | | - // 推送前拉取最新(防止与其他 workflow 微小时差冲突) |
142 | | - for (let attempt = 0; attempt < 3; attempt++) { |
143 | 62 | try { |
144 | | - execSync('git pull --rebase origin main'); |
145 | | - execSync('git push origin main'); |
146 | | - break; |
| 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++; |
| 75 | +
|
| 76 | + // 切回 main 准备下一个 |
| 77 | + execSync('git checkout main', { stdio: 'pipe' }); |
147 | 78 | } catch (e) { |
148 | | - if (attempt === 2) throw e; |
149 | | - execSync('sleep 3'); |
| 79 | + core.warning(`❌ PR #${pr.number} rebase failed, aborting.`); |
| 80 | + failed++; |
| 81 | +
|
| 82 | + try { |
| 83 | + execSync('git rebase --abort', { stdio: 'pipe' }); |
| 84 | + } catch (_) {} |
| 85 | +
|
| 86 | + execSync('git checkout main', { stdio: 'pipe' }); |
| 87 | +
|
| 88 | + // 在 PR 上留言提示冲突 |
| 89 | + await github.rest.pulls.createReview({ |
| 90 | + owner: context.repo.owner, |
| 91 | + repo: context.repo.repo, |
| 92 | + pull_number: pr.number, |
| 93 | + event: 'COMMENT', |
| 94 | + body: '⚠️ 自动 rebase 失败,此 PR 存在无法自动解决的冲突,需要手动处理。' |
| 95 | + }); |
150 | 96 | } |
151 | 97 | } |
152 | 98 |
|
153 | | - // ── 6. 关闭 Issue 并评论 ── |
154 | | - await github.rest.issues.createComment({ |
155 | | - owner: context.repo.owner, |
156 | | - repo: context.repo.repo, |
157 | | - issue_number: issue.number, |
158 | | - body: [ |
159 | | - `✅ **已收录!** 感谢 @${issue.user?.login || author} 的投稿。`, |
160 | | - '', |
161 | | - `- 📄 脚本: \`${scriptId}.py\``, |
162 | | - `- 📁 分类: ${category}`, |
163 | | - `- 📊 ${lineCount} 行代码`, |
164 | | - '', |
165 | | - '脚本将在用户下次刷新脚本库时可见。' |
166 | | - ].join('\n') |
167 | | - }); |
168 | | -
|
169 | | - await github.rest.issues.update({ |
170 | | - owner: context.repo.owner, |
171 | | - repo: context.repo.repo, |
172 | | - issue_number: issue.number, |
173 | | - state: 'closed' |
174 | | - }); |
175 | | -
|
176 | | - core.info(`Successfully merged: ${scriptId} (${lineCount} lines)`); |
| 99 | + core.info(`\nDone. Succeeded: ${succeeded}, Failed: ${failed}`); |
0 commit comments