Add din_integration/: SWE-bench DinD evaluation pipeline integration #7
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR Quality Check (PR 合入质量看护) | |
| # 触发时机: | |
| # - pull_request_target.opened :PR 新建时触发 | |
| # - pull_request_target.synchronize :PR 中代码内容出现变更(新 commit 推送)时触发 | |
| # - issue_comment.created :在 PR 评论中输入 /pr_check(可附说明)手动触发重检 | |
| # - workflow_dispatch :管理员可手动触发以便回溯校验(非自动触发) | |
| # | |
| # 阻断合入的原理(请管理员在仓库 Settings -> Branches 中配置): | |
| # 1. 本 workflow 中的 Job 名 "PR Quality Gate" 即为一条 status check。 | |
| # 2. 任意一项检查不通过时,本 Job 调用 core.setFailed() 失败,status check 为 failure。 | |
| # 3. 在分支保护规则中把 "PR Quality Gate" 加入 "Required status checks" 后, | |
| # 只要该 check 未通过,GitHub 将不允许合入,从而达到"无论如何都不允许合入"的效果。 | |
| on: | |
| pull_request_target: | |
| types: | |
| - opened # PR 创建 | |
| - synchronize # PR 中代码内容出现变更(push 新 commit) | |
| issue_comment: | |
| types: [created] # 在 PR 评论中输入 /pr_check 触发手动重检 | |
| workflow_dispatch: | |
| permissions: | |
| pull-requests: write | |
| issues: write | |
| contents: read | |
| jobs: | |
| pr-quality-gate: | |
| name: PR Quality Gate | |
| runs-on: ubuntu-latest | |
| # 注意:本工作流的主要目的就是拦截"从 fork 仓库往主仓提的 PR", | |
| # 因此不要用 if 把 fork PR 过滤掉;无论 PR 来自哪,都要执行检查。 | |
| # 阻断原理:检查不通过时调用 core.setFailed(),配合分支保护中的 Required status check 实现拦截。 | |
| # | |
| # 仅 issue_comment 触发时按下面规则过滤(其它事件不受此 if 影响): | |
| # 1) 评论必须挂在 PR 下(不是普通 Issue) | |
| # 2) 评论正文必须为 /pr_check 或以 "/pr_check " 开头(允许附加参数/说明) | |
| if: | | |
| github.event_name != 'issue_comment' || | |
| ( | |
| github.event.issue.pull_request && | |
| ( | |
| github.event.comment.body == '/pr_check' || | |
| startsWith(github.event.comment.body, '/pr_check ') | |
| ) | |
| ) | |
| steps: | |
| - name: 确保失败标签存在(红色) | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| // GitHub 标准红色 d73a4a | |
| const labels = [ | |
| { name: 'large_pr', color: 'd73a4a', description: 'PR 新增代码超过 1000 行,建议拆分为更小的 PR' }, | |
| { name: 'no_issue', color: 'd73a4a', description: 'PR 未关联任何 Issue,合入前必须关联 Issue' }, | |
| { name: 'no_review', color: 'd73a4a', description: 'PR 未包含任何检视意见,合入前必须包含检视意见' }, | |
| { name: 'review_opened', color: 'd73a4a', description: 'PR 中存在未闭环的检视意见,所有检视意见必须闭环后才能合入' }, | |
| { name: 'no_UT', color: 'd73a4a', description: 'feature / bugfix 类型 PR 缺少 tests/UT 下的单元测试新增或修改' } | |
| ]; | |
| for (const label of labels) { | |
| try { | |
| await github.rest.issues.getLabel({ owner, repo, name: label.name }); | |
| console.log(`标签 '${label.name}' 已存在`); | |
| } catch (e) { | |
| if (e.status === 404) { | |
| try { | |
| await github.rest.issues.createLabel({ owner, repo, ...label }); | |
| console.log(`已创建红色标签 '${label.name}'`); | |
| } catch (err) { | |
| console.log(`创建标签 '${label.name}' 失败: ${err.message}`); | |
| } | |
| } else { | |
| throw e; | |
| } | |
| } | |
| } | |
| - name: 执行 PR 合入质量五项检查 | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| // 兼容三种事件来源:pull_request_target / issue_comment / workflow_dispatch | |
| // - pull_request_target:payload 含 pull_request | |
| // - issue_comment :payload 含 issue(PR 评论时 issue.number 即为 PR 号) | |
| // - workflow_dispatch :payload 含 pull_request(GitHub 自动注入) | |
| const prNumber = context.eventName === 'issue_comment' | |
| ? context.payload.issue.number | |
| : context.payload.pull_request.number; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const triggerSource = context.eventName === 'issue_comment' | |
| ? 'PR 评论 /pr_check' | |
| : (context.eventName === 'workflow_dispatch' ? '手动 workflow_dispatch' : context.eventName); | |
| console.log(`开始对 PR #${prNumber} 进行合入质量检查...`); | |
| console.log(`触发来源: ${triggerSource}`); | |
| // 使用 GraphQL 一次性获取全部所需数据 | |
| const query = ` | |
| query($owner: String!, $repo: String!, $number: Int!) { | |
| repository(owner: $owner, name: $repo) { | |
| pullRequest(number: $number) { | |
| additions | |
| body | |
| labels(first: 100) { nodes { name } } | |
| closingIssuesReferences(first: 10) { totalCount } | |
| reviews(first: 100) { | |
| totalCount | |
| nodes { state } | |
| } | |
| reviewThreads(first: 100) { | |
| totalCount | |
| nodes { isResolved } | |
| } | |
| } | |
| } | |
| } | |
| `; | |
| const result = await github.graphql(query, { owner, repo, number: prNumber }); | |
| const prData = result.repository.pullRequest; | |
| const currentLabels = prData.labels.nodes.map(l => l.name); | |
| // 分页拉取 PR 中所有变更的文件(用于检查 tests/UT 是否被改动) | |
| const filesQuery = ` | |
| query($owner: String!, $repo: String!, $number: Int!, $after: String) { | |
| repository(owner: $owner, name: $repo) { | |
| pullRequest(number: $number) { | |
| files(first: 100, after: $after) { | |
| pageInfo { hasNextPage endCursor } | |
| nodes { path } | |
| } | |
| } | |
| } | |
| } | |
| `; | |
| let changedFiles = []; | |
| let fileCursor = null; | |
| let fileHasNext = true; | |
| while (fileHasNext) { | |
| const fileResult = await github.graphql(filesQuery, { | |
| owner, repo, number: prNumber, after: fileCursor | |
| }); | |
| const filesConn = fileResult.repository.pullRequest.files; | |
| changedFiles = changedFiles.concat(filesConn.nodes); | |
| fileHasNext = filesConn.pageInfo.hasNextPage; | |
| fileCursor = filesConn.pageInfo.endCursor; | |
| } | |
| console.log(`[信息] PR 共变更 ${changedFiles.length} 个文件`); | |
| const FAILURE_LABELS = ['large_pr', 'no_issue', 'no_review', 'review_opened', 'no_UT']; | |
| const failures = []; | |
| const failureMessages = []; | |
| // ===== 检查 1:超大 PR(新增代码行数 > 1000) ===== | |
| const additions = prData.additions || 0; | |
| console.log(`[检查 1] PR 新增行数 = ${additions}`); | |
| if (additions > 1000) { | |
| failures.push('large_pr'); | |
| failureMessages.push(`- **超大 PR**:新增代码 ${additions} 行,超过 1000 行上限,建议拆分。`); | |
| } else { | |
| console.log('[检查 1] 通过'); | |
| } | |
| // ===== 检查 2:必须关联 Issue ===== | |
| // 两种方式同时检测: | |
| // a) GraphQL 的 closingIssuesReferences(Fixes/Closes 等关键字 + 侧边栏 Development 链接) | |
| // b) PR body 中是否包含形如 #123 / issues/123 的引用 | |
| const prBody = prData.body || ''; | |
| const closingCount = prData.closingIssuesReferences.totalCount; | |
| const bodyIssueRef = /(?:fixes|closes|resolves|relates to|ref|references)?\s*#\d+|(?:issues\/)\d+/i.test(prBody); | |
| const hasIssueLink = closingCount > 0 || bodyIssueRef; | |
| console.log(`[检查 2] closingIssuesReferences=${closingCount}, body 中含 #N=${bodyIssueRef}`); | |
| if (!hasIssueLink) { | |
| failures.push('no_issue'); | |
| failureMessages.push('- **未关联 Issue**:请在 PR 描述中关联 Issue,例如 `Fixes #123` 或 `Relates to #123`。'); | |
| } else { | |
| console.log('[检查 2] 通过'); | |
| } | |
| // ===== 检查 3:必须至少含一条已提交的检视意见 ===== | |
| const reviews = prData.reviews.nodes; | |
| const submittedReviews = reviews.filter(r => | |
| r.state === 'APPROVED' || r.state === 'CHANGES_REQUESTED' || r.state === 'COMMENTED' | |
| ); | |
| console.log(`[检查 3] 总 review 数 = ${reviews.length}, 已提交 review 数 = ${submittedReviews.length}`); | |
| if (submittedReviews.length === 0) { | |
| failures.push('no_review'); | |
| failureMessages.push('- **无检视意见**:请至少邀请一位 reviewer 提交检视意见(Approve / Request changes / Comment 均可)。'); | |
| } else { | |
| console.log('[检查 3] 通过'); | |
| } | |
| // ===== 检查 4:所有行内检视意见 thread 必须已闭环 ===== | |
| // reviewThread 是 GitHub 的"行内检视意见对话",每条 thread 带有 isResolved 字段; | |
| // 当 thread 数量为 0 时视为通过(不存在需要闭环的对象)。 | |
| const threads = prData.reviewThreads.nodes; | |
| const unresolvedThreads = threads.filter(t => !t.isResolved); | |
| console.log(`[检查 4] reviewThread 总数 = ${threads.length}, 未闭环 = ${unresolvedThreads.length}`); | |
| if (threads.length > 0 && unresolvedThreads.length > 0) { | |
| failures.push('review_opened'); | |
| failureMessages.push(`- **检视意见未闭环**:还存在 ${unresolvedThreads.length} 条未 Resolve 的检视意见,请全部 Resolve 后再合入。`); | |
| } else { | |
| console.log('[检查 4] 通过'); | |
| } | |
| // ===== 检查 5:feature / bugfix 类型 PR 是否包含 UT 变更 ===== | |
| // 规则: | |
| // - 带 feature 标签:必须 tests/UT 路径下有文件新增或修改 | |
| // - 带 bugfix 标签 且 总新增 > 50 行:必须 tests/UT 路径下有文件新增或修改 | |
| // - 其余标签或 bugfix 但新增 ≤ 50:不强制要求 UT | |
| const labelLower = currentLabels.map(l => l.toLowerCase()); | |
| const hasFeature = labelLower.includes('feature'); | |
| const hasBugfix = labelLower.includes('bugfix'); | |
| const UT_DIR_PREFIX = 'tests/UT/'; | |
| const hasUTChange = changedFiles.some(f => f.path && f.path.startsWith(UT_DIR_PREFIX)); | |
| let needUT = false; | |
| let utReason = ''; | |
| if (hasFeature) { | |
| needUT = true; | |
| utReason = 'feature'; | |
| } else if (hasBugfix && additions > 50) { | |
| needUT = true; | |
| utReason = `bugfix 且新增 ${additions} 行 > 50 行`; | |
| } | |
| console.log(`[检查 5] hasFeature=${hasFeature}, hasBugfix=${hasBugfix}, additions=${additions}, hasUTChange=${hasUTChange}, needUT=${needUT}`); | |
| if (needUT && !hasUTChange) { | |
| failures.push('no_UT'); | |
| failureMessages.push(`- **缺少 UT**:PR 含 \`${utReason}\` 标签,但 \`${UT_DIR_PREFIX}\` 路径下没有任何文件新增或修改,请补充相应单元测试。`); | |
| } else if (needUT && hasUTChange) { | |
| console.log(`[检查 5] 通过(含 ${utReason} 标签且 ${UT_DIR_PREFIX} 下有变更)`); | |
| } else { | |
| console.log('[检查 5] 跳过(无需强制 UT)'); | |
| } | |
| // ===== 标签维护:失败则添加对应红色标签,通过则移除 ===== | |
| const labelsToAdd = failures.filter(label => !currentLabels.includes(label)); | |
| const labelsToRemove = currentLabels.filter(label => | |
| FAILURE_LABELS.includes(label) && !failures.includes(label) | |
| ); | |
| for (const label of labelsToRemove) { | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner, repo, issue_number: prNumber, name: label | |
| }); | |
| console.log(`已移除标签: ${label}`); | |
| } catch (e) { | |
| // 404 = 标签本来就不存在,可忽略;403 = fork PR 写权限受限,也可忽略; | |
| // 这两种情况都不应让 Job 失败(status check 是否失败只取决于检查项本身)。 | |
| if (e.status === 404) { | |
| console.log(`标签 ${label} 不存在,跳过移除`); | |
| } else if (e.status === 403) { | |
| console.log(`移除标签 ${label} 失败(fork PR 权限受限,可忽略): ${e.message}`); | |
| } else { | |
| throw e; | |
| } | |
| } | |
| } | |
| if (labelsToAdd.length > 0) { | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner, repo, issue_number: prNumber, labels: labelsToAdd | |
| }); | |
| console.log(`已添加标签: ${labelsToAdd.join(', ')}`); | |
| } catch (e) { | |
| // fork PR 的 GITHUB_TOKEN 通常为只读,写标签会 403; | |
| // 这种情况下静默失败即可——status check 本身仍会按规则失败,照样能阻断合入。 | |
| console.log(`添加标签失败(可能为 fork PR 权限受限,可忽略): ${e.message}`); | |
| } | |
| } | |
| // ===== 输出汇总 + 决定是否失败 ===== | |
| console.log('\n===== PR 合入质量检查结果 ====='); | |
| console.log(`失败项数: ${failures.length}`); | |
| console.log(`失败项列表: ${failures.length === 0 ? '无' : failures.join(', ')}`); | |
| if (failures.length > 0) { | |
| const summary = [ | |
| '🚫 **PR 合入质量检查未通过,PR 已被禁止合入。**', | |
| '', | |
| '未通过项:' + failures.map(f => `\`${f}\``).join('、'), | |
| '', | |
| ...failureMessages, | |
| '', | |
| '请按上述提示整改后,重新推送(push)或评论本 PR 即可触发再次检查。' | |
| ].join('\n'); | |
| // 在 PR 上贴一条可见评论,方便 PR 作者定位问题 | |
| try { | |
| await github.rest.issues.createComment({ | |
| owner, repo, issue_number: prNumber, body: summary | |
| }); | |
| } catch (e) { | |
| console.log(`创建评论失败(可忽略): ${e.message}`); | |
| } | |
| // 让 status check 失败 -> 分支保护若将本 Job 设为 Required,则合入被阻断 | |
| core.setFailed(summary); | |
| } else { | |
| try { | |
| await github.rest.issues.createComment({ | |
| owner, repo, issue_number: prNumber, | |
| body: '✅ PR 合入质量五项检查全部通过,允许合入。' | |
| }); | |
| } catch (e) { | |
| console.log(`创建评论失败(可忽略): ${e.message}`); | |
| } | |
| console.log('✅ 所有检查通过,PR 可合入。'); | |
| } |