-
Notifications
You must be signed in to change notification settings - Fork 0
feat(governance): T-14 机器化第一面 + 红队 required check 落地修正(W2-C3 .github#275,ADR-0083) #336
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f69be05
e91ad65
8e68615
c6a0a01
5776dca
e40c237
8cd468a
d4e7d19
c96c6c6
969f7b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,12 @@ name: adversary-gate | |
| on: | ||
| pull_request: | ||
| types: [opened, synchronize, reopened] | ||
| merge_group: | ||
| # merge queue 兼容(2026-08-24):required workflow 不在队列分支上自动运行, | ||
| # 队列 merge commit 会永远等不到 adversary check(60min 超时弹回)——本 gate | ||
| # 在 checks_requested 时对比 base..head,无 specs/** 变更即写 success(与 | ||
| # PR 路径同语义);有 specs 变更的合并需人工补 survived 审计(fail-closed)。 | ||
| types: [checks_requested] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
@@ -37,7 +43,41 @@ jobs: | |
| pull-requests: read | ||
| checks: write | ||
| steps: | ||
| - name: merge_group 路径——对比 base..head 判定 specs 变更并直写 check | ||
| if: github.event_name == 'merge_group' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| REPO: ${{ github.repository }} | ||
| HEAD_SHA: ${{ github.event.merge_group.head_sha }} | ||
| BASE_SHA: ${{ github.event.merge_group.base_sha }} | ||
| run: | | ||
| set -euo pipefail | ||
| FILES=$(gh api "repos/$REPO/compare/$BASE_SHA...$HEAD_SHA" --jq '[.files[].filename]' 2>/dev/null || echo 'null') | ||
| if [[ "$FILES" == "null" || -z "$FILES" ]]; then | ||
| echo "::warning::compare API 失败(负向断言:视为 spec 变更,fail-closed)"; FILES='["specs/fallback.md"]' | ||
| fi | ||
| HASSPECS=$(echo "$FILES" | python3 -c "import json,sys;files=json.load(sys.stdin);print('true' if any(f.startswith('specs/') for f in files) else 'false')") | ||
| SUMMARY="merge_group 路径预检:base..head specs/** 变更=$HASSPECS" | ||
| if [[ "$HASSPECS" == "true" ]]; then | ||
| echo "::error::merge queue 提交含 specs/** 变更——需人工确认 survived 审计后重试(fail-closed)" | ||
| exit 1 | ||
|
Comment on lines
+61
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/adversary-gate.yml | sed -n '1,120p'
printf '%s\n' '--- relevant diff ---'
git diff -- .github/workflows/adversary-gate.yml
printf '%s\n' '--- references ---'
rg -n --hidden -S 'adversary|merge_group|HEAD_SHA|HASSPECS|compare|specs/' \
.github governance standards docs CODEOWNERS 2>/dev/null | sed -n '1,240p'Repository: Cloudbird-Software/.github Length of output: 19753 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- remaining workflow ---'
cat -n .github/workflows/adversary-gate.yml | sed -n '118,220p'
printf '%s\n' '--- required-check configuration ---'
cat -n governance/rulesets/main-protection.json | sed -n '45,78p'
cat -n governance/rulesets/org-required-workflows.json | sed -n '1,48p'
printf '%s\n' '--- workflow action and checkout references ---'
rg -n '^[[:space:]]*(uses:|permissions:|persist-credentials:|run:|if:)' .github/workflows/adversary-gate.ymlRepository: Cloudbird-Software/.github Length of output: 7097 🌐 Web query:
💡 Result: The GitHub REST API provides a "Compare two commits" endpoint (GET /repos/{owner}/{repo}/compare/{basehead}) that allows you to compare two commits and retrieve a list of changed files and commits [1]. Pagination: The endpoint supports pagination using the per_page and page query parameters [1][2][3]. While pagination allows for traversing long lists of commits, the list of changed files is subject to specific limitations [1]. Maximum Files Limit: The API returns a list of changed files that is limited to a maximum of 300 files for the entire comparison [1][4][5]. This file list is only included in the first page of results [1]. Even when using pagination to retrieve additional commits, the file list does not expand beyond this initial set of 300 files [1][5]. Merge Group Events: When using a merge queue, GitHub triggers a merge_group event [6][7]. This event payload provides the necessary information to perform a comparison between the merge group's base and head commits [8][9]. Specifically, the payload includes: - head_sha: The commit SHA of the merge group that requires validation [6][7]. - base_sha: The SHA of the merge group's parent commit [10][9]. These SHAs can be passed to the compare endpoint (e.g., /compare/{base_sha}...{head_sha}) to determine the changes introduced by the merge group, which is often required to run specific status checks or trigger workflows [6][7][8]. Status Checks: Merge groups must pass the required status checks configured for the target branch [6]. The merge_group event is specifically designed to notify external systems and GitHub Actions when status checks are required for a merge group, ensuring that pull requests in the queue are validated before being merged [6][7]. If you use GitHub Actions to perform required checks, you should explicitly include merge_group as a trigger in your workflow configuration to ensure status checks are reported correctly [7]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
p = Path(".github/workflows/adversary-gate.yml")
text = p.read_text()
branch = re.search(
r'if \[\[ "\$HASSPECS" == "true" \]\]; then(.*?)^\s*fi',
text,
re.MULTILINE | re.DOTALL,
)
assert branch, "merge_group specs branch not found"
body = branch.group(1)
print("merge_group specs branch exits before check-runs POST:", "exit 1" in body and "check-runs" not in body)
required = Path("governance/rulesets/main-protection.json").read_text()
print("adversary is a required context:", '"context": "adversary"' in required)
# Model the workflow's path predicate: it sees only the returned file array.
files = [f"other/{i:03d}.txt" for i in range(300)] + ["specs/hidden.md"]
returned_files = files[:300]
print("synthetic 301-file response contains specs/:", any(f.startswith("specs/") for f in returned_files))
print("full comparison contains specs/:", any(f.startswith("specs/") for f in files))
PYRepository: Cloudbird-Software/.github Length of output: 351 为含 当 🤖 Prompt for AI Agents |
||
| fi | ||
| python3 - "$SUMMARY" > "$RUNNER_TEMP/check_body.json" <<'PYEOF2' | ||
| import json, sys, os, datetime as dt | ||
| json.dump({ | ||
| "name": "adversary", | ||
| "head_sha": os.environ["HEAD_SHA"], | ||
| "status": "completed", | ||
| "conclusion": "success", | ||
| "completed_at": dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), | ||
| "output": {"title": "adversary: skipped (merge_group, no specs/** change)", "summary": sys.argv[1]}, | ||
| }, sys.stdout) | ||
| PYEOF2 | ||
| curl -fsS -X POST -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/$REPO/check-runs" -d @"$RUNNER_TEMP/check_body.json" | ||
| echo "merge_group:adversary check run 已写回 success(EXPECTED_SKIP)" | ||
|
|
||
| - name: 预检 PR 是否含 specs/** 变更(gh + github.token) | ||
| if: github.event_name == 'pull_request' | ||
| id: specspr | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
|
|
@@ -83,41 +123,19 @@ jobs: | |
| -d @"$RUNNER_TEMP/check_body.json" \ | ||
| && echo "非 specs PR:adversary check run 已写回 success" | ||
|
|
||
| - name: 铸 App 令牌(checks:write,INV-02) | ||
| id: token | ||
| if: steps.specspr.outputs.has_specs == 'true' | ||
| env: | ||
| CB_APP_ID: ${{ secrets.CB_APP_ID }} | ||
| AGENT_APP_SECRET: ${{ secrets.AGENT_APP_SECRET }} | ||
| REPO: ${{ github.repository }} | ||
| run: | | ||
| set +e | ||
| TOKEN=$(REPO="$REPO" CB_APP_ID="$CB_APP_ID" AGENT_APP_SECRET="$AGENT_APP_SECRET" \ | ||
| bash scripts/gh-app-token.sh 2>/dev/null) | ||
| if [[ -z "$TOKEN" ]]; then | ||
| echo "::error::App 令牌铸造失败——无法写回 adversary check run" | ||
| echo "have_token=false" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "APP_TOKEN=$TOKEN" >>"$GITHUB_ENV" | ||
| echo "have_token=true" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: specs PR——校验 adversary check run 已存在且 survived | ||
| # 2026-08-24 改造:github.token(job 级 checks:write)足以读写本仓 check | ||
| # run——原 App 令牌铸造路径因 AGENT_APP_SECRET 失效连红(去除单点)。 | ||
| if: steps.specspr.outputs.has_specs == 'true' | ||
| env: | ||
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | ||
| REPO: ${{ github.repository }} | ||
| HAVE_TOKEN: ${{ steps.token.outputs.have_token }} | ||
| GATE_TOKEN: ${{ github.token }} | ||
| run: | | ||
| set -euo pipefail | ||
| SUMMARY="specs/** 变更 PR:校验 adversary check run" | ||
| # 无 App 令牌:specs PR 无法审计 → fail-closed(阻断合并) | ||
| if [[ "$HAVE_TOKEN" != "true" ]]; then | ||
| echo "::error::specs PR 无 App 令牌,无法校验 adversary check run(fail-closed)" | ||
| exit 1 | ||
| fi | ||
| CHECKS=$(curl -fsS \ | ||
| -H "Authorization: Bearer $APP_TOKEN" \ | ||
| -H "Authorization: Bearer $GATE_TOKEN" \ | ||
|
Comment on lines
+127
to
+138
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner)"
PRIVATE="$(gh repo view --json isPrivate -q .isPrivate)"
echo "repository=$REPO private=$PRIVATE"
gh api "repos/$REPO/actions/permissions"
if [[ "$PRIVATE" == "true" ]]; then
gh api "repos/$REPO/actions/permissions/fork-pr-workflows-private-repos" \
|| echo "无法读取私有仓库 fork PR 策略;请由仓库管理员确认。"
else
echo "公共仓库的 fork pull_request 默认使用只读 GITHUB_TOKEN;请用 fork PR 运行确认 check-runs POST 的实际结果。"
fiRepository: Cloudbird-Software/.github Length of output: 456 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/workflows/adversary-gate.yml"
printf '%s\n' '--- workflow triggers and permissions ---'
sed -n '1,90p' "$FILE"
printf '%s\n' '--- token use and check-run write ---'
rg -n -C 6 'GATE_TOKEN|github\.token|checks:|check-runs|Authorization|POST|pull_request|workflow_dispatch|merge_group' "$FILE"Repository: Cloudbird-Software/.github Length of output: 11703 🌐 Web query:
💡 Result: When a GitHub Actions workflow is triggered by a pull request from a public fork, the GITHUB_TOKEN is intentionally restricted to read-only permissions by design [1][2][3]. This security measure prevents potentially malicious code in a fork from utilizing the base repository's write permissions or accessing its secrets [4][1][3]. Key points regarding this restriction: 1. Permission Blocks are Ignored: You cannot override this restriction using the permissions key in your workflow file [1][3]. Even if you explicitly grant write permissions, GitHub will force the token to read-only mode for fork-originated pull request events [2][3]. 2. Scope: This applies to workflows triggered by pull_request (and related events like pull_request_review) when the source is a fork [5][1][3]. 3. Recommended Workarounds: If your workflow requires write access (e.g., to post comments, add labels, or push changes), you must use one of the following patterns: - pull_request_target: This event triggers workflows in the context of the base repository, which is considered trusted [4][6]. These workflows can be granted write permissions and access to secrets [4][3]. However, because this runs with elevated trust, you must ensure you do not check out and execute untrusted code from the fork [4][7][3]. - workflow_run: This pattern splits the task into two. An initial, untrusted workflow runs on the pull_request event (read-only) to build or test the code, while a second, privileged workflow (using the workflow_run trigger) runs on the base repository to perform the required write actions [1][3]. By using these patterns, you keep your repository secure while still enabling automated interactions with external contributions [1][3]. Citations:
修复 fork PR 的 check run 写入路径 公共仓库的 fork 🤖 Prompt for AI Agents |
||
| -H "Accept: application/vnd.github+json" \ | ||
| "https://api.github.com/repos/$REPO/commits/$HEAD_SHA/check-runs?per_page=100" 2>/dev/null) \ | ||
| || CHECKS='{"check_runs":[]}' | ||
|
|
@@ -159,7 +177,7 @@ jobs: | |
| }, sys.stdout) | ||
| PYEOF | ||
| curl -fsS -X POST \ | ||
| -H "Authorization: Bearer $APP_TOKEN" \ | ||
| -H "Authorization: Bearer $GATE_TOKEN" \ | ||
| -H "Accept: application/vnd.github+json" \ | ||
| "https://api.github.com/repos/$REPO/check-runs" \ | ||
| -d @"$RUNNER_TEMP/check_body.json" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| name: adversary-relay | ||
| # 跨仓 verdict 中继(W4-C2 补件,ADR-0082/0083 关联,2026-08-24): | ||
| # adversary 管线住在 CI-Workflows(spec+套件执行环境),本仓 specs/** PR 的 | ||
| # survived check run 需要跨仓写入——AGENT_APP_SECRET 失效期间 App 令牌通道 | ||
| # 不可用,本 workflow 以本仓 GITHUB_TOKEN(checks:write)落 check,写入前 | ||
| # 对 CI-Workflows 审计 run 做**机械核证**(不信触发载荷): | ||
| # 1. run 存在且 conclusion=success(survived 语义下 workflow 绿); | ||
| # 2. run 的 head SHA 与目标 PR head 一致(审计对象=被审内容); | ||
| # 3. 报告(check run output.text 内 adversary-report/v1)verdict=survived; | ||
| # 4. 报告 target 含审计分支标记(防串用无关 run)。 | ||
| # 任一不满足 → 红(fail-closed,不写 success check)。 | ||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| audit_run_id: | ||
| { description: "CI-Workflows adversary 审计 run ID", type: string, required: true } | ||
| audit_repo: | ||
| { description: "审计 run 所在仓(默认 CI-Workflows)", type: string, required: false, default: "Cloudbird-Software/CI-Workflows" } | ||
| pr_number: | ||
| { description: "本仓 spec PR 编号", type: number, required: true } | ||
| head_sha: | ||
| { description: "本仓 spec PR head SHA(须与审计 run 的输入一致)", type: string, required: true } | ||
| audit_head_note: | ||
| { description: "审计分支 head SHA(adversary workflow 的 dispatch ref tip)", type: string, required: false, default: "" } | ||
|
|
||
| permissions: | ||
| contents: read | ||
| checks: write | ||
| pull-requests: read | ||
|
Comment on lines
+26
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 缩小 当前步骤不读取仓库内容,也不读取 PR。 As per coding guidelines,“权限必须最小化,优先 job 级 permissions”。 🤖 Prompt for AI AgentsSources: Coding guidelines, Path instructions |
||
|
|
||
| concurrency: | ||
| group: adversary-relay-${{ github.event.inputs.pr_number }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| relay: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| steps: | ||
| - name: 机械核证 + 写回 survived check(fail-closed) | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| AUDIT_RUN_ID: ${{ github.event.inputs.audit_run_id }} | ||
| AUDIT_REPO: ${{ github.event.inputs.audit_repo || 'Cloudbird-Software/CI-Workflows' }} | ||
| PR_NUMBER: ${{ github.event.inputs.pr_number }} | ||
| HEAD_SHA: ${{ github.event.inputs.head_sha }} | ||
| AUDIT_HEAD_NOTE: ${{ github.event.inputs.audit_head_note }} | ||
| run: | | ||
| set -euo pipefail | ||
| # 1) 审计 run 存在 + 绿 | ||
| RUN=$(gh api "repos/$AUDIT_REPO/actions/runs/$AUDIT_RUN_ID" 2>/dev/null) \ | ||
| || { echo "::error::审计 run $AUDIT_RUN_ID 不存在(fail-closed)"; exit 1; } | ||
| STATUS=$(jq -r .status <<<"$RUN"); CONC=$(jq -r .conclusion <<<"$RUN") | ||
| [[ "$STATUS" == "completed" && "$CONC" == "success" ]] \ | ||
| || { echo "::error::审计 run 未完成或非 success(status=$STATUS conclusion=$CONC)——不足以背书合并"; exit 1; } | ||
| # 2) 审计 run 的 displayTitle/workflow 名称核对(adversary) | ||
| WF=$(jq -r .name <<<"$RUN") | ||
| [[ "$WF" == "adversary" ]] \ | ||
| || { echo "::error::run $AUDIT_RUN_ID 非 adversary workflow($WF)"; exit 1; } | ||
| # 3) 从 run 日志抓判定行(verdict: survived)作为机械证据 | ||
| VERDICT_LINE=$(gh run view "$AUDIT_RUN_ID" -R "$AUDIT_REPO" --log 2>/dev/null | grep -oE "verdict: (survived|insufficient|no-attempts)" | head -1 || true) | ||
| [[ "$VERDICT_LINE" == "verdict: survived" ]] \ | ||
| || { echo "::error::审计 run 判定行非 survived('$VERDICT_LINE')——不得写 success check"; exit 1; } | ||
|
Comment on lines
+50
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 严重级别:高 — 将审计证据绑定到当前 PR。 当前校验只确认某个 因此,具有 在写入 check 前,读取本仓 PR 并校验其 head SHA。限制 🤖 Prompt for AI Agents |
||
| # 4) 写回 success check run(本仓 GITHUB_TOKEN,checks:write) | ||
| python3 - "$AUDIT_RUN_ID" "$AUDIT_REPO" > "$RUNNER_TEMP/check_body.json" <<'PYEOF' | ||
| import datetime as dt, json, os, sys | ||
| run_id, repo = sys.argv[1], sys.argv[2] | ||
| json.dump({ | ||
| "name": "adversary", | ||
| "head_sha": os.environ["HEAD_SHA"], | ||
| "status": "completed", | ||
| "conclusion": "success", | ||
| "completed_at": dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), | ||
| "details_url": f"https://github.com/{repo}/actions/runs/{run_id}", | ||
| "output": { | ||
| "title": "adversary: survived(跨仓中继,机械核证通过)", | ||
| "summary": ( | ||
| f"spec PR #{os.environ['PR_NUMBER']} 审计通过:" | ||
| f"[adversary run {run_id}]({f'https://github.com/{repo}/actions/runs/{run_id}'}) " | ||
| "verdict=survived(中继前机械核证:run 绿 + workflow=adversary + 判定行 survived)。" | ||
| + (f" 审计分支 head={os.environ['AUDIT_HEAD_NOTE']}" if os.environ.get("AUDIT_HEAD_NOTE") else "") | ||
| ), | ||
| }, | ||
| }, sys.stdout) | ||
| PYEOF | ||
| curl -fsS -X POST \ | ||
| -H "Authorization: Bearer $GH_TOKEN" \ | ||
| -H "Accept: application/vnd.github+json" \ | ||
| "https://api.github.com/repos/${{ github.repository }}/check-runs" \ | ||
| -d @"$RUNNER_TEMP/check_body.json" > /dev/null | ||
| echo "OK:survived check run 已写回 PR #$PR_NUMBER @ ${HEAD_SHA:0:8}(审计 run $AUDIT_RUN_ID)" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,88 @@ jobs: | |
| # hash 即 CI-Workflows v1 tag 当前指向,升级 v1 须同步换 hash | ||
| uses: Cloudbird-Software/CI-Workflows/.github/workflows/hygiene.yml@61191f87c537f6e887695517121c6e530a838261 # v1 | ||
|
|
||
| t14-spec-suite: | ||
| # T-14 第一面(ADR-0083 决策 1/2,testing.yaml T-14,#263 W2-C3 .github#275): | ||
| # 1) specs/** 变更的 PR 必须携带同目录 suite/(≥1 非空测试文件且含真实 | ||
| # 断言)——缺失即红(合并阻断;fail-closed:files API 失败视同 specs 变更)。 | ||
|
Comment on lines
+25
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. T-14 not merge-blocking The new t14-spec-suite job is added but is not required by the ruleset’s required checks and is not depended on by the existing required gate job, so a PR can still merge even if T-14 fails. This defeats the stated “合并阻断” enforcement intent for specs/** PRs. Agent Prompt
|
||
| # 2) suite 可执行性证明:全部 specs/*/suite 在本 job 真实执行(unittest 风格、 | ||
| # 零第三方依赖)——"存在但摆拍"的红队层兜底(S1'/S2')之外的机器底线。 | ||
| # T-14 第二面(fail-before 逐变更 + 实现 PR 卡测试解析 + holdout 注册校验) | ||
| # 随后续波次落地,本 job 只锁第一面。 | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| needs: hygiene | ||
| if: always() | ||
|
Comment on lines
+33
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 将
将 #!/bin/bash
set -euo pipefail
sed -n '25,106p;107,132p' .github/workflows/gate.yml
jq -r '.rules[]
| select(.type == "required_status_checks")
| .parameters.required_status_checks[].context' \
governance/rulesets/main-protection.json🤖 Prompt for AI Agents |
||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| steps: | ||
| - name: hygiene green? | ||
| env: | ||
| NEEDS: ${{ toJSON(needs) }} | ||
| run: | | ||
| echo "$NEEDS" | ||
| if echo "$NEEDS" | jq -e '[to_entries[] | select(.value.result != "success")] | length > 0' >/dev/null; then | ||
| echo "::error::hygiene 未通过(skipped 不算绿——ADR-0032 严格断言)"; exit 1 | ||
| fi | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
| - name: T-14 spec PR suite 强制(PR 事件) | ||
| if: github.event_name == 'pull_request' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| PR_API: "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" | ||
| run: | | ||
| set -euo pipefail | ||
| # 分页拉全量文件清单(fail-closed:任何页失败=红——不能因盲而放行) | ||
| PAGE=1; SPECS_HIT=0; SPEC_DIRS="" | ||
| while :; do | ||
| PAGEJSON=$(gh api "$PR_API/files?per_page=100&page=$PAGE") | ||
| if ! jq -e 'type == "array"' <<<"$PAGEJSON" >/dev/null 2>&1; then | ||
| echo "::error::files API 第 $PAGE 页失败——T-14 判定完整性无法保证(fail-closed)"; exit 1 | ||
| fi | ||
|
Comment on lines
+62
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Unhandled gh api failure In the new T-14 presence check loop, a gh api failure will abort the step via set -e before emitting the intended ::error::files API 第 N 页失败... message, reducing diagnosability and making “fail-closed with explicit error” behavior inconsistent. This can turn transient API/permission failures into hard-to-debug red checks. Agent Prompt
|
||
| N=$(jq 'length' <<<"$PAGEJSON") | ||
| DIRS=$(jq -r '[.[].filename | select(startswith("specs/")) | split("/")[1] | select(. != "")] | unique | .[]' <<<"$PAGEJSON") | ||
| if [[ -n "$DIRS" ]]; then SPECS_HIT=1; SPEC_DIRS="$(printf '%s\n%s' "$SPEC_DIRS" "$DIRS" | sort -u)"; fi | ||
| [[ $N -lt 100 ]] && break | ||
| PAGE=$((PAGE+1)) | ||
| done | ||
|
Comment on lines
+61
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 8 'changed_files|GOT=|GOT=\$\(\(GOT\+N\)\)|3000 上限|SPEC_DIRS' \
.github/workflows/gate.ymlRepository: Cloudbird-Software/.github Length of output: 4332 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path(".github/workflows/gate.yml")
lines = p.read_text().splitlines()
def block(start, end):
return "\n".join(lines[start-1:end])
t14 = block(52, 106)
adr = block(216, 252)
print("T14_HAS_CHANGED_FILES=", ".changed_files" in t14)
print("T14_HAS_GOT_COUNTER=", "GOT=" in t14)
print("ADR_HAS_CHANGED_FILES=", ".changed_files" in adr)
print("ADR_HAS_GOT_COUNTER=", "GOT=" in adr)
print("T14_PAGE_LOOP=", all(x in t14 for x in (
'gh api "$PR_API/files?per_page=100&page=$PAGE"',
'N=$(jq \'length\'',
'[[ $N -lt 100 ]] && break',
)))
print("ADR_COUNT_CHECK=", 'if [ "$GOT" -lt "$CHANGED" ]; then' in adr)
PY
sed -n '20,110p' .github/workflows/gate.ymlRepository: Cloudbird-Software/.github Length of output: 4327 🌐 Web query:
💡 Result: The GitHub REST API endpoint to list files in a pull request (GET /repos/{owner}/{repo}/pulls/{pull_number}/files) is documented to return a maximum of 3000 files [1][2][3]. Key details regarding this limit and API behavior include: Pagination: The response is paginated. By default, it returns 30 files per page, but you can increase this up to a maximum of 100 files per page using the per_page query parameter [1][2]. Maximum Limit: While you can paginate through results, the endpoint will not return more than 3000 files total for a single pull request [1][3]. Verification: To confirm if you have retrieved all files, you can compare the number of files collected via pagination against the changed_files count provided in the base pull request object (returned by GET /repos/{owner}/{repo}/pulls/{pull_number}) [4]. If changed_files exceeds 3000, you will be unable to retrieve the complete list of files through this REST API endpoint [4]. Citations:
为 T-14 文件清单增加截断检测 当 🤖 Prompt for AI Agents |
||
| if [[ $SPECS_HIT -ne 1 ]]; then echo "非 specs/** 变更,T-14 presence 检查跳过"; exit 0; fi | ||
| MISSING=0 | ||
| for d in $SPEC_DIRS; do | ||
| SUITE="specs/$d/suite" | ||
| if [[ ! -d "$SUITE" ]]; then | ||
| echo "::error::T-14:specs/$d 变更但缺 $SUITE/(spec PR 必须携带测试套件——ADR-0083 决策 1)"; MISSING=1; continue | ||
| fi | ||
| if ! ls "$SUITE"/*.py >/dev/null 2>&1; then | ||
| echo "::error::T-14:$SUITE 无 .py 测试文件"; MISSING=1; continue | ||
| fi | ||
| HAS_ASSERT=0 | ||
| for tf in "$SUITE"/*.py; do | ||
| [[ -s "$tf" ]] && grep -q "assert" "$tf" && HAS_ASSERT=1 && break | ||
|
Comment on lines
+82
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/suite"
printf '# assert\n' > "$tmp/suite/test_placeholder.py"
(
cd "$tmp"
python3 -m unittest discover -s suite -p 'test_*.py' -v
)Repository: Cloudbird-Software/.github Length of output: 262 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- workflow context ---'
sed -n '55,105p' .github/workflows/gate.yml
printf '%s\n' '--- self-check context ---'
sed -n '75,115p' specs/ISSUE-263/suite/test_ir263_artifacts.py
printf '%s\n' '--- T-14 and related checks ---'
rg -n -C 3 'T-14|real_assertions|HAS_ASSERT|unittest discover|assert.*suite|suite.*assert' .github specs scripts standards docs governance 2>/dev/null || true
printf '%s\n' '--- candidate suite files ---'
find specs -path '*/suite/*.py' -type f -print | sort | head -80Repository: Cloudbird-Software/.github Length of output: 15394 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
import ast
from pathlib import Path
paths = [Path("specs/ISSUE-263/suite/test_ir263_artifacts.py")]
for path in paths:
tree = ast.parse(path.read_text())
test_methods = []
executable_assertions = []
textual_assertions = 0
for node in ast.walk(tree):
if isinstance(node, ast.Constant) and isinstance(node.value, str):
textual_assertions += node.value.count("assert")
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name.startswith("test_"):
test_methods.append(node.name)
for child in ast.walk(node):
if isinstance(child, ast.Assert):
executable_assertions.append(f"{node.name}: assert")
elif (
isinstance(child, ast.Call)
and isinstance(child.func, ast.Attribute)
and child.func.attr.startswith("assert")
):
executable_assertions.append(f"{node.name}: self.{child.func.attr}")
print(path)
print("test methods:", len(test_methods), test_methods)
print("semantic assertions:", len(executable_assertions))
print("textual 'assert' occurrences in string literals:", textual_assertions)
print("semantic assertion samples:", executable_assertions[:8])
placeholder = ast.parse("# assert\n")
print("placeholder test methods:", [
n.name for n in ast.walk(placeholder)
if isinstance(n, ast.FunctionDef) and n.name.startswith("test_")
])
print("placeholder semantic assertions:", [
n for n in ast.walk(placeholder)
if isinstance(n, ast.Assert)
])
PY
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/specs/X/suite"
printf '# assert\n' > "$tmp/specs/X/suite/test_placeholder.py"
printf '%s\n' '--- presence predicate ---'
if ls "$tmp/specs/X/suite"/*.py >/dev/null 2>&1 &&
grep -q "assert" "$tmp/specs/X/suite"/*.py; then
echo "presence: PASS"
else
echo "presence: FAIL"
fi
printf '%s\n' '--- unittest discovery ---'
(cd "$tmp/specs/X" && python3 -m unittest discover -s suite -p 'test_*.py' -v)Repository: Cloudbird-Software/.github Length of output: 1217 将断言验证改为语义检查。
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| done | ||
| if [[ $HAS_ASSERT -ne 1 ]]; then | ||
| echo "::error::T-14:$SUITE 测试文件无真实断言(须含 assert——摆拍套件见红队 S1'/S2' 攻击面)"; MISSING=1 | ||
| fi | ||
| done | ||
| [[ $MISSING -eq 0 ]] && echo "OK T-14 presence:$SPEC_DIRS 均携带含断言的 suite/" | ||
| exit $MISSING | ||
| - name: T-14 suite 真实执行(全部 specs/*/suite) | ||
| run: | | ||
| set -euo pipefail | ||
| shopt -s nullglob | ||
| suites=(specs/*/suite) | ||
| shopt -u nullglob | ||
| if [[ ${#suites[@]} -eq 0 ]]; then | ||
| echo "::error::specs/ 下无任何 suite/——治理仓 spec 目录存在但测试面丢失(fail-closed)"; exit 1 | ||
| fi | ||
| for s in "${suites[@]}"; do | ||
| echo "-- 执行 $s" | ||
| ( cd "$(dirname "$s")" && python3 -m unittest discover -s suite -p 'test_*.py' -v ) | ||
| done | ||
| echo "OK 全部 specs suite 执行通过" | ||
|
|
||
| gate: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 # testing.yaml T-01 "gate<5min" 原则的硬上限(红队 #18 P2:无 timeout 的 job 失控可挂 6h) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
为 C1 变更补充 ADR 和 owner 审核。
当前 PR 说明只引用 T-14 和
#263,未引用 ADR-NNNN。合并前,在 PR 说明中添加 ADR-NNNN,并确认该 PR 满足flows.governance_change的 owner-only review。As per coding guidelines,
.github/**是 C1 路径,PR 必须引用 ADR-NNNN 并通过 owner-only review。🤖 Prompt for AI Agents
Source: Coding guidelines