chadtmiller.com/knotless
git that lives in your Atmosphere account
knotless-serve: The delegation funnel follows the owner the gateway writes for
MergedThis adds a gateway.served event, told by the repository owner, and moves the delegated funnel from gateway.push to gateway.served. Docs, the lexicon's known values and the tests follow. The idea is sound, and the commit path correctly fires only on ok.
The push path has a problem. gateway.served is emitted whenever push returns, including when every ref was refused or failed. report.extend(refused) shows that a push can return Ok with only per-ref errors. The funnel is meant to show the gateway writing for the owner, so a delegated owner whose only pushes were all refused would count as reached. Emit it only when at least one ref in report succeeded, which is res.is_ok().
The tests don't cover this. The new tests/usage.rs case calls app.event directly and never goes through the handler, so it can't catch the problem above. A handler-level test with a refused push would.
Minor: one line in deploy/README.md runs long ("written for then or since, by anyone's push or commit; of those that signed in to MCP so, how many have").
crates/knotless-serve/src/gateway.rsline 665Outdated
report.extend(refused); | |
app.event("gateway.push", "", true, started.elapsed(), Some(&caller)); | |
// By the owner it wrote for, whoever asked: what delegating led to. | |
app.event("gateway.served", "", true, started.elapsed(), Some(remote.did())); |
This fires even if every ref was refused or failed, since report can hold only Err entries. Guard it with report.iter().any(|(_, r)| r.is_ok()), as the commit path does with ok.
Guarded with report.iter().any(|(_, res)| res.is_ok()) in c284fbf5, and protected_branches_hold_collaborators now checks through the handler that a refused push tells nothing and one that lands does. The README line is rewrapped.
Both points from my earlier review are addressed. In push, gateway.served now fires only when at least one ref in report succeeded (res.is_ok()). commit still fires it only on ok. The new handler-level assertions in tests/gateway.rs cover this: after the refused push, served is false for the owner. After a push that moves a ref, it is true, even though a collaborator pushed. The README funnel text is also reflowed, so the long line is gone. I didn't run the checks, so I haven't seen the tests pass.
8 passed, 1 skipped
What changes
New event,
gateway.served. Told by the repository's owner (remote.did()) whenever the gateway pushes for them and at least one ref moves, or lands a commit for them. A push whose every ref was refused serves no one.gateway.pushandgateway.commitstill record the caller, so their account counts keep meaning who wrote.The funnel follows
gateway.delegated -> gateway.servedinstead of-> gateway.push. Docs, thegetUsagelexicon's known values and the tests follow.Why
gateway.delegatedis told by the owner who approved delegation,gateway.pushby whoever pushed through the gateway: a collaborator or a runner. The funnel joins the two on one DID, so it only counted an owner who later pushed through their own gateway, which owners rarely do since they can write past it. Prod showed 1 of 3 reached, which said nothing about whether delegation led to use.Recording the owner on the existing push event was the other option, but it would lose who actually pushed.
How it was checked
usage.rs's cohort test andtests/usage.rstellgateway.servedand assert the new funnel. The gateway tests now count usage, andprotected_branches_hold_collaboratorschecks through the handler that a collaborator's refused push tells nogateway.servedfor the owner, and that the next push, which lands, does. Left to CI.Shortcomings
No backfill: until this deploys and a delegated owner's gateway writes again, the funnel reads 0 reached for the days already counted. Runner pushes (merge queue landings) count as served too, which seems right since they need delegation, but it means an owner whose only gateway use is the queue still reaches.
Review
A quick look; the guarded events in
gateway.rsare the change.Changeset: none
🤖 Generated with Claude Code