Skip to content

feat(agent-gui): 上下文用量环与手动上下文压缩 #58

feat(agent-gui): 上下文用量环与手动上下文压缩

feat(agent-gui): 上下文用量环与手动上下文压缩 #58

Workflow file for this run

name: PR Governance
on:
pull_request_target:
types: [opened, edited, reopened, synchronize, ready_for_review]
permissions:
pull-requests: write
contents: read
concurrency:
group: pr-governance-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
governance:
name: Check PR compliance
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.draft && github.event.pull_request.state == 'open' }}
steps:
- uses: actions/github-script@v8
env:
DRAFT_PAT: ${{ secrets.PR_GOVERNANCE_PAT }}
with:
script: |
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const body = (pr.body || '').replace(/<!--[\s\S]*?-->/g, '');
const MARKER = '<!-- pr-governance-report -->';
// PR(如 dependabot)与维护者显式豁免的 PR 跳过检查。
const labels = pr.labels.map((l) => l.name);
if (pr.user.type === 'Bot' || labels.includes('governance-exempt')) {
core.info('Bot PR or governance-exempt label present, skipping governance checks.');
return;
}
const problems = [];
// 1) 必须关联 issue:优先看 GitHub 解析出的关闭引用,正则兜底
const gql = await github.graphql(
`query ($owner: String!, $repo: String!, $num: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $num) {
closingIssuesReferences(first: 1) { totalCount }
}
}
}`,
{ owner, repo, num: pr.number },
);
const linkedByGithub =
gql.repository.pullRequest.closingIssuesReferences.totalCount > 0;
const linkedByText =
/(clos(?:e|es|ed)|fix(?:es|ed)?|resolv(?:e|es|ed))\s*:?\s+(?:[\w.-]+\/[\w.-]+)?#\d+/i.test(body);
if (!linkedByGithub && !linkedByText) {
problems.push(
'**No linked issue**: the PR body must contain `Closes #123` / `Fixes #123` / `Resolves #123`. ' +
'This project requires an issue before a PR — see the [contribution guidelines](https://github.com/' +
`${owner}/${repo}/blob/main/.github/CONTRIBUTING.md).`,
);
}
// 2) UI 改动必须附截图/预览:改动文件命中前端路径,而正文没有图片即视为缺失。
const UI_PATHS = [
'crates/agent-gui/src/',
'crates/agent-gateway/web/src/',
];
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: pr.number,
per_page: 100,
});
const uiTouched = files.some((f) =>
UI_PATHS.some((p) => f.filename.startsWith(p)),
);
const hasImage =
/!\[[^\]]*\]\([^)]+\)|<img\s|user-attachments\/assets/i.test(body);
if (uiTouched && !hasImage) {
problems.push(
'**UI change without screenshots**: this PR modifies frontend code. Please add before/after screenshots or a recording under "Screenshots / preview" in the PR body.',
);
}
// 3) 必须与基线无冲突。mergeable 由 GitHub 异步计算,null 表示尚未算完,轮询等待。
let mergeable = pr.mergeable ?? null;
for (let i = 0; i < 6 && mergeable === null; i++) {
await new Promise((r) => setTimeout(r, 5000));
const { data } = await github.rest.pulls.get({
owner,
repo,
pull_number: pr.number,
});
mergeable = data.mergeable;
}
if (mergeable === false) {
problems.push(
'**Merge conflicts with the target branch**: merge or rebase the latest base branch and resolve conflicts on your branch before requesting review. Maintainers do not resolve conflicts for you.',
);
}
// 汇总:不合规先尝试转 draft,再按实际结果生成报告,避免文案与真实状态不符。
const passed = problems.length === 0;
// 转失败(未配 PAT / token 失效)只警告:下面的 setFailed 会把检查置红,
// 配合 main 的 required status check 仍能阻止合并,治理不会失效。
let drafted = false;
if (!passed) {
try {
const pat = process.env.DRAFT_PAT;
if (!pat) throw new Error('PR_GOVERNANCE_PAT is not configured');
// 不走 github.graphql:octokit 的 auth 钩子会强制覆盖 authorization 头,传入的 PAT 会被 GITHUB_TOKEN 顶掉,而该 mutation 不接受 integration token。
const res = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
authorization: `Bearer ${pat}`,
'content-type': 'application/json',
},
body: JSON.stringify({
query: `mutation ($id: ID!) {
convertPullRequestToDraft(input: { pullRequestId: $id }) {
pullRequest { isDraft }
}
}`,
variables: { id: pr.node_id },
}),
});
const out = await res.json();
if (!res.ok || out.errors?.length) {
throw new Error(out.errors?.map((e) => e.message).join('; ') || `HTTP ${res.status}`);
}
drafted = true;
} catch (e) {
core.warning(`Failed to convert to draft (${e.message}); the check is still red and merge blocking is unaffected.`);
}
}
const report = passed
? `${MARKER}\n**PR governance checks passed.** Awaiting human review.`
: [
MARKER,
drafted
? '**PR governance checks failed — this PR has been converted to draft.**'
: '**PR governance checks failed.**',
'',
...problems.map((p) => `- ${p}`),
'',
drafted
? 'Fix the items above, then click **Ready for review** to re-run the checks.'
: 'Fix the items above — pushing new commits or editing the PR body will re-run the checks.',
].join('\n');
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: pr.number,
per_page: 100,
});
const existing = comments.find((c) => c.body?.includes(MARKER));
if (existing) {
// 结果没变化就不重复编辑,避免 timeline 噪音。
if (existing.body !== report) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body: report,
});
}
} else if (!passed) {
// 首次即通过的 PR 不发评论,保持安静。
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body: report,
});
}
if (!passed) {
core.setFailed(`PR governance checks failed:\n- ${problems.join('\n- ')}`);
}