Add push/merge/approvals whitelist params so an admin can allow a specific user (e.g. a release bot) to push to an otherwise locked branch: - enable_push_whitelist / push_whitelist_usernames - enable_merge_whitelist / merge_whitelist_usernames - enable_approvals_whitelist / approvals_whitelist_usernames Username lists are comma-separated and replace the existing list. PATCH null-safety extends to them: an unpassed list serializes as null (leave-unchanged), never [] (which would wipe an existing whitelist). Covered by TestEditBranchProtectionFn_PushWhitelistRoundTrip. Update the co-located branch-protection showboat demo and link it from demos/README.md (new "Branch protection (governance)" section).
7.2 KiB
Branch protection management (uc6)
2026-06-02T14:30:58Z by Showboat 0.6.1
Captured: 2026-06-02 via Showboat 0.6.1
Proves the branch-protection capability — spec spec.md (archived change 2026-06-02-branch-protection-management), issue forgejo-mcp-uc6 (discovered from forgejo-mcp-f6h: a repo with no protection let Renovate automerge before CI was green).
Token-free demo. Reading or writing real branch protection requires a repo-admin token, and the server, like any client, must not leak it. This demo deliberately uses no token and no live instance: it proves the surface through the CLI tool registry and the validation path, and proves behaviour through the test suite, which exercises the exact Forgejo HTTP round-trips against an in-process
httptestserver. No secrets appear anywhere in this file.
Replay setup
export FORGEJO_MCP_BIN="${FORGEJO_MCP_BIN:-./forgejo-mcp}" # local build of this branch
# All commands below run from the repo root and need NO token / NO network.
The five tools are registered
The server exposes the branch-protection CRUD tools (token-free introspection via the CLI tool registry).
"${FORGEJO_MCP_BIN:-./forgejo-mcp}" --cli list 2>/dev/null | grep branch_protection
create_branch_protection Create a branch protection rule (e.g. require status checks before merge)
delete_branch_protection Delete a branch protection rule by name
edit_branch_protection Edit a branch protection rule. Only fields you pass are changed; omitted fields are left untouched.
get_branch_protection Get a single branch protection rule by name
list_branch_protections List a repository's branch protection rules (bounded by page/limit)
Scenario: Create requires a branch name
Spec: create_branch_protection ... SHALL return an error result and SHALL NOT call Forgejo when branch_name is missing. The guard runs before any client/network call, so this is reproducible with no token and no URL.
"${FORGEJO_MCP_BIN:-./forgejo-mcp}" -debug=false --cli create_branch_protection --args '{"owner":"goern","repo":"forgejo-mcp"}' 2>&1 | grep -o "branch_name is required" | head -1
branch_name is required
Scenario: Whitelist a user to push to a protected branch
edit_branch_protection accepts push/merge/approvals whitelist params so an
admin can allow a specific user (e.g. a release bot like op1st-gitops) to
push to an otherwise locked branch. Usernames are comma-separated and replace
the existing list; each user must already be a collaborator with write access —
the whitelist filters which writers may push, it does not grant access.
"${FORGEJO_MCP_BIN:-./forgejo-mcp}" --cli list 2>/dev/null \
| grep edit_branch_protection
edit_branch_protection Edit a branch protection rule. Only fields you pass are changed; omitted fields are left untouched.
PATCH null-safety extends to the whitelists: an unpassed list serializes as
null (leave-unchanged), never [] (which would silently wipe an existing
whitelist). This is asserted by TestEditBranchProtectionFn_PushWhitelistRoundTrip
below, which sends enable_push_whitelist: true + push_whitelist_usernames: ["goern","op1st-gitops"] and verifies the unpassed merge whitelist stays null.
Scenario coverage via the executable spec
Each spec scenario is a test against an in-process httptest Forgejo (real request/response, no network, no token). The names map 1:1 to the spec: status_check_contexts round-trip, edit PATCH null-safety, push-whitelist round-trip, list/collection bounding + truncation sentinel, get/collection 404 → resource error, single-resource happy path, malformed URI → invalid-params, and the slash-glob rule URI.
go test -v -run "BranchProtection|ParseBranchProtection|SplitContexts" ./operation/branchprotection/ ./operation/resource/ 2>&1 | grep -E "^(=== RUN|--- PASS|--- FAIL|PASS|FAIL|ok)" | sed -E "s#\t# #g"
=== RUN TestListBranchProtectionsFn
--- PASS: TestListBranchProtectionsFn (0.00s)
=== RUN TestGetBranchProtectionFn_OK
--- PASS: TestGetBranchProtectionFn_OK (0.00s)
=== RUN TestGetBranchProtectionFn_NotFound
--- PASS: TestGetBranchProtectionFn_NotFound (0.00s)
=== RUN TestCreateBranchProtectionFn_StatusCheckRoundTrip
--- PASS: TestCreateBranchProtectionFn_StatusCheckRoundTrip (0.00s)
=== RUN TestCreateBranchProtectionFn_MissingBranchName
--- PASS: TestCreateBranchProtectionFn_MissingBranchName (0.00s)
=== RUN TestEditBranchProtectionFn_OnlyPassedFields
--- PASS: TestEditBranchProtectionFn_OnlyPassedFields (0.00s)
=== RUN TestEditBranchProtectionFn_ContextsRoundTrip
--- PASS: TestEditBranchProtectionFn_ContextsRoundTrip (0.00s)
=== RUN TestEditBranchProtectionFn_PushWhitelistRoundTrip
--- PASS: TestEditBranchProtectionFn_PushWhitelistRoundTrip (0.00s)
=== RUN TestDeleteBranchProtectionFn_OK
--- PASS: TestDeleteBranchProtectionFn_OK (0.00s)
=== RUN TestSplitContexts
--- PASS: TestSplitContexts (0.00s)
=== RUN TestBranchProtectionsResource_HappyPath
--- PASS: TestBranchProtectionsResource_HappyPath (0.00s)
=== RUN TestBranchProtectionsResource_Truncation
--- PASS: TestBranchProtectionsResource_Truncation (0.00s)
=== RUN TestBranchProtectionsResource_NotFound
--- PASS: TestBranchProtectionsResource_NotFound (0.00s)
=== RUN TestBranchProtectionResource_HappyPath
--- PASS: TestBranchProtectionResource_HappyPath (0.00s)
=== RUN TestBranchProtectionResource_MalformedURI
--- PASS: TestBranchProtectionResource_MalformedURI (0.00s)
PASS
ok codeberg.org/goern/forgejo-mcp/v2/operation/branchprotection (cached)
=== RUN TestParseBranchProtections
--- PASS: TestParseBranchProtections (0.00s)
=== RUN TestParseBranchProtections_Invalid
--- PASS: TestParseBranchProtections_Invalid (0.00s)
=== RUN TestParseBranchProtection
--- PASS: TestParseBranchProtection (0.00s)
=== RUN TestParseBranchProtection_GlobRuleWithSlash
--- PASS: TestParseBranchProtection_GlobRuleWithSlash (0.00s)
=== RUN TestParseBranchProtection_Invalid
--- PASS: TestParseBranchProtection_Invalid (0.00s)
PASS
ok codeberg.org/goern/forgejo-mcp/v2/operation/resource (cached)
What this demo does not show (by design)
A live create_branch_protection / list_branch_protections against a real repo is omitted: it needs a repo-admin token, and showing it risks leaking that token in CLI debug output. The httptest suite above drives the identical SDK calls and asserts the exact request bodies (e.g. enable_status_check: true + the status_check_contexts list) and responses, so the contract is proven without a secret. To run it live yourself: forgejo-mcp --cli list_branch_protections --args '{"owner":"...","repo":"..."}' with -url/-token set in your shell — never on the command line of a shared session.