feat(pr_merge): gate it like tbd_ship — green CI, unprotected base, exactly one risk/low issue (gitea-mcp#74)
pr_merge was an independent merge path with no CI, protection or tier check. It now reads the PR, the base's branch protection and the head commit's CI runs, takes the tier from the issue named in `issue`, and runs the same evaluateShipGate as tbd_ship (helpers shared, not copied). A refusal is a result (merged:false, reason, ci_status, risk); an unreadable PR or protection probe is an error; a merge conflict is still an error. Above risk/low a human merges in the UI. Tests: tier table, no-issue, issue lookup failures, CI and protection not overridden by risk/low, read-side failures, tier read from the named issue only; nine wiring mutants flip-tested red. Shared reason text now says "PR left open" (the PR already exists for pr_merge). Behaviour change: pr_merge without `issue`, or on a PR with no green CI, merges nothing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZkhi49hfeSqBp5aZJNXsM
This commit is contained in:
@@ -125,12 +125,19 @@ by area:
|
||||
| Search / Templates | code_search, create_project_from_template |
|
||||
| Composite | tbd_ship (trunk-based ship helper) |
|
||||
|
||||
**`tbd_ship` merge gate (gitea-mcp#63).** A PR is squash-merged only when ALL of: the head commit's CI is fully green,
|
||||
the base branch is not review-protected, and the issue named in `issue` carries **exactly one** `risk/` label, `risk/low`.
|
||||
No `issue`, no `risk/` label, several, an unknown value (`risk/lowish`), `risk/medium`, `risk/high`, or an issue that
|
||||
cannot be read all fail closed: the PR is opened, `merged:false`, with a `reason` and the `risk` tier in the result.
|
||||
Unclassified is not low. Limit: the gate enforces the tier of the issue the caller names; it cannot prove that issue is
|
||||
the one the change belongs to, so the link is the caller's claim (the convention in local-dev rule 5 still applies).
|
||||
**Merge gate: `tbd_ship` (gitea-mcp#63) and `pr_merge` (gitea-mcp#74).** The two merge tools share one gate
|
||||
(`evaluateShipGate`). A PR is merged only when ALL of: the head commit's CI is fully green, the base branch is not
|
||||
review-protected, and the issue named in `issue` carries **exactly one** `risk/` label, `risk/low`. No `issue`, no
|
||||
`risk/` label, several, an unknown value (`risk/lowish`), `risk/medium`, `risk/high`, or an issue that cannot be read
|
||||
all fail closed: nothing is merged, the result is `merged:false` with a `reason`, the `ci_status` and the `risk` tier.
|
||||
Unclassified is not low. Above `risk/low` a human reviews and merges in the Gitea UI (which this gate does not touch).
|
||||
`pr_merge` additionally fails with an error, merging nothing, when the PR or the base's branch-protection state cannot
|
||||
be read. A merge conflict is still an error.
|
||||
|
||||
Limits: the gate enforces the tier of the issue the caller names; it cannot prove that issue is the one the change
|
||||
belongs to, so the link is the caller's claim (local-dev rule 5's convention still applies). Not gated: merging in the
|
||||
Gitea UI, and `file_write_branch` committing directly to a branch (`main` included), which is how `risk/low` work
|
||||
reaches trunk. Platform-level enforcement for those is branch protection on `main` (local-dev#27 option 2).
|
||||
|
||||
Shared tool-layer hygiene (`internal/tools/tool.go`):
|
||||
- **Alias normalization** — `repo`↔`name` and `number`↔`index` are reconciled so
|
||||
|
||||
Reference in New Issue
Block a user