Repository navigation
feat(ISSUE-263): backfill g060-guard + adversary-gate workflows #313
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 1 commit
260f843
281ba06
6fa1047
65c9941
3dfca31
18ab927
c2cfd70
ee018a9
210a06b
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 | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,196 @@ | ||||||||||||||||||||||||
| name: adversary-gate | ||||||||||||||||||||||||
| # W4-C3 adversary gate(Cloudbird-Software/.github#284,AC-14/AC-19,ADR-0067/0082) | ||||||||||||||||||||||||
| # | ||||||||||||||||||||||||
| # 目标:specs/** 路径 PR 必须含 adversary check(漏配/摘除/跳过即红);开发路径 | ||||||||||||||||||||||||
| # 豁免谓词由 diff 路径集确定性派生(禁人工打标,AC-14)。 | ||||||||||||||||||||||||
| # | ||||||||||||||||||||||||
| # 机制: | ||||||||||||||||||||||||
| # - 本 workflow 在每 PR 上运行(org-required-workflows required workflow), | ||||||||||||||||||||||||
| # 产出名为 "adversary" 的 check run(与 main-protection.json required_status_checks | ||||||||||||||||||||||||
| # 登记的 context 一致)。 | ||||||||||||||||||||||||
| # - 纯开发路径 PR(diff 无 specs/** 实质内容变更):经 expected_skip.py 判定 | ||||||||||||||||||||||||
| # EXPECTED_SKIP=True → 写 "success"(留痕 reason),不阻塞合并。 | ||||||||||||||||||||||||
| # - specs/** 实质变更 PR(未全命中 owner 豁免清单):EXPECTED_SKIP=False → | ||||||||||||||||||||||||
| # 必须由 adversary workflow(W3-C1/W4-C2)产出 verdict=survived 的 adversary | ||||||||||||||||||||||||
| # check run 才能合并;本 gate 检查该 check run 是否存在且结论为 success—— | ||||||||||||||||||||||||
| # 不存在/结论非 success 即红(fail-closed,负向断言)。 | ||||||||||||||||||||||||
| # | ||||||||||||||||||||||||
| # 部署注意: | ||||||||||||||||||||||||
| # - 本文件 + CI-Workflows .github/workflows/adversary.yml 当前 App 无 | ||||||||||||||||||||||||
| # workflows 权限,待权限到位后补推;rulesets JSON 已含 adversary context。 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||
| pull_request: | ||||||||||||||||||||||||
| types: [opened, synchronize, reopened] | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| permissions: | ||||||||||||||||||||||||
| contents: read | ||||||||||||||||||||||||
| pull-requests: read | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| concurrency: | ||||||||||||||||||||||||
| group: adversary-gate-${{ github.event.pull_request.number }} | ||||||||||||||||||||||||
| cancel-in-progress: false | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||
| gate: | ||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||
| timeout-minutes: 5 | ||||||||||||||||||||||||
| permissions: | ||||||||||||||||||||||||
| contents: read | ||||||||||||||||||||||||
| pull-requests: read | ||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||
| - name: Checkout pipeline/adversary(expected_skip.py) | ||||||||||||||||||||||||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||
| persist-credentials: false | ||||||||||||||||||||||||
| sparse-checkout: | | ||||||||||||||||||||||||
| pipeline/adversary/expected_skip.py | ||||||||||||||||||||||||
| sparse-checkout-cone-mode: false | ||||||||||||||||||||||||
|
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 | 🔴 Critical | ⚡ Quick win 必须将 当前检出仅包含 开发路径会跳过 success check 写回。 建议修改 sparse-checkout: |
pipeline/adversary/expected_skip.py
+ scripts/gh-app-token.sh📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: 派生 PR diff 路径集 + EXPECTED_SKIP(禁人工打标,AC-14) | ||||||||||||||||||||||||
| id: skip | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| GH_TOKEN: ${{ github.token }} | ||||||||||||||||||||||||
| PR_API: "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| set -euo pipefail | ||||||||||||||||||||||||
| set +e | ||||||||||||||||||||||||
| FILES=$(gh api "$PR_API/files?per_page=300" \ | ||||||||||||||||||||||||
| --jq '[.[].filename]' 2>/dev/null) | ||||||||||||||||||||||||
|
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 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- workflow context ---'
cat -n .github/workflows/adversary-gate.yml | sed -n '1,125p'
printf '%s\n' '--- repository references ---'
rg -n --hidden --glob '!node_modules' 'expected_skip|PR_API/files|per_page=|gh api .*files|AC-14' .
printf '%s\n' '--- workflow diff summary ---'
git diff --statRepository: Cloudbird-Software/.github Length of output: 15561 🌐 Web query:
💡 Result: The official GitHub REST API documentation confirms that for the List pull requests files endpoint (GET /repos/{owner}/{repo}/pulls/{pull_number}/files), the per_page query parameter has a maximum value of 100 [1][2]. Key details regarding this parameter and pagination include: - Default Value: The default number of results returned per page is 30 [1][2]. - Maximum Value: The maximum allowed value for per_page is 100 [1][2]. - Behavior for Exceeding Maximum: If you specify a per_page value greater than 100, the GitHub API does not return an error [3][4]. Instead, it automatically clamps (reduces) the value to the maximum (100) and returns the results for that page without notifying you that the requested value was ignored [3][4]. - Pagination Usage: You can use the link header provided in the API response to retrieve subsequent pages of data [3][4]. For further implementation details, refer to the official GitHub documentation on using pagination in the REST API [3][5]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- remaining workflow ---'
cat -n .github/workflows/adversary-gate.yml | sed -n '120,210p'
printf '%s\n' '--- expected_skip implementation and tests ---'
fd -i 'expected_skip.py' .
EXPECTED_SKIP=$(fd -i 'expected_skip.py' . | head -n 1)
cat -n "$EXPECTED_SKIP" | sed -n '1,260p'
printf '%s\n' '--- relevant acceptance criteria ---'
cat -n specs/ISSUE-263/spec.md | sed -n '45,85p'Repository: Cloudbird-Software/.github Length of output: 8325 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- tracked pipeline files ---'
git ls-files | rg '(^|/)pipeline/|expected_skip'
printf '%s\n' '--- pagination output-shape probe ---'
if command -v jq >/dev/null 2>&1; then
printf '%s\n' 'single-page jq:'
printf '%s\n' '[[{"filename":"a"},{"filename":"b"}],[{"filename":"c"}]]' |
jq '[.[].filename]' || true
printf '%s\n' 'slurped-pages jq candidates:'
printf '%s\n' '[[{"filename":"a"},{"filename":"b"}],[{"filename":"c"}]]' |
jq '[.[][] | .filename]'
else
echo 'jq unavailable'
fi
printf '%s\n' '--- exact API call forms in repository ---'
cat -n .github/workflows/gate.yml | sed -n '135,155p'
cat -n .github/workflows/post-merge-verify.yml | sed -n '100,112p'Repository: Cloudbird-Software/.github Length of output: 195 🏁 Script executed: #!/bin/bash
printf '%s\n' '--- tracked expected_skip and token paths ---'
git ls-files | grep -E '(^|/)(pipeline/adversary/expected_skip\.py|scripts/gh-app-token\.sh)$' || true
printf '%s\n' '--- all tracked pipeline paths ---'
git ls-files | grep -E '(^|/)pipeline/' | head -50 || true
printf '%s\n' '--- pagination output-shape probe ---'
if command -v jq >/dev/null 2>&1; then
printf '%s\n' '[[{"filename":"a"},{"filename":"b"}],[{"filename":"c"}]]' | jq '[.[].filename]' || true
printf '%s\n' '[[{"filename":"a"},{"filename":"b"}],[{"filename":"c"}]]' | jq '[.[][] | .filename]' || true
else
echo 'jq unavailable'
fi
printf '%s\n' '--- sparse checkout and token references ---'
cat -n .github/workflows/adversary-gate.yml | sed -n '41,86p'Repository: Cloudbird-Software/.github Length of output: 2884 必须分页获取完整的 PR 文件列表。 GitHub 会将 使用 🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| RC=$? | ||||||||||||||||||||||||
|
Comment on lines
+47
to
+49
Comment on lines
+48
to
+49
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. Specs detection under-fetch adversary-gate precheck uses a single-page gh api .../files?per_page=300 call; GitHub caps per_page at 100 and oversized values are silently clamped, so specs/** changes can be missed when the PR touches >100 files and appear later pages. That can incorrectly set has_specs=false and write a green "adversary" check run, bypassing the intended gate for specs PRs. Agent Prompt
|
||||||||||||||||||||||||
| set -e | ||||||||||||||||||||||||
| if [[ $RC -ne 0 || -z "$FILES" || "$FILES" == "null" ]]; then | ||||||||||||||||||||||||
| echo "::warning::取 PR files 失败(负向断言:视为 spec 变更)" | ||||||||||||||||||||||||
| FILES='["specs/_api_failed_fallback/spec.md"]' | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
| # 调 expected_skip.py judge 派生(确定性,禁人工打标) | ||||||||||||||||||||||||
| SKIP_OUT=$(python3 pipeline/adversary/expected_skip.py judge --paths "$FILES" 2>/dev/null) \ | ||||||||||||||||||||||||
| || SKIP_OUT='{"expected_skip":false,"reason":"expected_skip.py 执行失败(fail-closed)","specs_paths":[]}' | ||||||||||||||||||||||||
| echo "$SKIP_OUT" > "$RUNNER_TEMP/skip_result.json" | ||||||||||||||||||||||||
| SKIP=$(echo "$SKIP_OUT" | python3 -c "import json,sys;print(str(json.load(sys.stdin)['expected_skip']).lower())" 2>/dev/null || echo "false") | ||||||||||||||||||||||||
| echo "expected_skip=$SKIP" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||
| echo "skip_result=$SKIP_OUT" | ||||||||||||||||||||||||
|
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 必须将 Line 71 只写入日志,不会创建 使用多行 建议修改 echo "expected_skip=$SKIP" >> "$GITHUB_OUTPUT"
- echo "skip_result=$SKIP_OUT"
+ {
+ echo "skip_result<<EOF"
+ echo "$SKIP_OUT"
+ echo "EOF"
+ } >> "$GITHUB_OUTPUT"
echo "EXPECTED_SKIP=$SKIP"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| echo "EXPECTED_SKIP=$SKIP" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: 铸 App 令牌(checks:write,INV-02) | ||||||||||||||||||||||||
| id: token | ||||||||||||||||||||||||
| 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: 开发路径豁免——写 success check run(EXPECTED_SKIP=True) | ||||||||||||||||||||||||
| if: steps.skip.outputs.expected_skip == 'true' && steps.token.outputs.have_token == 'true' | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| SKIP_RESULT: ${{ steps.skip.outputs.skip_result }} | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| set -euo pipefail | ||||||||||||||||||||||||
| SUMMARY=$(echo "$SKIP_RESULT" | python3 -c " | ||||||||||||||||||||||||
| import json,sys | ||||||||||||||||||||||||
| d=json.load(sys.stdin) | ||||||||||||||||||||||||
| lines=['EXPECTED_SKIP=True(开发路径豁免,AC-14)',d.get('reason','')] | ||||||||||||||||||||||||
| sp=d.get('specs_paths',[]) | ||||||||||||||||||||||||
| if sp: lines.append('specs_paths(已豁免): '+', '.join(sp)) | ||||||||||||||||||||||||
| lines.append('派生方式=diff路径集确定性派生(禁人工打标)') | ||||||||||||||||||||||||
| sys.stdout.write('\\n'.join(lines)) | ||||||||||||||||||||||||
| ") | ||||||||||||||||||||||||
| python3 - "$SUMMARY" > "$RUNNER_TEMP/check_body.json" <<'PYEOF' | ||||||||||||||||||||||||
| import json, sys, datetime as dt | ||||||||||||||||||||||||
| summary = sys.argv[1] | ||||||||||||||||||||||||
| body = { | ||||||||||||||||||||||||
| "name": "adversary", | ||||||||||||||||||||||||
| "head_sha": "${{ github.event.pull_request.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 (dev path EXPECTED_SKIP)", "summary": summary}, | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| json.dump(body, sys.stdout) | ||||||||||||||||||||||||
| PYEOF | ||||||||||||||||||||||||
| curl -fsS -X POST \ | ||||||||||||||||||||||||
| -H "Authorization: Bearer $APP_TOKEN" \ | ||||||||||||||||||||||||
| -H "Accept: application/vnd.github+json" \ | ||||||||||||||||||||||||
| "https://api.github.com/repos/${{ github.repository }}/check-runs" \ | ||||||||||||||||||||||||
| -d @"$RUNNER_TEMP/check_body.json" \ | ||||||||||||||||||||||||
| && echo "开发路径:adversary check run 已写回 success(EXPECTED_SKIP)" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: specs/** PR——校验 adversary check run 已存在且 survived | ||||||||||||||||||||||||
| if: steps.skip.outputs.expected_skip == 'false' | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| SKIP_RESULT: ${{ steps.skip.outputs.skip_result }} | ||||||||||||||||||||||||
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | ||||||||||||||||||||||||
| REPO: ${{ github.repository }} | ||||||||||||||||||||||||
| HAVE_TOKEN: ${{ steps.token.outputs.have_token }} | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| set -euo pipefail | ||||||||||||||||||||||||
| SUMMARY=$(echo "$SKIP_RESULT" | python3 -c " | ||||||||||||||||||||||||
| import json,sys | ||||||||||||||||||||||||
| d=json.load(sys.stdin) | ||||||||||||||||||||||||
| sp=d.get('remaining_specs',[]) or d.get('specs_paths',[]) | ||||||||||||||||||||||||
| head=', '.join(sp[:5])+('...' if len(sp)>5 else '') | ||||||||||||||||||||||||
| sys.stdout.write('EXPECTED_SKIP=False: specs/'+head) | ||||||||||||||||||||||||
| ") | ||||||||||||||||||||||||
| # 查 head sha 上名为 adversary 的 check run(取 completed 最新) | ||||||||||||||||||||||||
| if [[ "$HAVE_TOKEN" != "true" ]]; then | ||||||||||||||||||||||||
| echo "::error::无 App 令牌,无法校验 adversary check run(fail-closed)" | ||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
| CHECKS=$(curl -fsS \ | ||||||||||||||||||||||||
| -H "Authorization: Bearer $APP_TOKEN" \ | ||||||||||||||||||||||||
| -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":[]}' | ||||||||||||||||||||||||
| VERDICT=$(echo "$CHECKS" | python3 -c " | ||||||||||||||||||||||||
| import json,sys | ||||||||||||||||||||||||
| runs=json.loads(sys.stdin.read()).get('check_runs',[]) | ||||||||||||||||||||||||
| adv=sorted([r for r in runs if r.get('name')=='adversary'], key=lambda r:(r.get('status')!='completed',)) | ||||||||||||||||||||||||
| if not adv: | ||||||||||||||||||||||||
| print('MISSING') | ||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||
| a=adv[-1] | ||||||||||||||||||||||||
| if a.get('status')=='completed' and a.get('conclusion')=='success': | ||||||||||||||||||||||||
|
Comment on lines
+126
to
+132
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 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- workflow outline ---'
ast-grep outline .github/workflows/adversary-gate.yml --view expanded || true
printf '%s\n' '--- relevant workflow sections ---'
cat -n .github/workflows/adversary-gate.yml | sed -n '1,210p'
printf '%s\n' '--- related identifiers and check-run fields ---'
rg -n -C 3 'check_runs|completed_at|conclusion|adversary|skip_result|gh-app-token|app\.id|actions/' .github/workflows/adversary-gate.yml .github scripts pipeline 2>/dev/null || trueRepository: Cloudbird-Software/.github Length of output: 50382 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- adversary app configuration ---'
rg -n -i -C 4 'adversary.*app|app.*adversary|adversary_app|app_id|app\.id|cloudbrid-agent|sender|creator' \
.github governance standards docs REPOS.yaml expected-state.json 2>/dev/null || true
printf '%s\n' '--- adversary workflow references ---'
rg -n -i -C 5 'name: adversary|check-runs|create.*check|verdict|survived|adversary' \
.github/workflows pipeline scripts 2>/dev/null | head -n 300 || true
printf '%s\n' '--- sorting behavior with representative check runs ---'
python3 - <<'PY'
import json
def verdict(runs):
adv = sorted(
[r for r in runs if r.get("name") == "adversary"],
key=lambda r: (r.get("status") != "completed",),
)
if not adv:
return "MISSING"
a = adv[-1]
if a.get("status") == "completed" and a.get("conclusion") == "success":
return "SURVIVED"
if a.get("status") == "completed":
return "RED:" + str(a.get("conclusion"))
return "PENDING:" + str(a.get("status"))
cases = {
"newer_failure_after_older_success": [
{"id": 101, "name": "adversary", "status": "completed",
"conclusion": "success", "completed_at": "2026-08-01T00:00:00Z"},
{"id": 102, "name": "adversary", "status": "completed",
"conclusion": "failure", "completed_at": "2026-08-02T00:00:00Z"},
],
"newer_success_after_older_failure": [
{"id": 201, "name": "adversary", "status": "completed",
"conclusion": "failure", "completed_at": "2026-08-01T00:00:00Z"},
{"id": 202, "name": "adversary", "status": "completed",
"conclusion": "success", "completed_at": "2026-08-02T00:00:00Z"},
],
"pending_after_completed_success": [
{"id": 301, "name": "adversary", "status": "completed",
"conclusion": "success", "completed_at": "2026-08-01T00:00:00Z"},
{"id": 302, "name": "adversary", "status": "in_progress",
"conclusion": None, "completed_at": None},
],
"same_name_from_other_app": [
{"id": 401, "name": "adversary", "status": "completed",
"conclusion": "success", "app": {"id": 999}},
],
}
for name, runs in cases.items():
print(name, "=>", verdict(runs))
PYRepository: Cloudbird-Software/.github Length of output: 50384 🌐 Web query:
💡 Result: The GitHub REST API endpoint to list check runs for a Git reference ( Citations:
按 App ID 过滤并显式选择最新 completed run 当前逻辑只匹配 🤖 Prompt for AI Agents
Comment on lines
+127
to
+132
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. 4. Wrong adversary run chosen For specs PRs, adversary-gate sorts check runs only by completion status and then selects adv[-1], which can pick the oldest completed run (or a pending run) instead of the most recent run for the head SHA. This can incorrectly allow merges when the latest adversary run failed, or incorrectly block merges when an older run failed but the latest succeeded. Agent Prompt
|
||||||||||||||||||||||||
| print('SURVIVED') | ||||||||||||||||||||||||
| elif a.get('status')=='completed': | ||||||||||||||||||||||||
| print('RED:'+str(a.get('conclusion'))) | ||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||
| print('PENDING:'+str(a.get('status'))) | ||||||||||||||||||||||||
| ") | ||||||||||||||||||||||||
| if [[ "$VERDICT" == "SURVIVED" ]]; then | ||||||||||||||||||||||||
| echo "adversary check run 已存在且 survived:$SUMMARY" | ||||||||||||||||||||||||
| exit 0 | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
| # 未审计/未 survived:写 failure check run(阻断合并,AC-4 负向断言) | ||||||||||||||||||||||||
| if [[ "$VERDICT" == MISSING* ]]; then | ||||||||||||||||||||||||
| TITLE="adversary: 缺失(specs/** PR 未含 adversary check,阻断)" | ||||||||||||||||||||||||
| else | ||||||||||||||||||||||||
| TITLE="adversary: ${VERDICT}(specs/** PR 审计未通过,阻断)" | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
| python3 - "$TITLE" "$SUMMARY" > "$RUNNER_TEMP/check_body.json" <<'PYEOF' | ||||||||||||||||||||||||
| import json, sys, datetime as dt | ||||||||||||||||||||||||
| title, summary = sys.argv[1], sys.argv[2] | ||||||||||||||||||||||||
| json.dump({ | ||||||||||||||||||||||||
| "name": "adversary", | ||||||||||||||||||||||||
| "head_sha": "${{ github.event.pull_request.head.sha }}", | ||||||||||||||||||||||||
| "status": "completed", | ||||||||||||||||||||||||
| "conclusion": "failure", | ||||||||||||||||||||||||
| "completed_at": dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), | ||||||||||||||||||||||||
| "output": {"title": title[:255], "summary": summary}, | ||||||||||||||||||||||||
| }, sys.stdout) | ||||||||||||||||||||||||
| PYEOF | ||||||||||||||||||||||||
| curl -fsS -X POST \ | ||||||||||||||||||||||||
| -H "Authorization: Bearer $APP_TOKEN" \ | ||||||||||||||||||||||||
| -H "Accept: application/vnd.github+json" \ | ||||||||||||||||||||||||
| "https://api.github.com/repos/$REPO/check-runs" \ | ||||||||||||||||||||||||
| -d @"$RUNNER_TEMP/check_body.json" | ||||||||||||||||||||||||
| echo "阻断:$VERDICT —— $SUMMARY" | ||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| name: g060 guard | ||
|
|
||
| # ADR-0061 g060 语义扩展至治理仓(ISSUE-263 W2-C2): | ||
| # - PR 变更 specs/*/suite/** 时校验写者身份; | ||
| # - 定时扫描未裁决的 g060 阻断 issue,超 TTL 触发 dead-man 提醒。 | ||
| # | ||
| # 注意:当前 App 无 workflows 权限,本文件已完整实现但暂无法推送至上游, | ||
| # 阻塞记录在 ISSUE-263 W2-C2 交付说明中。 | ||
|
Comment on lines
+7
to
+8
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - 'specs/*/suite/**' | ||
| schedule: | ||
| # 每 6 小时巡检一次(与 butler 系列对齐) | ||
| - cron: '0 */6 * * *' | ||
| workflow_dispatch: | ||
| inputs: | ||
| issue: | ||
| description: '指定裁决 issue 编号(未指定则处理全部 open g060 issue)' | ||
| required: false | ||
| type: string | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| g060-lock: | ||
| # 仅在 PR 事件且命中 paths 时触发 | ||
| if: github.event_name == 'pull_request' | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| permissions: | ||
| contents: read # checkout | ||
| issues: write # 非法修改时创建裁决 issue | ||
| pull-requests: read # 读取 PR 文件清单 | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
| fetch-depth: 0 # 需要完整历史做 base..head diff 兜底 | ||
|
|
||
| - name: g060 lock check | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| GH_REPO: ${{ github.repository }} | ||
| GITHUB_ACTOR: ${{ github.actor }} | ||
| GITHUB_EVENT_PATH: ${{ github.event_path }} | ||
| run: bash scripts/g060-lock.sh | ||
|
Comment on lines
+10
to
+49
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 | 🔴 Critical | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -u
echo '--- workflow files ---'
git ls-files '.github/workflows/g060-guard.yml' 'scripts/g060-lock.sh' 'CODEOWNERS' 'ADR-*' 'governance/**' 'standards/**' 'docs/**' | sed -n '1,160p'
echo '--- workflow ---'
cat -n .github/workflows/g060-guard.yml
echo '--- guard script ---'
cat -n scripts/g060-lock.sh
echo '--- ownership and ADR references ---'
for f in CODEOWNERS .github/CODEOWNERS docs/CODEOWNERS; do
if [ -f "$f" ]; then
echo "### $f"
cat -n "$f"
fi
done
rg -n -i --hidden --glob '!node_modules/**' --glob '!dist/**' \
'ADR-0061|owner.only|owner-only|codeowners|g060-guard|g060-lock' \
.github governance standards scripts docs CODEOWNERS 2>/dev/null | sed -n '1,240p'Repository: Cloudbird-Software/.github Length of output: 17127 🌐 Web query:
💡 Result: GitHub Actions handles security for pull requests (PRs) from forks by restricting the permissions of the GITHUB_TOKEN and isolating the execution environment [1][2][3]. Understanding the difference between pull_request and pull_request_target is critical for managing these security boundaries [1][4][5]. Key Security Principles: 1. pull_request vs. pull_request_target - pull_request: This event triggers on the merge commit of the PR [1]. Because the code in a fork-originated PR is untrusted, GitHub automatically grants the GITHUB_TOKEN read-only permissions and denies access to repository secrets, regardless of the permissions configured in your workflow [1][2][3]. - pull_request_target: This event runs in the context of the base repository's default branch rather than the PR's merge commit [1][4]. Consequently, it has access to repository secrets and read/write tokens [1][4][5]. It is intended for trusted metadata operations (like labeling, commenting, or status updates) on a PR [3][4][5]. 2. Securely Using pull_request_target (The "Pwn Request" Risk) - The primary risk with pull_request_target is the execution of untrusted code from a fork with privileged access (secrets and write tokens) [4][6][7]. This pattern is known as a "pwn request" [4][6]. - GitHub has implemented safety guardrails: the actions/checkout action will now block attempts to check out untrusted code from a fork when using the pull_request_target or workflow_run events by default [6][8][9]. 3. Trusted Base Branch Checkout - If a workflow must checkout code in a pull_request_target context, it should only check out the trusted base branch [1][10]. - Recent updates (as of June 2026) recognize explicit checkouts of the base repository and base reference as safe [10]. Safe patterns include: - Omitting the checkout ref (defaults to the base branch) [10]. - Explicitly setting ref: ${{ github.event.pull_request.base.sha }} or ${{ github.event.pull_request.base.ref }} [10]. - If you genuinely need to check out the fork's PR code, you must explicitly opt-in by setting the allow-unsafe-pr-checkout: true input on the actions/checkout step, acknowledging the security risks involved [6][8][9]. For workflows requiring write access to fork PRs (e.g., commenting on a PR), the recommended practice is to avoid using pull_request_target for processing the code itself [3]. Instead, use a two-step process: perform the build/test in a restricted pull_request workflow, output results as an artifact, and use a subsequent workflow_run event (which runs in the trusted base context) to consume the artifact and perform the write operation [3]. Citations:
🏁 Script executed: #!/bin/bash
set -u
echo '--- invocation and fallback references ---'
rg -n -C 3 'g060-lock\.sh|G060_PR|G060_BASE|G060_HEAD|git diff --name-only HEAD|gh pr diff|compare/' \
.github/workflows/g060-guard.yml scripts/g060-lock.sh scripts/g060-guard.yml.local
echo '--- current worktree diff probe ---'
printf 'git status: '
git status --porcelain
printf 'git diff --name-only HEAD: '
git diff --name-only HEAD
echo '--- deterministic script-behavior probe ---'
python3 - <<'PY'
from pathlib import Path
import subprocess
script = Path("scripts/g060-lock.sh").read_text()
workflow = Path(".github/workflows/g060-guard.yml").read_text()
assert 'bash scripts/g060-lock.sh' in workflow
assert 'G060_PR' not in workflow
assert 'G060_BASE' not in workflow
assert 'G060_HEAD' not in workflow
assert 'PR_NUMBER="${G060_PR:-}"' in script
assert 'elif [[ -n "$BASE_SHA" && -n "$HEAD_SHA" ]]' in script
assert 'git diff --name-only HEAD' in script
# Reproduce the workflow's shell generation without executing the generated command.
payload = '$(printf INJECTED)'
generated = f'''if [[ -n "{payload}" ]]; then
python3 scripts/g060-escalation.py --issue "{payload}"
else
python3 scripts/g060-escalation.py --all
fi
'''
print("generated shell:")
print(generated)
print("bash -n:", subprocess.run(
["bash", "-n"], input=generated, text=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE
).returncode)
print("command substitution present:",
'$(printf INJECTED)' in generated)
PY
echo '--- actions/checkout tag resolution ---'
if command -v gh >/dev/null 2>&1; then
gh api repos/actions/checkout/git/ref/tags/v7.0.1 \
--jq '{ref:.ref, type:.object.type, sha:.object.sha}' 2>&1 || true
else
echo 'gh unavailable'
fiRepository: Cloudbird-Software/.github Length of output: 4872 严重:不要在
改用 将 🤖 Prompt for AI AgentsSource: Path instructions
Comment on lines
+44
to
+49
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. 1. g060-guard uses secrets.github_token The new g060-guard and adversary-gate workflows pass ${{ secrets.GITHUB_TOKEN }}/`${{
github.token }} as GH_TOKEN for scripts and commands that call GitHub APIs via gh`, REST, and
curl. This violates the requirement that agent GitHub operations must use cloudbrid-agent GitHub
App tokens (constrained single-repo scope and short TTL) instead of GITHUB_TOKEN/PATs.
Agent Prompt
Comment on lines
+44
to
+49
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. 5. G060 lock effectively bypassed The new g060-guard workflow does not pass a usable token or PR identity to scripts/g060-lock.sh, causing the script to fall back to empty/HEAD-only diffs and exit 0 (allow) even when locked suite files changed. Because the script also swallows gh failures (|| true), a missing/invalid token can silently under-detect changes and let unauthorized edits through. Agent Prompt
|
||
|
|
||
| g060-escalation: | ||
| # 定时/手动触发:处理未裁决 issue | ||
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| permissions: | ||
| contents: read | ||
| issues: write | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: g060 escalation | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| GH_REPO: ${{ github.repository }} | ||
| run: | | ||
| if [[ -n "${{ inputs.issue }}" ]]; then | ||
| python3 scripts/g060-escalation.py --issue "${{ inputs.issue }}" | ||
| else | ||
| python3 scripts/g060-escalation.py --all | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
Comment on lines
+71
to
+74
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. 6. G060 escalation invocation invalid The new g060-escalation job calls scripts/g060-escalation.py without the required --repo argument and uses a non-existent --all flag, so the scheduled/manual escalation path will fail every time and never perform the intended TTL/dead-man closure loop. Agent Prompt
|
||
| fi | ||
|
Comment on lines
+70
to
+75
|
||
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.
2. Missing card: metadata line
📘 Rule violation§ ComplianceAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools