From f19f3b77093bc28153d200499e5a205d5449d097 Mon Sep 17 00:00:00 2001 From: DeliciousBuding Date: Fri, 21 Aug 2026 17:02:39 +0800 Subject: [PATCH 1/5] =?UTF-8?q?ci(checks):=20go-edge/go-hub=20=E6=81=92?= =?UTF-8?q?=E6=8A=A5=20report=20=E4=BF=AE=E5=A4=8D=E7=BA=AF=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=20PR=20=E7=9A=84=20required=20check=20=E8=B7=B3?= =?UTF-8?q?=E8=BF=87=E9=98=BB=E5=A1=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .github/workflows/checks.yml | 63 ++++++++++++++++--- docs/governance/verifier-map.md | 2 +- scripts/verify/tests/verify-ci-gates.Tests.py | 54 ++++++++++++++++ scripts/verify/verify-ci-gates.py | 20 ++++++ 4 files changed, 128 insertions(+), 11 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index cb96a4aa3..ce5da8baa 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -35,23 +35,26 @@ jobs: # profiles with scripts/verify/merge-coverprofiles.py (repo-local). go-edge: needs: [changes, go-edge-test] - if: >- - github.event_name == 'workflow_dispatch' || - needs.changes.outputs.go == 'true' + # Required check: always report a result so a path-filtered skip cannot + # block branch protection. Real gates below run only for Go changes. + if: ${{ !cancelled() }} runs-on: ubuntu-latest defaults: run: working-directory: edge-server steps: - - uses: actions/checkout@v7 + - if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' + uses: actions/checkout@v7 - - uses: actions/setup-go@v7 + - if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' + uses: actions/setup-go@v7 with: go-version: ${{ env.GO_VERSION }} cache: true cache-dependency-path: edge-server/go.sum - name: Lint + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' # Wave 10: advisory — gocognit findings in pre-existing complex functions continue-on-error: true uses: golangci/golangci-lint-action@v9 @@ -61,12 +64,14 @@ jobs: args: --timeout=5m - name: Download shard coverage data + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' uses: actions/download-artifact@v7 with: pattern: go-edge-coverage-shard-* path: edge-server/cov-shards - name: Merge shard coverage (coverprofile text) + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' run: | python ../scripts/verify/merge-coverprofiles.py \ cov-shards/go-edge-coverage-shard-1/shard-1.out \ @@ -74,12 +79,14 @@ jobs: -o coverage.out - name: Coverage check (informational) + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' run: | COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') echo "Overall coverage: ${COVERAGE}%" echo "::notice::edge-server overall coverage: ${COVERAGE}%" - name: Coverage per-package minimums + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' run: | echo "=== Per-package coverage minimums ===" check_pkg() { @@ -106,6 +113,7 @@ jobs: # Restore must precede both gosec and staticcheck steps. - name: Cache static-analysis binaries (staticcheck + gosec) id: sa-cache + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' uses: actions/cache@v5 with: path: | @@ -114,6 +122,7 @@ jobs: key: sa-tools-2026-08 - name: Security scan (gosec) + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' # #1574: hard fail (findings zero-baselined). Fail-closed wrapper # verify-gosec-gates.sh rejects empty/unparseable output (no false green). run: | @@ -122,9 +131,11 @@ jobs: fi ~/go/bin/gosec -fmt=json ./... 2>/dev/null | bash ../scripts/verify/verify-gosec-gates.sh - name: Vet + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' run: go vet ./... - name: staticcheck + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' run: | if [ ! -x ~/go/bin/staticcheck ]; then go install honnef.co/go/tools/cmd/staticcheck@2026.1 @@ -136,13 +147,24 @@ jobs: # internal/orchestration 不得 import 任何 adapters。此前该脚本只在 # 本地跑、CI 未接线——治理声明与执行不一致,现接进 go-edge(已 setup-go)。 - name: Verify orchestrator dependency direction (#1566) + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' working-directory: . run: python scripts/verify/verify-orchestrator-deps.py - name: Self-test orchestrator dependency direction gate + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' working-directory: . run: python scripts/verify/tests/verify-orchestrator-deps.Tests.py + # Fallback: keep the required check reported even when the Go path + # filter deselects this job (skipped jobs never satisfy branch + # protection and permanently block frontend-only PRs). + - name: Report no-Go-changes skip (required check) + if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' && needs.changes.outputs.go != 'true' }} + run: | + echo "skipped: no Go changes; reporting success for required check go-edge" + exit 0 + # ── Go: Edge Server tests (shard×2) ────────── # CI5 wave2 (#1689): the single-pass `go test ./... -race -coverprofile` # (~116s wall) is split into a 2-shard matrix; each shard gets its own @@ -197,23 +219,26 @@ jobs: # ── Go: Hub Server ────────────────────────── go-hub: needs: [changes, go-hub-test] - if: >- - github.event_name == 'workflow_dispatch' || - needs.changes.outputs.go == 'true' + # Required check: always report a result so a path-filtered skip cannot + # block branch protection. Real gates below run only for Go changes. + if: ${{ !cancelled() }} runs-on: ubuntu-latest defaults: run: working-directory: hub-server steps: - - uses: actions/checkout@v7 + - if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' + uses: actions/checkout@v7 - - uses: actions/setup-go@v7 + - if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' + uses: actions/setup-go@v7 with: go-version: ${{ env.GO_VERSION }} cache: true cache-dependency-path: hub-server/go.sum - name: Lint + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' # #1657: hard gate — lint failures now block merges. The 17 # pre-existing complexity findings remain baseline-registered in the # #1573 fingerprint ratchet, so only-new-issues keeps those from @@ -232,12 +257,14 @@ jobs: # advisory until the debt is repaid to zero. Reads the same JSON report # the Lint step just produced (single source, no version drift). - name: Verify Hub lint fingerprint ratchet (#1573) + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' # Wave 10: CI golangci-lint version produces different findings; re-baseline on CI continue-on-error: true working-directory: . run: python ./scripts/verify/verify-hub-lint-ratchet.py --LintJsonPath "${{ runner.temp }}/hub-lint-report.json" - name: Self-test Hub lint fingerprint ratchet + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' working-directory: . run: python ./scripts/verify/tests/verify-hub-lint-ratchet.Tests.py @@ -246,12 +273,14 @@ jobs: # scripts/verify/merge-coverprofiles.py and keeps the overall >=40% # gate hard-blocking. - name: Download shard coverage data + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' uses: actions/download-artifact@v7 with: pattern: go-hub-coverage-shard-* path: hub-server/cov-shards - name: Merge shard coverage (coverprofile text) + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' run: | python ../scripts/verify/merge-coverprofiles.py \ cov-shards/go-hub-coverage-shard-1/shard-1.out \ @@ -259,6 +288,7 @@ jobs: -o coverage.out - name: Coverage check (overall >= 40%) + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' run: | COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') THRESHOLD=40 @@ -274,6 +304,7 @@ jobs: # Restore must precede both gosec and staticcheck steps. - name: Cache static-analysis binaries (staticcheck + gosec) id: sa-cache + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' uses: actions/cache@v5 with: path: | @@ -282,6 +313,7 @@ jobs: key: sa-tools-2026-08 - name: Security scan (gosec) + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' # #1574: hard fail (findings zero-baselined), matching go-edge. run: | if [ ! -x ~/go/bin/gosec ]; then @@ -290,15 +322,26 @@ jobs: ~/go/bin/gosec -fmt=json ./... 2>/dev/null | bash ../scripts/verify/verify-gosec-gates.sh - name: Vet + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' run: go vet ./... - name: staticcheck + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' run: | if [ ! -x ~/go/bin/staticcheck ]; then go install honnef.co/go/tools/cmd/staticcheck@2026.1 fi staticcheck ./... 2>&1 + # Fallback: keep the required check reported even when the Go path + # filter deselects this job (skipped jobs never satisfy branch + # protection and permanently block frontend-only PRs). + - name: Report no-Go-changes skip (required check) + if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' && needs.changes.outputs.go != 'true' }} + run: | + echo "skipped: no Go changes; reporting success for required check go-hub" + exit 0 + # ── Go: Hub Server tests (shard×2) ─────────── # CI5 wave2 (#1689): same 2-shard package split as go-edge-test # (Go has no native -shard flag; round-robin on `go list ./...`). diff --git a/docs/governance/verifier-map.md b/docs/governance/verifier-map.md index 36863c4b7..b13a35a46 100644 --- a/docs/governance/verifier-map.md +++ b/docs/governance/verifier-map.md @@ -11,7 +11,7 @@ | 规则 | 验证脚本 | CI job | |---|---|---| -| CI 路径筛选与 job 结构(统一 `changes` job;design-css fail-closed 变异测试) | `scripts/verify/verify-ci-gates.py`(负向自测 `scripts/verify/tests/verify-ci-gates.Tests.py`) | checks.yml → validate | +| CI 路径筛选与 job 结构(统一 `changes` job;design-css fail-closed 变异测试;go-edge/go-hub 恒报 report 防 required check 跳过阻塞) | `scripts/verify/verify-ci-gates.py`(负向自测 `scripts/verify/tests/verify-ci-gates.Tests.py`) | checks.yml → validate | | action runtime 只允许 node24(防 Node-20 major 回退,#1580) | `scripts/verify/verify-action-runtimes.py`(负向自测 `scripts/verify/tests/verify-action-runtimes.Tests.py`) | checks.yml → validate | | Hub lint finding fingerprint ratchet(防新增/替换,#1573) | `scripts/verify/verify-hub-lint-ratchet.py`(负向自测 `scripts/verify/tests/verify-hub-lint-ratchet.Tests.py`,baseline `scripts/verify/hub-lint-baseline.json`) | checks.yml → go-hub | | skill 白名单只提交 active skill | `scripts/verify/verify-project-skills.py` | checks.yml → validate | diff --git a/scripts/verify/tests/verify-ci-gates.Tests.py b/scripts/verify/tests/verify-ci-gates.Tests.py index 62727ee00..9fba3eec1 100644 --- a/scripts/verify/tests/verify-ci-gates.Tests.py +++ b/scripts/verify/tests/verify-ci-gates.Tests.py @@ -11,6 +11,8 @@ 5. swap pnpm test:css-syntax* for pnpm lint:css 6. delete the windows-go MATRIX_RESULT binding → aggregator no longer fail-closed 7. delete the windows-frontend non-success failure branch → aggregator always green +8. restore the go-hub job-level path filter → required check skip-able again +9. delete the go-hub no-Go-changes fallback step → job has no success path when filtered The unmutated copy must exit 0, proving the policy test only reddens on actual policy violations (fail-closed, no false green). @@ -117,6 +119,46 @@ def delete_windows_frontend_failure_branch(text: str) -> str: return text[: match.start("body")] + mutated_body + text[match.end("body"):] +def get_go_hub_body(text: str) -> tuple: + go_hub_block = re.compile(r"(?ms)^ go-hub:\r?\n(?P.*?)(?=^ [A-Za-z0-9_-]+:\r?\n)") + match = go_hub_block.search(text) + if not match: + raise AssertionError("go-hub job block not found") + return match.start("body"), match.end("body"), match.group("body") + + +def restore_go_hub_job_path_filter(text: str) -> str: + start, end, body = get_go_hub_body(text) + always_if = " if: ${{ !cancelled() }}" + if always_if not in body: + raise AssertionError("go-hub job-level always-report if not found") + path_filter_if = ( + " if: >-\n" + " github.event_name == 'workflow_dispatch' ||\n" + " needs.changes.outputs.go == 'true'" + ) + return text[:start] + body.replace(always_if, path_filter_if, 1) + text[end:] + + +GO_HUB_FALLBACK_TEXT = ( + " # Fallback: keep the required check reported even when the Go path\n" + " # filter deselects this job (skipped jobs never satisfy branch\n" + " # protection and permanently block frontend-only PRs).\n" + " - name: Report no-Go-changes skip (required check)\n" + " if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' && needs.changes.outputs.go != 'true' }}\n" + " run: |\n" + ' echo "skipped: no Go changes; reporting success for required check go-hub"\n' + " exit 0\n" +) + + +def delete_go_hub_fallback_step(text: str) -> str: + start, end, body = get_go_hub_body(text) + if GO_HUB_FALLBACK_TEXT not in body: + raise AssertionError("go-hub fallback step text not found") + return text[:start] + body.replace(GO_HUB_FALLBACK_TEXT, "", 1) + text[end:] + + class VerifyCiGatesMutationTests(unittest.TestCase): def assert_mutation_fails(self, mutated_text: str, case_name: str) -> None: exit_code, output = run_verifier(mutated_text) @@ -157,6 +199,18 @@ def test_delete_windows_frontend_failure_branch_fails(self): "deleted windows-frontend non-success failure branch", ) + def test_restore_go_hub_job_path_filter_fails(self): + self.assert_mutation_fails( + restore_go_hub_job_path_filter(read_workflow()), + "restored go-hub job-level path filter", + ) + + def test_delete_go_hub_fallback_step_fails(self): + self.assert_mutation_fails( + delete_go_hub_fallback_step(read_workflow()), + "deleted go-hub no-Go-changes fallback step", + ) + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/scripts/verify/verify-ci-gates.py b/scripts/verify/verify-ci-gates.py index ea2149404..e05e7e1e4 100644 --- a/scripts/verify/verify-ci-gates.py +++ b/scripts/verify/verify-ci-gates.py @@ -108,6 +108,26 @@ def main() -> int: assert_contains(edge, re.escape('check_pkg "edge-server/internal/adapters/" 55 "adapters"'), "go-edge must keep adapters package coverage minimum") assert_contains(hub, r"THRESHOLD=40", "go-hub coverage threshold must be 40%") + # go-edge / go-hub 恒报 report:两者是 required checks,GitHub 不把 + # skipped 视为 required check 通过,纯前端 PR 会被永久 BLOCK。 + # 策略:job 级 if 恒真(!cancelled()),真实门禁步骤带 go 条件(省成本), + # 末尾 fallback step 在无 Go 变更时输出 skipped 并 exit 0。 + for job_name, job_body in (("go-edge", edge), ("go-hub", hub)): + assert_contains( + job_body, + r"(?m)^\s+if:\s+\$\{\{\s*!cancelled\(\)\s*\}\}\s*$", + f"{job_name} must always report a result (job-level if must not path-filter)", + ) + assert_contains( + job_body, + r"if:\s+github\.event_name == 'workflow_dispatch' \|\| needs\.changes\.outputs\.go == 'true'", + f"{job_name} real gates must stay step-level path-filtered", + ) + fallback_step = get_step_block(job_body, "Report no-Go-changes skip (required check)") + assert_contains(fallback_step, r"needs\.changes\.outputs\.go != 'true'", f"{job_name} fallback must only run when the Go filter is off") + assert_contains(fallback_step, r"reporting success for required check", f"{job_name} fallback must report success for the required check") + assert_contains(fallback_step, r"exit 0", f"{job_name} fallback must exit 0") + # #1536: Edge lint is at 0 issues and hardened to hard-blocking; Hub lint # still carries pre-existing findings (tracked in #1573) and stays # warning-only until a finding-fingerprint ratchet exists. Complexity From 6e31094cf9ee9b597280176bec97e368ff8b094b Mon Sep 17 00:00:00 2001 From: DeliciousBuding Date: Fri, 21 Aug 2026 19:08:02 +0800 Subject: [PATCH 2/5] =?UTF-8?q?docs(verify):=20ci-gates=20=E6=81=92?= =?UTF-8?q?=E6=8A=A5=E6=96=AD=E8=A8=80=E8=A1=A5=E5=85=85=20docstring=20(#1?= =?UTF-8?q?829)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- scripts/verify/tests/verify-ci-gates.Tests.py | 5 +++++ scripts/verify/verify-ci-gates.py | 1 + 2 files changed, 6 insertions(+) diff --git a/scripts/verify/tests/verify-ci-gates.Tests.py b/scripts/verify/tests/verify-ci-gates.Tests.py index 9fba3eec1..46e0687f0 100644 --- a/scripts/verify/tests/verify-ci-gates.Tests.py +++ b/scripts/verify/tests/verify-ci-gates.Tests.py @@ -120,6 +120,7 @@ def delete_windows_frontend_failure_branch(text: str) -> str: def get_go_hub_body(text: str) -> tuple: + """提取 go-hub job 块边界与正文,供恒报回归用例在块内做定点修改。""" go_hub_block = re.compile(r"(?ms)^ go-hub:\r?\n(?P.*?)(?=^ [A-Za-z0-9_-]+:\r?\n)") match = go_hub_block.search(text) if not match: @@ -128,6 +129,7 @@ def get_go_hub_body(text: str) -> tuple: def restore_go_hub_job_path_filter(text: str) -> str: + """把 go-hub 的恒报 if 还原成路径筛选,模拟 required check 重新可被跳过(防回退)。""" start, end, body = get_go_hub_body(text) always_if = " if: ${{ !cancelled() }}" if always_if not in body: @@ -153,6 +155,7 @@ def restore_go_hub_job_path_filter(text: str) -> str: def delete_go_hub_fallback_step(text: str) -> str: + """删除 go-hub 无 Go 变更时的 fallback 报成功步骤,模拟 required check 失去成功出口(防回退)。""" start, end, body = get_go_hub_body(text) if GO_HUB_FALLBACK_TEXT not in body: raise AssertionError("go-hub fallback step text not found") @@ -200,12 +203,14 @@ def test_delete_windows_frontend_failure_branch_fails(self): ) def test_restore_go_hub_job_path_filter_fails(self): + """go-hub 恒报 if 被还原为路径筛选时,CI 政策校验器必须非零退出(防回退断言)。""" self.assert_mutation_fails( restore_go_hub_job_path_filter(read_workflow()), "restored go-hub job-level path filter", ) def test_delete_go_hub_fallback_step_fails(self): + """go-hub fallback 步骤被删除时,CI 政策校验器必须非零退出(防回退断言)。""" self.assert_mutation_fails( delete_go_hub_fallback_step(read_workflow()), "deleted go-hub no-Go-changes fallback step", diff --git a/scripts/verify/verify-ci-gates.py b/scripts/verify/verify-ci-gates.py index e05e7e1e4..875ec9786 100644 --- a/scripts/verify/verify-ci-gates.py +++ b/scripts/verify/verify-ci-gates.py @@ -62,6 +62,7 @@ def assert_step_continue_on_error(job_block: str, step_name: str, expected: bool def main() -> int: + """解析 checks.yml 并断言全部 CI 门禁政策;违例即抛错退出 1(fail-closed,防回退)。""" parser = argparse.ArgumentParser(description="CI gate policy verifier") parser.add_argument("--WorkflowPath", default=".github/workflows/checks.yml") args = parser.parse_args() From f737b656261cbd2413691b9174bf212b06009dcb Mon Sep 17 00:00:00 2001 From: DeliciousBuding Date: Fri, 21 Aug 2026 20:07:18 +0800 Subject: [PATCH 3/5] =?UTF-8?q?ci(hub):=20lint=20=E7=A1=AC=E9=97=A8?= =?UTF-8?q?=E7=A6=81=E6=94=B9=E7=94=B1=20#1573=20ratchet=20=E6=89=BF?= =?UTF-8?q?=E6=8B=85=EF=BC=8C=E5=85=A8=E9=87=8F=E5=9B=9E=E9=80=80=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E8=B1=81=E5=85=8D=2017=20=E6=9D=A1=E5=AD=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1832 大 PR(>300 文件)触发 golangci-lint-action 全量回退, 17 条已登记 baseline 的复杂度发现直接 fail 阻塞合并。原 Lint step 的退出码不读 baseline,无法豁免;把硬门禁移到 fingerprint ratchet step(读同一份 JSON 报告),Lint step 降为 report-only。baseline (scripts/verify/hub-lint-baseline.json)已含全部 17 条 fingerprint, 无需增删;本 commit 仅改门禁接线与 verify-ci-gates 断言。 Co-authored-by: Cursor --- .github/workflows/checks.yml | 23 +++++++++++++---------- docs/governance/verifier-map.md | 2 +- scripts/verify/verify-ci-gates.py | 16 ++++++++++------ 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index ce5da8baa..81569b5f0 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -239,11 +239,14 @@ jobs: - name: Lint if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' - # #1657: hard gate — lint failures now block merges. The 17 - # pre-existing complexity findings remain baseline-registered in the - # #1573 fingerprint ratchet, so only-new-issues keeps those from - # failing this step until the debt is repaid; once the baseline is - # empty this knob can be dropped for a full-report hard fail. + # #1657/#1832: report-only — the action's raw exit code cannot consult + # the #1573 baseline, and GitHub refuses PR diffs >300 files, so the + # action falls back to a full-repo lint that reports the 17 + # baseline-registered complexity findings and would hard-fail a + # debt-clean large PR. The hard gate lives in the fingerprint ratchet + # step below. only-new-issues stays so small PRs still produce a + # new-findings-only report. + continue-on-error: true uses: golangci/golangci-lint-action@v9 with: working-directory: hub-server @@ -253,13 +256,13 @@ jobs: # #1573: finding fingerprint ratchet — any finding NOT in the baseline # (new linter, new rule, escalated severity, replacement with same count) - # hard-fails this job even though pre-existing baseline findings stay - # advisory until the debt is repaid to zero. Reads the same JSON report - # the Lint step just produced (single source, no version drift). + # hard-fails this job. Baseline-registered findings pass whether the + # report is patch-filtered or the >300-file full-lint fallback, so the + # 17 pre-existing complexity findings never block a debt-clean PR. + # Reads the same JSON report the Lint step just produced (single source, + # no version drift; gofmt is excluded as toolchain-drift-prone). - name: Verify Hub lint fingerprint ratchet (#1573) if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.go == 'true' - # Wave 10: CI golangci-lint version produces different findings; re-baseline on CI - continue-on-error: true working-directory: . run: python ./scripts/verify/verify-hub-lint-ratchet.py --LintJsonPath "${{ runner.temp }}/hub-lint-report.json" diff --git a/docs/governance/verifier-map.md b/docs/governance/verifier-map.md index b13a35a46..238732f6f 100644 --- a/docs/governance/verifier-map.md +++ b/docs/governance/verifier-map.md @@ -13,7 +13,7 @@ |---|---|---| | CI 路径筛选与 job 结构(统一 `changes` job;design-css fail-closed 变异测试;go-edge/go-hub 恒报 report 防 required check 跳过阻塞) | `scripts/verify/verify-ci-gates.py`(负向自测 `scripts/verify/tests/verify-ci-gates.Tests.py`) | checks.yml → validate | | action runtime 只允许 node24(防 Node-20 major 回退,#1580) | `scripts/verify/verify-action-runtimes.py`(负向自测 `scripts/verify/tests/verify-action-runtimes.Tests.py`) | checks.yml → validate | -| Hub lint finding fingerprint ratchet(防新增/替换,#1573) | `scripts/verify/verify-hub-lint-ratchet.py`(负向自测 `scripts/verify/tests/verify-hub-lint-ratchet.Tests.py`,baseline `scripts/verify/hub-lint-baseline.json`) | checks.yml → go-hub | +| Hub lint finding fingerprint ratchet(防新增/替换,#1573;go-hub 硬门禁,全量 lint 回退模式对 baseline 存量豁免) | `scripts/verify/verify-hub-lint-ratchet.py`(负向自测 `scripts/verify/tests/verify-hub-lint-ratchet.Tests.py`,baseline `scripts/verify/hub-lint-baseline.json`) | checks.yml → go-hub | | skill 白名单只提交 active skill | `scripts/verify/verify-project-skills.py` | checks.yml → validate | | 文档与 Agent 入口 SSOT:根级入口/路径/行数/标记/映射表保鲜 | `scripts/verify/verify-doc-ssot.py`(负向自测 `scripts/verify/tests/verify-doc-entrypoints.Tests.py`) | checks.yml → validate | | Web Hub-only 边界(不直连 Local Edge) | `scripts/verify/verify-web-hub-boundary.py` | checks.yml → validate | diff --git a/scripts/verify/verify-ci-gates.py b/scripts/verify/verify-ci-gates.py index 875ec9786..1d5d25fe1 100644 --- a/scripts/verify/verify-ci-gates.py +++ b/scripts/verify/verify-ci-gates.py @@ -137,14 +137,18 @@ def main() -> int: # complex functions (admin.go/mcp_server.go/agent_dispatch.go); re-harden # after refactoring or threshold adjustment. assert_step_continue_on_error(edge, "Lint", True) - # #1657: go-hub Lint converted from soft gate to hard-blocking. The 17 - # pre-existing complexity findings stay tolerated through the action's - # only-new-issues mode until the #1573 baseline debt is repaid; the step - # must keep the pinned golangci-lint action (no placeholder commands). - assert_step_continue_on_error(hub, "Lint", False) + # #1657/#1832: go-hub Lint is report-only (advisory) — the action's raw + # exit code cannot consult the #1573 baseline, and >300-file PR diffs + # fall back to a full-repo lint that would hard-fail debt-clean large + # PRs. The hard gate lives in the fingerprint ratchet step below, which + # exempts baseline-registered findings in both patch and full-lint mode. + # The step must keep the pinned golangci-lint action (no placeholder + # commands) and only-new-issues so small PRs keep a new-findings report. + assert_step_continue_on_error(hub, "Lint", True) hub_lint_step = get_step_block(hub, "Lint") assert_contains(hub_lint_step, r"golangci/golangci-lint-action@v9", "go-hub Lint must keep the pinned golangci-lint action (no placeholder commands)") - assert_contains(hub_lint_step, r"only-new-issues:\s*true", "go-hub Lint must restrict hard failures to new findings while baseline debt remains") + assert_contains(hub_lint_step, r"only-new-issues:\s*true", "go-hub Lint must keep only-new-issues so patch-mode reports stay scoped to new findings") + assert_step_continue_on_error(hub, "Verify Hub lint fingerprint ratchet (#1573)", False) # #1574: gosec findings triaged and cleared in both servers; the gosec # security scan steps are hard-blocking (no continue-on-error) and run # through the fail-closed verify-gosec-gates.sh wrapper. From 95e5d784dfad56687d4e8b4b43634a3f6eaf68f0 Mon Sep 17 00:00:00 2001 From: DeliciousBuding Date: Fri, 21 Aug 2026 20:25:47 +0800 Subject: [PATCH 4/5] =?UTF-8?q?ci(hub):=20quality-debt=20baseline=20?= =?UTF-8?q?=E8=B7=9F=E8=BF=9B=20Lint=20=E9=99=8D=20advisory=20=E7=9A=84=20?= =?UTF-8?q?soft=5Fgate=20=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- scripts/verify/quality-debt-baseline.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/verify/quality-debt-baseline.json b/scripts/verify/quality-debt-baseline.json index a1f51bfbd..e4087d24f 100644 --- a/scripts/verify/quality-debt-baseline.json +++ b/scripts/verify/quality-debt-baseline.json @@ -10,12 +10,12 @@ "review_by": "2026-10-01" }, { - "location": "go-hub: Verify Hub lint fingerprint ratchet (#1573)", + "location": "go-hub: Lint", "kind": "continue-on-error", - "reason": "CI golangci-lint version produces findings differing from local baseline; re-baseline on CI before re-hardening (#1573)", + "reason": "lint 硬门禁已移到 fingerprint ratchet step(#1573);Lint step 因 golangci-lint-action 退出码不读 baseline 且 >300 文件 PR 全量回退,保持 report-only (#1657/#1832)", "issue": 1573, "owner": "hub-owners", - "introduced_at": "2026-08-11", + "introduced_at": "2026-08-21", "review_by": "2026-10-01" }, { From 6b12877fa65f044bc7ac2c5e0c56165e67ad7409 Mon Sep 17 00:00:00 2001 From: DeliciousBuding Date: Fri, 21 Aug 2026 20:52:23 +0800 Subject: [PATCH 5/5] =?UTF-8?q?ci(verify):=20go=20=E6=81=92=E6=8A=A5=20fal?= =?UTF-8?q?lback=20=E5=A2=9E=E5=8A=A0=20changes=20=E5=A4=B1=E8=B4=A5=20fai?= =?UTF-8?q?l-closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit changes job 失败时 outputs.go 为空,fallback 原条件会误报成功; 现要求 changes.result=='success',失败时新增 step exit 1 防 false green。 Co-authored-by: Cursor --- .github/workflows/checks.yml | 22 ++++++++- scripts/verify/tests/verify-ci-gates.Tests.py | 48 ++++++++++++++++++- scripts/verify/verify-ci-gates.py | 7 ++- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 81569b5f0..730d045d1 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -160,11 +160,20 @@ jobs: # filter deselects this job (skipped jobs never satisfy branch # protection and permanently block frontend-only PRs). - name: Report no-Go-changes skip (required check) - if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' && needs.changes.outputs.go != 'true' }} + if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' && needs.changes.result == 'success' && needs.changes.outputs.go != 'true' }} run: | echo "skipped: no Go changes; reporting success for required check go-edge" exit 0 + # Fail-closed: when the path filter itself fails, the Go verdict is + # unknown. Report failure instead of letting the skip fallback mask + # the skipped real gates as green. + - name: Fail when Go path filter failed + if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' && needs.changes.result != 'success' }} + run: | + echo "::error::changes job result: ${{ needs.changes.result }}; cannot decide Go gates for go-edge" + exit 1 + # ── Go: Edge Server tests (shard×2) ────────── # CI5 wave2 (#1689): the single-pass `go test ./... -race -coverprofile` # (~116s wall) is split into a 2-shard matrix; each shard gets its own @@ -340,11 +349,20 @@ jobs: # filter deselects this job (skipped jobs never satisfy branch # protection and permanently block frontend-only PRs). - name: Report no-Go-changes skip (required check) - if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' && needs.changes.outputs.go != 'true' }} + if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' && needs.changes.result == 'success' && needs.changes.outputs.go != 'true' }} run: | echo "skipped: no Go changes; reporting success for required check go-hub" exit 0 + # Fail-closed: when the path filter itself fails, the Go verdict is + # unknown. Report failure instead of letting the skip fallback mask + # the skipped real gates as green. + - name: Fail when Go path filter failed + if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' && needs.changes.result != 'success' }} + run: | + echo "::error::changes job result: ${{ needs.changes.result }}; cannot decide Go gates for go-hub" + exit 1 + # ── Go: Hub Server tests (shard×2) ─────────── # CI5 wave2 (#1689): same 2-shard package split as go-edge-test # (Go has no native -shard flag; round-robin on `go list ./...`). diff --git a/scripts/verify/tests/verify-ci-gates.Tests.py b/scripts/verify/tests/verify-ci-gates.Tests.py index 46e0687f0..c20a01ea8 100644 --- a/scripts/verify/tests/verify-ci-gates.Tests.py +++ b/scripts/verify/tests/verify-ci-gates.Tests.py @@ -13,6 +13,8 @@ 7. delete the windows-frontend non-success failure branch → aggregator always green 8. restore the go-hub job-level path filter → required check skip-able again 9. delete the go-hub no-Go-changes fallback step → job has no success path when filtered +10. remove the changes.result=='success' guard from both Go fallbacks → changes failure reports green again +11. delete the go-hub changes-failure fail-closed step → required check false-green again The unmutated copy must exit 0, proving the policy test only reddens on actual policy violations (fail-closed, no false green). @@ -147,7 +149,7 @@ def restore_go_hub_job_path_filter(text: str) -> str: " # filter deselects this job (skipped jobs never satisfy branch\n" " # protection and permanently block frontend-only PRs).\n" " - name: Report no-Go-changes skip (required check)\n" - " if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' && needs.changes.outputs.go != 'true' }}\n" + " if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' && needs.changes.result == 'success' && needs.changes.outputs.go != 'true' }}\n" " run: |\n" ' echo "skipped: no Go changes; reporting success for required check go-hub"\n' " exit 0\n" @@ -162,6 +164,36 @@ def delete_go_hub_fallback_step(text: str) -> str: return text[:start] + body.replace(GO_HUB_FALLBACK_TEXT, "", 1) + text[end:] +def remove_go_fallback_changes_success_guard(text: str) -> str: + """删除 go-edge/go-hub fallback 的 changes.result=='success' 守卫,模拟 changes + 失败时 fallback 仍报成功(false green 防回退)。""" + pattern = re.compile(r"needs\.changes\.result == 'success' && ") + mutated, count = pattern.subn("", text) + if count != 2: + raise AssertionError(f"expected exactly two fallback changes-result guards, removed {count}") + return mutated + + +GO_HUB_CHANGES_FAIL_STEP_TEXT = ( + " # Fail-closed: when the path filter itself fails, the Go verdict is\n" + " # unknown. Report failure instead of letting the skip fallback mask\n" + " # the skipped real gates as green.\n" + " - name: Fail when Go path filter failed\n" + " if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' && needs.changes.result != 'success' }}\n" + " run: |\n" + ' echo "::error::changes job result: ${{ needs.changes.result }}; cannot decide Go gates for go-hub"\n' + " exit 1\n" +) + + +def delete_go_hub_changes_fail_step(text: str) -> str: + """删除 go-hub changes 失败 fail-closed 步骤,模拟 required check 重新可 false green(防回退)。""" + start, end, body = get_go_hub_body(text) + if GO_HUB_CHANGES_FAIL_STEP_TEXT not in body: + raise AssertionError("go-hub changes-fail step text not found") + return text[:start] + body.replace(GO_HUB_CHANGES_FAIL_STEP_TEXT, "", 1) + text[end:] + + class VerifyCiGatesMutationTests(unittest.TestCase): def assert_mutation_fails(self, mutated_text: str, case_name: str) -> None: exit_code, output = run_verifier(mutated_text) @@ -216,6 +248,20 @@ def test_delete_go_hub_fallback_step_fails(self): "deleted go-hub no-Go-changes fallback step", ) + def test_remove_go_fallback_changes_success_guard_fails(self): + """fallback 失去 changes.result=='success' 守卫时,校验器必须非零退出(false green 防回退)。""" + self.assert_mutation_fails( + remove_go_fallback_changes_success_guard(read_workflow()), + "removed fallback changes-result success guard", + ) + + def test_delete_go_hub_changes_fail_step_fails(self): + """go-hub changes 失败 fail-closed 步骤被删除时,校验器必须非零退出(防回退断言)。""" + self.assert_mutation_fails( + delete_go_hub_changes_fail_step(read_workflow()), + "deleted go-hub changes-failure fail-closed step", + ) + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/scripts/verify/verify-ci-gates.py b/scripts/verify/verify-ci-gates.py index 1d5d25fe1..2dce1d3fd 100644 --- a/scripts/verify/verify-ci-gates.py +++ b/scripts/verify/verify-ci-gates.py @@ -112,7 +112,8 @@ def main() -> int: # go-edge / go-hub 恒报 report:两者是 required checks,GitHub 不把 # skipped 视为 required check 通过,纯前端 PR 会被永久 BLOCK。 # 策略:job 级 if 恒真(!cancelled()),真实门禁步骤带 go 条件(省成本), - # 末尾 fallback step 在无 Go 变更时输出 skipped 并 exit 0。 + # 末尾 fallback step 在无 Go 变更时输出 skipped 并 exit 0;changes 失败时 + # 由 fail-closed step(result != 'success')exit 1,杜绝 false green。 for job_name, job_body in (("go-edge", edge), ("go-hub", hub)): assert_contains( job_body, @@ -125,9 +126,13 @@ def main() -> int: f"{job_name} real gates must stay step-level path-filtered", ) fallback_step = get_step_block(job_body, "Report no-Go-changes skip (required check)") + assert_contains(fallback_step, r"needs\.changes\.result == 'success'", f"{job_name} fallback must require the changes job to succeed") assert_contains(fallback_step, r"needs\.changes\.outputs\.go != 'true'", f"{job_name} fallback must only run when the Go filter is off") assert_contains(fallback_step, r"reporting success for required check", f"{job_name} fallback must report success for the required check") assert_contains(fallback_step, r"exit 0", f"{job_name} fallback must exit 0") + fail_step = get_step_block(job_body, "Fail when Go path filter failed") + assert_contains(fail_step, r"needs\.changes\.result != 'success'", f"{job_name} must fail closed when the changes job fails") + assert_contains(fail_step, r"exit 1", f"{job_name} changes-failure step must exit 1") # #1536: Edge lint is at 0 issues and hardened to hard-blocking; Hub lint # still carries pre-existing findings (tracked in #1573) and stays