-
Notifications
You must be signed in to change notification settings - Fork 0
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 all commits
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,167 @@ | ||
| 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。 | ||
| # - 预检:用 gh + github.token 判断 PR 是否含 specs/** 变更。 | ||
| # - 非 specs PR → 直接写 success check run,放行(零外部依赖)。 | ||
| # - specs PR → 铸 App 令牌,查 head sha 上是否存在 verdict=survived 的 | ||
| # adversary check run;不存在/结论非 success 即红(fail-closed)。 | ||
| # | ||
| # 部署注意: | ||
| # - expected_skip.py 在 CI-Workflows 仓;本 gate 用 gh api 预检 specs/ | ||
| # 路径,不依赖 expected_skip.py,避免跨仓 sparse-checkout 失败。 | ||
| on: | ||
| pull_request: | ||
| types: [opened, synchronize, reopened] | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| checks: write | ||
|
|
||
| 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 | ||
| checks: write | ||
| steps: | ||
| - name: 预检 PR 是否含 specs/** 变更(gh + github.token) | ||
| id: specspr | ||
| 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) | ||
| 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 | ||
| # API 失败 → 负向断言:视为 spec 变更,走完整审计路径 | ||
| echo "has_specs=true" >> "$GITHUB_OUTPUT" | ||
| echo "::warning::取 PR files 失败(负向断言:视为 spec 变更)" | ||
| else | ||
| 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')") | ||
| echo "has_specs=$HASSPECS" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: 非 specs PR——写 success check run 并放行(github.token) | ||
| if: steps.specspr.outputs.has_specs == 'false' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| set -euo pipefail | ||
| SUMMARY="specs/** 未变更:EXPECTED_SKIP=True(路径预检:diff 无 specs/ 前缀文件)" | ||
| python3 - "$SUMMARY" > "$RUNNER_TEMP/check_body.json" <<'PYEOF' | ||
| import json, sys, datetime as dt | ||
| summary = sys.argv[1] | ||
| json.dump({ | ||
| "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 (no specs/** change)", "summary": summary}, | ||
| }, 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" \ | ||
| && 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 | ||
| 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 }} | ||
| 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 "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 | ||
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