Second half of #01M5XF4XWHVMMCQEYENA8X32GK. The landing operation is on main
(#01M5XFEHF38KDXD21TZVJS15GS); this adds vole change merge <id>, which is the part
the CLI could not do from a clone.
Why this needs a transport
Landing is allowed or refused by three things that live in the instance and nowhere
in a working tree: the repository’s ACL, the approvals filed against the request, and
the run results. The CLI already commits metadata by pushing, but a merge has to move
refs/heads/<base> — a push of the base branch is precisely what this feature
replaces, and doing it from the client would put the decision on the wrong side.
So the client asks the server over the key it already pushes with
(vole-change-merge <repo-path> <request-id>), next to vole-runner, vole-roster
and vole-whoami on the same SSH channel. The server resolves the rules, runs
forge/landing.Land, and writes the merge line to the audit log the way a push does.
What changed
server/ssh/merge.go: the verb. It takes exactly a repository path and a request id — no shell, no arbitrary arguments — runs the repository path through the samerepoNamecheck the git verbs use, requiresmaintain(CanTriage), builds alanding.Gatefrom the server’s own open run queue and the ACL’s effective role, and answers one line:merged … into main at <sha>, or the gate’s refusal prefixedvole:so the client can turn it into an error.client/remote.go:MergeChangeresolves the remote, refuses a path that does not name a repository, and runs the verb. It reusesRunSSH, so the error path is the one the other verbs already use.cmd/vole/change.go:vole change merge <id>, printed as the server wrote it — the answer names the commit that landed.server/ssh/server.go: the verb is registered, and the “only … are available” refusal lists it.
Verification
vole workflow run verify in the pinned image: gofmt, vet, build, govulncheck
(0 reachable), packaging, links, go test -race ./... — all pass.
New tests (server/ssh/merge_test.go), driven over a real SSH session:
- a
readaccount is refused, and the refusal names the missing capability; - a maintainer asking for a request that does not exist gets the landing gate’s own
refusal, prefixed
vole:, which is what the client turns into an error; - the verb is not a shell: a missing argument, a missing id, a name with
.., and a command with; cat /etc/passwdappended all fail without executing anything.
The landing rules themselves are covered by the nine tests in forge/landing from the
first half; this change is the wiring, and the wiring is what these tests pin.
Dogfooding note
The instance is running the first half, so the browser button and the merged status are live. The verb needs a restart onto this commit, after which the CLI path can be exercised against the same repository:
vole change merge <id>
Acceptance against the issue
-
vole change merge <id>— lands the approved head and marks the request merged - Same gate as the browser button and the pre-receive hook (
forge/landing.Check) -
maintainand above; the author may merge their own approved request - Refusals name the reason and change nothing
- Audited, in the same shape as a merge that arrives by push