feat(tbd_ship): gate auto-merge on the risk tier; unclassified is not low (#63) #73

Merged
mathias merged 2 commits from wt/risk-tier-gate into main 2026-10-08 06:02:34 +00:00
Owner

Closes gitea-mcp#63. Enforces local-dev rule 5's second half (risk/medium+ is reviewed by a human), which until now was a convention (local-dev#27).

Behaviour: tbd_ship takes an optional issue. It squash-merges only when CI is fully green, the base is not review-protected AND that issue has exactly one risk/ label, risk/low. No issue / no label / several labels / unknown value / risk/medium / risk/high / unreadable issue → PR opened, merged:false, reason, and the risk tier in the result. The zero tier is not low.

Tests: gate table (every non-low tier with green CI), classifyRisk table (case, prefix containment, duplicates), end-to-end through the fake Gitea (medium/high/none/ambiguous/unknown/case, issue lookup 500/404/403, risk/low not overriding red CI or a protected base, tier reported). Nine mutants (gate ignores risk, no-issue=low, unreadable=low, prefix→contains, ambiguous=low, unknown=low, medium=low, …) each turn tests red. task check exit 0.

Behaviour change for callers: a tbd_ship call without issue now opens the PR and stops (it used to merge on green). That is the point; callers that want merge-on-green pass the risk/low issue.

Stated limit: the gate enforces the tier of the issue the caller names; it cannot prove that issue is the change's own. The link is the caller's claim.

Not done here / for Mathias: merging this PR triggers gitea-mcp's CD to production on koala. Gate code (rule 8): an adversarial review by execution is requested before merge; this PR is deliberately left open for it.


Created via git-mcp on behalf of @mathias

Closes gitea-mcp#63. Enforces local-dev rule 5's second half (risk/medium+ is reviewed by a human), which until now was a convention (local-dev#27). **Behaviour:** `tbd_ship` takes an optional `issue`. It squash-merges only when CI is fully green, the base is not review-protected AND that issue has exactly one `risk/` label, `risk/low`. No issue / no label / several labels / unknown value / `risk/medium` / `risk/high` / unreadable issue → PR opened, `merged:false`, `reason`, and the `risk` tier in the result. The zero tier is not low. **Tests:** gate table (every non-low tier with green CI), `classifyRisk` table (case, prefix containment, duplicates), end-to-end through the fake Gitea (medium/high/none/ambiguous/unknown/case, issue lookup 500/404/403, risk/low not overriding red CI or a protected base, tier reported). Nine mutants (gate ignores risk, no-issue=low, unreadable=low, prefix→contains, ambiguous=low, unknown=low, medium=low, …) each turn tests red. `task check` exit 0. **Behaviour change for callers:** a `tbd_ship` call without `issue` now opens the PR and stops (it used to merge on green). That is the point; callers that want merge-on-green pass the `risk/low` issue. **Stated limit:** the gate enforces the tier of the issue the caller names; it cannot prove that issue is the change's own. The link is the caller's claim. **Not done here / for Mathias:** merging this PR triggers gitea-mcp's CD to production on koala. Gate code (rule 8): an adversarial review by execution is requested before merge; this PR is deliberately left open for it. --- _Created via git-mcp on behalf of @mathias_
mathias added 1 commit 2026-10-07 17:36:31 +00:00
feat(tbd_ship): gate auto-merge on the risk tier; unclassified is not low (gitea-mcp#63)
CD / Lint / Test / Vet (pull_request) Successful in 12s
CD / Build & Import (pull_request) Skipped
CD / Deploy via GitOps (pull_request) Skipped
c7c283d34f
tbd_ship takes an optional `issue`; the PR is squash-merged only when CI is fully green, the base is not
review-protected AND the issue carries exactly one risk/ label, risk/low. No issue, no label, several labels,
an unknown value, risk/medium, risk/high or an unreadable issue all fail closed to PR-only with a reason, and the
result reports the tier. The zero tier is not low. Tests: gate table incl. every non-low tier, classifyRisk table,
end-to-end cases (green+medium, lookup 500/404/403, label case, containment), nine mutants flip-tested red.

Limit: the gate enforces the tier of the issue the caller names; it cannot prove it is the right issue.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SZkhi49hfeSqBp5aZJNXsM
mathias added 1 commit 2026-10-07 17:57:26 +00:00
test(tbd_ship): pin value case/whitespace as non-low; descriptor lists the risk key (review nits)
CD / Lint / Test / Vet (pull_request) Successful in 13s
CD / Build & Import (pull_request) Skipped
CD / Deploy via GitOps (pull_request) Skipped
66339c817b
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SZkhi49hfeSqBp5aZJNXsM
Author
Owner

Adversarial review verdict (gate code, local-dev rule 8): PASS at c7c283d (+ nit commit). Agent reviewer, not four-eyes; review only, not merged.

By execution: label tricks (trailing space/newline/NUL, Cyrillic and zero-width lookalikes, risk/low/extra, risk/ low, case, fullwidth slash), duplicate/multiple labels in any order, issue as duplicate/string/float/overflow/negative/null, alias shadowing (number/index cannot reach issue), issue lookup 404/403/500/malformed JSON, resume path, merge-conflict path, and the only MergePullRequest call site in tbd_ship sits behind evaluateShipGate. No bypass. 14 reviewer mutants, 12 killed; the 2 survivors were an equivalent dead nil-guard and a test gap (value case/whitespace) now pinned by new tests (mutant re-run: 4 failing). go test ./... -race -count=1 passes; task check exit 0.

Stated limits (not bypasses): the caller names the issue (an unrelated, closed, or its own PR number that carries risk/low is taken on trust; documented); pr_merge (internal/tools/pr_merge.go) is a separate merge path with no tier gate; fakes, not real Gitea, so the real issue shape for a PR number is unverified.

Not merged on purpose: merging triggers gitea-mcp's CD to production. Behaviour change for callers: tbd_ship without issue now opens the PR and stops.


Created via git-mcp on behalf of @mathias

**Adversarial review verdict (gate code, local-dev rule 8): PASS** at c7c283d (+ nit commit). Agent reviewer, not four-eyes; review only, not merged. By execution: label tricks (trailing space/newline/NUL, Cyrillic and zero-width lookalikes, `risk/low/extra`, `risk/ low`, case, fullwidth slash), duplicate/multiple labels in any order, `issue` as duplicate/string/float/overflow/negative/null, alias shadowing (`number`/`index` cannot reach `issue`), issue lookup 404/403/500/malformed JSON, resume path, merge-conflict path, and the only `MergePullRequest` call site in tbd_ship sits behind `evaluateShipGate`. No bypass. 14 reviewer mutants, 12 killed; the 2 survivors were an equivalent dead nil-guard and a test gap (value case/whitespace) now pinned by new tests (mutant re-run: 4 failing). `go test ./... -race -count=1` passes; `task check` exit 0. Stated limits (not bypasses): the caller names the issue (an unrelated, closed, or its own PR number that carries `risk/low` is taken on trust; documented); `pr_merge` (internal/tools/pr_merge.go) is a separate merge path with no tier gate; fakes, not real Gitea, so the real issue shape for a PR number is unverified. **Not merged on purpose:** merging triggers gitea-mcp's CD to production. Behaviour change for callers: `tbd_ship` without `issue` now opens the PR and stops. --- _Created via git-mcp on behalf of @mathias_
mathias merged commit d3faffc4c4 into main 2026-10-08 06:02:34 +00:00
Sign in to join this conversation.