Skip to content

[投稿] 网吧模拟器(3.1) — by Daoyu268 #65

[投稿] 网吧模拟器(3.1) — by Daoyu268

[投稿] 网吧模拟器(3.1) — by Daoyu268 #65

name: Rebuild open submission PRs after merge
on:
pull_request:
types: [closed]
branches: [main]
concurrency:
group: rebuild-submissions
cancel-in-progress: false
jobs:
rebuild:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 1
token: ${{ secrets.GITHUB_TOKEN }}
- name: Rebuild open PRs onto latest main
uses: actions/github-script@v7
with:
script: |
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
execSync('git config user.name "github-actions[bot]"');
execSync('git config user.email "github-actions[bot]@users.noreply.github.com"');
// 获取所有 open 的 PR
const { data: openPRs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
base: 'main',
sort: 'created',
direction: 'asc'
});
if (openPRs.length === 0) {
core.info('No open PRs to rebuild.');
return;
}
core.info(`Found ${openPRs.length} open PR(s) to rebuild.`);
for (const pr of openPRs) {
const branch = pr.head.ref;
core.info(`\nRebuilding PR #${pr.number}: ${pr.title} (branch: ${branch})`);
try {
// 1. 获取 PR 的文件变更列表
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number
});
// 2. 找到新增的脚本文件(排除 index.json)
const scriptFiles = files.filter(f =>
f.status === 'added' && f.filename !== 'script_library/index.json'
);
if (scriptFiles.length === 0) {
core.warning(`PR #${pr.number}: No new script files found, skipping.`);
continue;
}
// 3. 从 PR 分支获取每个脚本文件的内容
const scriptContents = [];
for (const sf of scriptFiles) {
try {
const { data: fileData } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: sf.filename,
ref: branch
});
const content = Buffer.from(fileData.content, 'base64').toString('utf8');
scriptContents.push({ path: sf.filename, content });
} catch (e) {
core.warning(`PR #${pr.number}: Failed to read ${sf.filename}: ${e.message}`);
}
}
if (scriptContents.length === 0) {
core.warning(`PR #${pr.number}: Could not read script content, skipping.`);
continue;
}
// 4. 从 PR body 中提取元数据
const body = pr.body || '';
const field = (key) => {
const re = new RegExp(`${key}[::]\\s*(.+)`, 'i');
const m = body.match(re);
return m ? m[1].trim() : '';
};
const author = field('作者') || field('Author') || '社区投稿';
const category = field('分类') || field('Category') || 'basic';
const desc = field('描述') || field('Description') || pr.title;
const scriptName = pr.title.replace(/^\[投稿[::]\s*\w+\]\s*/, '').replace(/^\[投稿\]\s*/, '').trim() || 'untitled';
const catMap = {
'基础脚本': 'basic', 'basic': 'basic',
'UI 界面': 'ui', 'UI': 'ui', 'ui': 'ui',
'游戏': 'games', 'Games': 'games', 'games': 'games',
'小组件': 'widgets', 'Widgets': 'widgets', 'widgets': 'widgets',
'其他': 'other', 'Other': 'other', 'other': 'other'
};
let catId = catMap[category] || 'basic';
// 4b. 基于代码关键词自动修正分类
const codeForClassify = scriptContents.map(s => s.content).join('\n');
if (/\bimport\s+scene\b|from\s+scene\s+import\b|scene\.run\s*\(/.test(codeForClassify)) {
catId = 'games';
core.info(`PR #${pr.number}: Auto-classified as "games" (detected scene module)`);
} else if (/\bimport\s+ui\b|from\s+ui\s+import\b|\.present\s*\(/.test(codeForClassify)) {
catId = 'ui';
core.info(`PR #${pr.number}: Auto-classified as "ui" (detected ui module)`);
} else if (/from\s+widget\s+import\b|__WIDGET_LAYOUT__|Widget\s*\(/.test(codeForClassify)) {
catId = 'widgets';
core.info(`PR #${pr.number}: Auto-classified as "widgets" (detected widget module)`);
}
// 5. 基于最新 main 重建分支
execSync('git checkout main', { stdio: 'pipe' });
execSync('git pull origin main', { stdio: 'pipe' });
try {
execSync(`git branch -D ${branch}`, { stdio: 'pipe' });
} catch (_) {}
execSync(`git checkout -b ${branch}`, { stdio: 'pipe' });
// 6. 写入脚本文件
for (const sc of scriptContents) {
const dir = path.dirname(sc.path);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(sc.path, sc.content, 'utf8');
}
// 7. 读取最新 index.json 并添加条目
const indexPath = 'script_library/index.json';
const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
const safeId = scriptName
.toLowerCase()
.replace(/[\s]+/g, '_')
.replace(/[^a-z0-9_\u4e00-\u9fff]/g, '')
.substring(0, 40) || `submission_${pr.number}`;
const scriptId = `community_${safeId}`;
// 去重:如果已存在则跳过
if (!index.scripts.some(s => s.id === scriptId)) {
const mainScript = scriptContents[0];
const lineCount = mainScript.content.split('\n').length;
const fileType = path.extname(mainScript.path).replace('.', '') || 'py';
index.data_version += 1;
index.updated = new Date().toISOString().replace(/\.\d+Z$/, 'Z');
index.scripts.push({
id: scriptId,
name: scriptName,
name_en: scriptName,
desc: desc,
desc_en: desc,
category: catId,
file: mainScript.path.replace('script_library/', ''),
thumbnail: null,
version: 1,
file_type: fileType,
author: author,
author_en: author,
tags: ['community', catId],
requires: [],
min_app_version: '1.5.0',
added: new Date().toISOString().split('T')[0],
updated: null,
status: 'active',
lines: lineCount
});
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + '\n', 'utf8');
}
// 8. 提交并 force push
execSync(`git add -A`, { stdio: 'pipe' });
execSync(`git commit -m "投稿: ${scriptName}" --allow-empty`, { stdio: 'pipe' });
execSync(`git push origin ${branch} --force`, { stdio: 'pipe' });
core.info(`✅ PR #${pr.number} rebuilt successfully on latest main.`);
// 切回 main
execSync('git checkout main', { stdio: 'pipe' });
} catch (e) {
core.warning(`❌ PR #${pr.number} rebuild failed: ${e.message}`);
try { execSync('git checkout main', { stdio: 'pipe' }); } catch (_) {}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: '⚠️ 自动重建失败,可能需要手动处理。'
});
}
}