26 KiB
CI — PR 合入门禁
The pull-request admission layer has exactly nine required external contexts:
| Required context | Contract |
|---|---|
Lint |
Stable PR revision/risk classification plus applicable formatting, go vet, and Actionlint |
Test |
Tier-selected race/unit/release-script tests plus representative cross-platform compilation |
Coverage |
Scope-matched overall non-regression and 100% changed-code coverage |
Policy |
Repository policy and the fail-closed CHANGELOG contract |
Edition |
Edition contract tests |
Interface Integrity |
CLI, Schema, Skill, and stable-release compatibility |
AI Behavior |
Base-owned policy for PRs labeled ai-generated |
CLI Smoke |
Offline help for every public top-level command |
Mock MCP |
HTTP and stdio MCP lifecycle smoke tests |
The workflow display name is CI. Parallel helper
jobs may implement Test and Coverage, but they are not ruleset contexts.
Do not require an aggregate alias or a downstream integration check in place of
the nine contracts above.
AI Behavior is evaluated by a pull_request_target workflow that never
checks out or executes PR code. It writes the exact AI Behavior status to the
current PR head. Its Files API read is bracketed by base/head revision checks,
so a synchronize race fails closed. The same workflow supplies a successful
AI Behavior check run on protected main pushes for release governance.
Exact CHANGELOG-only fast path
A pull request qualifies only when GitHub reports exactly one changed file,
that file is an in-place modification of CHANGELOG.md, and the base and head
both retain it as a regular non-executable 100644 blob. Add, delete, rename,
symlink, executable-mode, and second-file changes do not qualify.
Lint classifies the Files API result only after verifying that the API's base
and head equal the event revision both before and after pagination. Policy
checks out GitHub's PR merge ref and verifies its parents:
HEAD^1 = pull_request.base.sha
HEAD^2 = pull_request.head.sha
It then runs:
./scripts/policy/check-changelog-pr.sh \
--fast-path "$PR_BASE_SHA" HEAD
The exact fast path remains limited to historic one-file maintenance. A
release-seal PR uses --content-only, which permits the generated
CHANGELOG.md change together with archival moves from .changes/ to
.changes/released/; it receives the normal scoped admission instead of this
fast path. Ordinary PRs must not modify CHANGELOG.md; they add a standalone
release fragment instead. The validator and its policy dependencies in that merge tree are byte-for-byte the current base
versions. Validation targets the synthetic merge tree, not the feature-branch
tree, so a stale branch cannot supply an older validator or combine with newer
base notes into an invalid final CHANGELOG.
All nine admission contexts are still emitted and must succeed. Expensive implementation helpers are skipped; the named contexts record that their code surface is unaffected.
The protected main push keeps that fast path only when all of these
fail-closed conditions hold:
- the event is a non-forced update of the existing
refs/heads/main; - the event
afterSHA is the exact workflow SHA, and both event SHAs are complete, non-zero commit IDs; - GitHub's comparison reports the previous main tip as the unique linear merge base, with no commits behind it;
- the complete resulting tree diff is exactly one in-place modification of
CHANGELOG.md; - the previous main tip already has successful GitHub Actions checks for all nine Code Admission contexts.
Policy then independently checks out the pushed revision and runs the same
check-changelog-pr.sh --fast-path contract from the event's before SHA to
its after SHA. If identity, ancestry, file scope, tree mode, CHANGELOG
content, or predecessor admission cannot be proved, classification falls back
to the complete main admission suite. A source change can therefore never
inherit the CHANGELOG-only result.
Any PR that touches CHANGELOG.md but also changes another file runs the same
content contract in Policy with --content-only. That mode accepts only
fragment archival moves (.changes/<name>.md to
.changes/released/<version>/<name>.md) alongside the changelog; source and
documentation changes are rejected. It still rejects invalid dates or
versions, missing bullets, placeholder TODO/TBD, unmanaged-section
changes, and unsafe tree modes.
Risk tiers and downstream boundaries
Lint resolves the complete base/head diff before any helper is skipped.
Unknown or truncated input fails closed into the high-risk tier.
| Tier | Selection | Admission work |
|---|---|---|
| Documentation-only | Only prose/documentation assets; no executable, generated, workflow, packaging, or interface surface | Documentation and repository-asset validation; expensive code helpers skip while every required context still succeeds |
| Standard | Ordinary code change with a stable package graph | Race tests for changed Go packages and their reverse dependencies; candidate and merge-base coverage over the same impacted scope and coverpkg; representative Darwin/Windows compilation |
High-risk / protected main |
Workflow/policy, package add/remove/rename, generated Schema/registry, platform, auth/keychain, installer, packaging, release, transport, recovery, or an unprovable infrastructure classification | Complete race suite and full native macOS/Windows tests, plus every affected domain gate |
Domain helpers (Edition, Interface Integrity, CLI Smoke, and Mock MCP,
for example) execute their substantive suites when the diff can affect that
contract or when the high-risk tier is selected. Otherwise their stable named
contexts still report a successful, explicit unaffected result. Release-script
tests follow the same impact rule. This preserves the ruleset contract without
charging every developer for unrelated work.
Platform-sensitive changes additionally run native changed-code coverage.
Protected main always runs native tests; generic portable changes are held to
the Linux changed-code gate rather than being forced to manufacture
platform-only coverage.
Complete Multi-profile E2E is not a PR admission context. It belongs to the
Main Integration — 主干集成 workflow and runs only after a push to main (or
an explicit manual dispatch). A failing downstream run remains a real
regression and must be repaired, but it must not be represented by a synthetic
successful PR check.
flowchart TB
PR["Pull request"] --> ADMISSION["CI"]
ADMISSION --> L["Lint"]
ADMISSION --> T["Test"]
ADMISSION --> C["Coverage"]
ADMISSION --> P["Policy"]
ADMISSION --> E["Edition"]
ADMISSION --> I["Interface Integrity"]
ADMISSION --> A["AI Behavior"]
ADMISSION --> S["CLI Smoke"]
ADMISSION --> M["Mock MCP"]
ADMISSION --> MAIN["Protected main"]
MAIN --> NATIVE["Full native platform matrix"]
MAIN --> E2E["Multi-profile E2E"]
MAIN --> RELEASE["Release delivery"]
Review ownership and auto-merge
A base-owned pull_request_target workflow routes newly opened, updated,
reopened, or newly ready PRs targeting main to one eligible peer reviewer. It
does not check out or execute PR code, excludes both the author and the known
latest pusher, and balances the open requested-review load across the reviewed
maintainer pool. A current-head approval or change request is preserved; after
a new push, stale activity does not suppress a fresh request, and an
outstanding change requester is preferred for continuity.
The branch rulesets keep one human approval and all nine strict required
contexts, require someone other than the latest pusher to approve after the
most recent head update, and restrict main updates to the dedicated Reviewer
Router App in pull-request mode plus the designated Formula publishers and
break-glass identities. Repository auto-merge is enabled for ready PRs, so the
App-owned request records the automation intent while the App's synchronous
merge path waits for that approval and the current revision's nine green
checks. If main advances, strict checks rerun before merge. The
reviewer routing job uses the built-in GITHUB_TOKEN to request reviewers with
Contents: read and Pull requests: write. A separate base-owned cleanup job
isolates the merge-authority permissions (Contents: write and Pull requests: write), revalidates the exact event base/head, and uses the built-in
token only to disable an existing request owned by github-actions[bot] or one
whose title or merge metadata requests that GitHub skip workflows; it never
enables auto-merge. The job then mints a current-repository installation token
for the dedicated Reviewer Router GitHub App, proves its emitted slug matches
the reviewed REVIEWER_ROUTER_APP_SLUG, replaces every non-App request, and
enables native auto-merge with fixed metadata. This
identity boundary is required because GitHub suppresses
most workflow events created by the built-in token; using it for auto-merge would
silently skip the merge commit's protected-main CI and baseline-cache
producer. Token minting or takeover fails closed without falling back to
GITHUB_TOKEN: the unsafe request is cleared before credentials are read, and
the required Test context live-verifies the exact main-merge-writers update
rule. GitHub's read APIs may omit parameters for the strict
update_allows_fetch_and_merge: false value, so the gate accepts only that
exact omission or a one-field parameters object containing explicit boolean
false; every other present shape or value fails closed. The gate then binds
the same ruleset node through GraphQL and requires its non-null
updateAllowsFetchAndMerge value to be exactly false. It also requires its
own built-in token to report
current_user_can_bypass: never. The minted App separately requires
pull_requests_only on that writer rule and never on every other active main
ruleset before it can enable, reconcile, or synchronously merge. The read-only
Test
token may receive a repository projection with both merge-default properties
omitted; it accepts only that complete omission or exact MERGE_MESSAGE plus
PR_TITLE/BLANK, while partial or malformed projections fail closed.
The same unprivileged pull_request job may receive an empty repository-variable
projection for an external fork. Only when the event head repository differs
from the base repository does it substitute the exact reviewed public slug
dingtalk-dws-reviewer-router for identity comparison. An empty variable on a
same-repository PR and every malformed non-empty value still fail closed. This
fallback neither mints a token nor grants merge authority; the base-owned
Router continues to require its minted App slug to equal the repository
variable before any mutation. The minted App's Contents: write token must
observe the exact reviewed defaults
before either mutation path proceeds. GitHub hides the complete
bypass_actors list from low-privilege callers, so the rollout audit must still
keep the writer list at exactly the Reviewer App, haofeng0705 (ID
30925823), and PeterGuy326 (ID 47820304). The required check finally
accepts a null or exact App-owned
request after a short takeover grace period. Null is safe from the suppressed
event path because the built-in Actions identity cannot update main; other
permitted identities produce either a main push or the trusted closed-PR
repair. Drafts skip the identity step, while ready_for_review, edited,
auto_merge_enabled, and auto_merge_disabled explicitly start fresh admission
for readiness, title, and merge-request changes. Router does not react to
auto_merge_disabled, so a
human can deliberately leave the PR manual-only for break-glass handling.
Reviewer routing remains available. The protected-main push that deploys the
workflow automatically migrates every open, ready non-App request and repairs
unsafe App metadata; it disables workflow-skipping requests for correction.
Because GitHub's deferred native auto-merge path does not reliably apply an
App's pull-request-only ruleset bypass, a zero-permission approval-signal
workflow and completed CI / Code Admission — AI Behavior workflows wake the
same trusted default-branch reconciliation through workflow_run. That event
is only a wake-up signal: the privileged job does not consume its pull-request
payload or artifacts and does not check out the triggering run's code. It
re-enumerates open main PRs through the API and attempts only an exact
App-owned request through the synchronous PR merge endpoint. Immediately before
each attempt it revalidates the App's ruleset boundary and PR intent, supplies
the current head SHA, and treats server-declared not-ready or
concurrent-revision responses as retriable. The live preflight requires the
exact repository-owned approval ruleset and exact nine-check strict quality
ruleset, with every context bound to the GitHub Actions App
(integration_id=15368) and the Reviewer Router App unable to bypass either;
a missing, disabled, incorrectly sourced, or weakened gate fails closed before
merge. GitHub—not the workflow—decides whether the
merge is admissible. A staggered twice-hourly schedule provides eventual
recovery, and a manual workflow_dispatch from main is the immediate
idempotent retry path.
Reconciliation never enables an originally null request. The reviewer router
is orchestration, not a quality context, and
must not be added to the ruleset.
Disabling the App-owned request before the reconcile job's final PR read keeps the PR manual-only. GitHub can atomically bind the subsequent merge to the head SHA, but it cannot bind that call to the auto-merge intent; a disable racing after the final read may therefore lose to the in-flight merge. Closing the PR or changing its head blocks the attempt only if GitHub observes that state before accepting the merge endpoint call; no client-side action can revoke a merge that the server has already accepted.
The merge endpoint has no expected-base precondition. Reconciliation checks
that the base is this repository's main immediately before and after the call,
but a retarget racing after the final read is not atomically preventable in the
workflow. Operators must disable the App-owned intent and wait for all running
Reviewer routing reconciliation jobs to finish before retargeting a PR; a
stronger adversarial guarantee requires a GitHub-side branch/ruleset control.
GitHub may omit pull_request_target for security-sensitive head branch names,
including names that look like commit SHAs. Those PRs cannot use Router App
takeover or the closed-event repair: rename the branch for the supported path,
or use the designated break-glass identity with a safe final message so main
push CI remains the exact-SHA producer.
Running focused gates locally
Run the contracts relevant to the change. Ordinary contributors are not expected to repeat every CI job locally:
make build
make policy
make interface-integrity BASE_REF=<merge-base> STABLE_REF=<stable-tag> CANDIDATE_REF=<candidate-sha>
make schema-compatibility BASE_REF=<merge-base> STABLE_REF=<stable-tag> CANDIDATE_REF=<candidate-sha>
make skill-command-integrity
make cli-smoke
make mock-mcp-smoke
go test -v -count=1 ./pkg/editiontest/...
CI 先解析并核对精确的 merge-base、最近可达且未撤回的 stable GA tag 和已提交的 candidate
SHA,再调用 make authoritative-interface-integrity。本地 make interface-integrity
与该 CI target 都只委托给同一个 modern authoritative wrapper,不存在第二个比较
入口。省略 BASE_REF 时本地 target 默认比较 origin/main,省略 STABLE_REF 时自动
选择该 base 可达且未撤回的最近 stable GA tag,省略 CANDIDATE_REF 时比较已提交的 HEAD。
需要逐字复现某次 CI 时,应显式传入该次运行记录的 merge-base、stable tag 和
candidate SHA。
make update-interface-baseline / make reset-interface-baseline 只维护
test/fixtures/cli-interface-baseline.txt 这一份非权威 CLI Smoke fixture。底层旧
check-interface-baseline.sh 不再作为本地或 CI 的兼容性审批入口,也不能用于批准
flag 迁移。
Schema compatibility 使用同一组 base、stable、candidate refs,以及 base-owned flag 与 command migration ledgers。merge-base-owned checker 分别规范化 merge-base 与 stable 的完整 Schema,并让 candidate 对两份历史 contract 独立执行检查;它只把已通过 Interface lifecycle 的 exact rename、command move 或 flag extraction 规范化到当前历史 副本,不会维护第二份 allowlist,也不会放宽其他 Schema 历史字段。
For a release-seal branch that archives rendered fragments:
base_ref=$(git merge-base HEAD origin/main)
./scripts/policy/check-changelog-pr.sh --content-only "$base_ref" HEAD
make coverage-gate is an enforcement step, not a profile generator. For a
standard PR, CI derives changed packages and their reverse-dependency test
closure, then generates candidate and merge-base profiles with the same test
scope and coverpkg. High-risk and protected-main runs use the complete
profiles. The complete candidate profile is produced by disjoint per-shard
helper jobs (scripts/ci/test-packages.sh list-coverage, kept serial with
-p 1 inside each shard; verify proves the shard union equals the
full-suite scope exactly once) and concatenated in the aggregate job before
enforcement. The complete merge-base profile is restored from an exact-key
cache written by the last green main push of that same commit (key:
merge-base SHA plus resolved Go version); any miss falls back to recomputing
it in a merge-base worktree. The trusted main producer and PR consumer use
the same dedicated cache profile path because GitHub includes that path in the
cache version; the runtime-facing candidate and baseline filenames remain
separate. Near-miss reuse is forbidden — the caches carry no prefix restore
keys, because a neighbouring commit's profile would compare the candidate
against the wrong baseline. PR concurrency is keyed by PR number, so a later
revision cancels the stale run instead of letting obsolete test matrices
compete with the replacement for hosted runners. If cancellation interrupts a
cold-cache fallback, the latest run recomputes the same exact merge-base
profile authoritatively. Main concurrency remains keyed by pushed SHA, so a
newer main push cannot cancel a predecessor's producer.
Every supported main advancement path has an exact-SHA producer. The required
Test context rejects GitHub workflow-skip directives in PR and auto-merge
metadata, reruns when that metadata is enabled, disabled, or edited, and
verifies the live App/writer-ruleset identity contract. Reviewer Router
additionally binds auto-merge to the exact head OID and writes a fixed safe
merge headline/body. The sole break-glass publisher must retain a safe final
message; the release-controlled Formula-only path
is the sole supported use of [skip ci]. A full source push
saves the assembled profile after the aggregate gate passes. A trusted
documentation or release-seal push independently verifies that the complete
before...after diff contains only the reviewed metadata allowlist, restores
only the exact before cache, recomputes the full profile if the chain is
cold, and makes that helper a dependency of the required Coverage context.
Release-generated Formula commits intentionally retain [skip ci]; after
their nine synthetic contexts are sealed, an independent release-governance
job creates an acknowledgement and emits a coverage-baseline-promote
repository dispatch. The default-branch promotion
workflow revalidates the exact single-parent Formula identity, successful
parent and target contexts, and main containment before it promotes the exact
parent cache or performs the same full fallback. Every target-main producer
follows its save with a lookup-only restore and requires
cache-hit=true for the exact key; this turns the cache action's otherwise
warning-only upload failure or prefix match into a hard failure. Formula
promotion additionally updates one release-created Coverage Baseline Cache
check. A separate confirmation job waits for that exact check-run ID while npm
and mirrors remain dependent only on the immutable publication job; cache
failure therefore makes the final delivery gate red without creating a
partially published release. Once Formula sealing exposes its SHA, a later
publication verification failure cannot suppress that confirmation job.
A separate base-owned pull_request_target: closed safety net covers the final
merged SHA even if a human or integration changes the merge message after PR
checks finish. Skip directives alone do not suppress pull_request_target,
subject to GitHub's separate security-sensitive branch-name restriction above.
That job executes no PR code and only dispatches after binding the exact
closed-event PR number and stable head SHA to merged-PR facts
(merged_at, base.ref, and merge_commit_sha) and proving main
containment. It does not compare the later REST base.sha, which follows the
live base branch after merge. Because GitHub makes default-branch caches
read-only to pull_request_target, the dispatcher first waits up to one minute
for a run from the exact protected
.github/workflows/ci.yml workflow and exits when that normal producer exists.
A successful main CI hard-verifies the exact key itself. A completed
non-success run starts a separate base-owned workflow_run dispatcher, which
binds the exact CI workflow ID/path, run ID/attempt, conclusion, upstream
repository, main branch, and head SHA. That trigger is also cache-read-only,
so either trusted dispatcher uses repository_dispatch; its producer
revalidates the merged-PR or failed-CI identity, checks out the contained SHA,
and produces/verifies the exact full cache.
An hourly schedule and a main-only manual dispatch repair the event-time main
SHA after a direct break-glass push or cache eviction. The dispatch exception
is intentional: unlike an ordinary event created by GITHUB_TOKEN, GitHub
allows repository_dispatch to start another workflow. A legacy built-in-token
merge can suppress the closed event too, which is why the required Test
identity gate and dedicated Reviewer Router App are still mandatory.
A cold miss can still occur during a producer race or after cache eviction,
but it remains fail-safe: the PR recomputes the authoritative baseline with a
30-minute job budget and saves a PR-scoped copy for same-PR reruns. It is no
longer possible for a supported main-advance path to omit its producer
silently. That PR-scoped fallback save remains a best-effort acceleration and
does not replace the normal push, metadata, Formula, and merged-PR repair
producers. Supporting and (when
platform-selected) native profiles are generated before the aggregate
Coverage context evaluates them. The
aggregate and native gates require 100% coverage for changed executable Go
statements. Overall coverage remains an unrounded, zero-tolerance,
scope-matched merge-base non-regression check. Candidate and baseline profiles
are evaluated by the same block-deduplicating checker; supporting policy and
shortcut profiles contribute to changed-code coverage only. The checked-in
badge is presentation only and is never read as a gate input.
CLI 兼容检查只使用 modern Interface Snapshot 这一处权威比较 seam,并从 PR merge-base 和最近的可达 stable release 生成权威快照。本治理机制合入后, merge-base 拥有生成器、比较器和已审批迁移清单,因此 candidate 不能通过修改 helper、fixture 或在同一 PR 新增 self-approval 记录来放行 breaking change。首次 bootstrap 仍由 merge-base 已有的 modern helper 做无豁免比较,并只接受 candidate 提交中的规范空清单;完整边界见下方治理文档。
精确的两阶段 flag 迁移生命周期见
CLI flag 兼容迁移治理。治理 PR 只能在
surface 未变化时新增 pending;后续产品 PR 达到审批的精确 surface 后,才能
消费 base-owned 记录并改为 consumed。在 main 与 stable 都达到 after 状态前
必须保留该回执,之后再由单独 PR 清理。机制只放行记录中的 legacy
visible-to-hidden,以及 canonical required 新增或提升;删除、type、scope、
shorthand、no-opt 和任何无关漂移仍然阻塞。Schema 可以新增;历史 product、
tool、parameter、mapping、positional execution、constraint 与 safety 语义继续
受保护。alias_of 只是一项由 FlagSpec.Aliases 产生的框架关系证据,不是 payload
等价证明;产品 PR 仍须证明 canonical 与 legacy 的最终运行 payload 等价并在 transport
前拒绝冲突输入。当前迁移清单为空,不授权 PR #904。
Required GitHub repository settings
The main quality ruleset must enable strict required-status-check policy
(strict_required_status_checks_policy=true) so a PR is revalidated whenever
main advances. Every entry must select the GitHub Actions App
(integration_id=15368), not “any source”. It must require these exact
context/source pairs and no legacy aliases:
LintTestCoveragePolicyEditionInterface IntegrityAI BehaviorCLI SmokeMock MCP
Do not require helper jobs, Multi-profile E2E, or an aggregate admission
alias. Update ruleset contexts only after the new names have appeared on the
protected branch, so a rename cannot silently remove enforcement or leave an
unproducible required context.
The branch ruleset also requires one approval after the latest push. Enable
repository auto-merge and automatic head-branch deletion; keep the base-owned
reviewer router outside the required-context list. Install its dedicated
GitHub App only on this repository with Contents: read and write and Pull requests: read and write; do not grant Actions, Workflows, or Administration.
Give it pull-request-only bypass on main-merge-writers and no bypass on any
other ruleset. Store the App client ID and lowercase slug in repository
variables REVIEWER_ROUTER_APP_CLIENT_ID and REVIEWER_ROUTER_APP_SLUG, and
its private key in repository secret REVIEWER_ROUTER_APP_PRIVATE_KEY. Do not
reuse release, Homebrew, or personal tokens for this boundary.