Commit 83d1ba3b20

83d1ba3b2086bd28958403ede0fa292eb8291266

parent: 0215292cee

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-12 06:00 UTC

control: every refusal says what to do instead

Ref #183
internal/control/audit.go +1 −1
@@ -20,7 +20,7 @@ func init() {
2020
2121func runAudit(c *Ctx, args []string) int {
2222 if !c.User.IsAdmin {
23 return c.fail(protocol.ExitDenied, "the audit log is for instance admins")
23 return c.fail(protocol.ExitDenied, "the audit log is for instance admins; ask one")
2424 }
2525 f := store.AuditFilter{Limit: 100}
2626 fl, err := parseFlags(args, flagSpec{Values: []string{"--limit", "--actor", "--action", "--since"}, MaxPos: 0, Usage: c.Cmd.Usage})
internal/control/build.go +4 −4
@@ -376,7 +376,7 @@ func runRunnerNext(c *Ctx, args []string) int {
376376 return c.fail(protocol.ExitFailure, "%v", err)
377377 }
378378 if !ok {
379 return c.fail(protocol.ExitDenied, "this key is not attached to %s", repo.Path())
379 return c.fail(protocol.ExitDenied, "this key is not attached to %s; a repository admin attaches it with repo runner add", repo.Path())
380380 }
381381 repoIDs = append(repoIDs, repo.ID)
382382 }
@@ -475,7 +475,7 @@ func runRunnerLog(c *Ctx, args []string) int {
475475 } else if ok, err := runnerMayBuild(c, key, b.RepoID); err != nil {
476476 return c.fail(protocol.ExitFailure, "%v", err)
477477 } else if !ok {
478 return c.fail(protocol.ExitDenied, "this key is not attached to the build's repository")
478 return c.fail(protocol.ExitDenied, "this key is not attached to the build's repository; a repository admin attaches it with repo runner add")
479479 }
480480 // Stream stdin into the log in chunks so long builds appear live. An
481481 // append that fails drops its chunk and the loop keeps draining: ending
@@ -555,7 +555,7 @@ func runRunnerDone(c *Ctx, args []string) int {
555555 if ok, err := runnerMayBuild(c, key, b.RepoID); err != nil {
556556 return c.fail(protocol.ExitFailure, "%v", err)
557557 } else if !ok {
558 return c.fail(protocol.ExitDenied, "this key is not attached to the build's repository")
558 return c.fail(protocol.ExitDenied, "this key is not attached to the build's repository; a repository admin attaches it with repo runner add")
559559 }
560560 // Cancelled underneath the runner: its report is late, not wrong.
561561 // The row, the status and the log were settled by the cancel.
@@ -829,7 +829,7 @@ func runBuildCancel(c *Ctx, args []string) int {
829829 return c.fail(protocol.ExitFailure, "%v", err)
830830 }
831831 if !policy.CanWrite(c.User, repo, grant) {
832 return c.fail(protocol.ExitDenied, "cancelling a build needs write access to %s", repo.Path())
832 return c.fail(protocol.ExitDenied, "cancelling a build needs write access to %s; ask its owner", repo.Path())
833833 }
834834 if b.Status != "pending" && b.Status != "running" {
835835 return c.fail(protocol.ExitUsage, "build %d is %s; only a queued or running build can be cancelled", b.Number, b.Status)
internal/control/commitfile.go +1 −1
@@ -56,7 +56,7 @@ func runCommitFile(c *Ctx, args []string) int {
5656 return c.fail(protocol.ExitUsage, "path must stay inside the repository")
5757 }
5858 if repo.Settings.RequireMR && slices.Contains(repo.Settings.ProtectedBranches, ref) {
59 return c.fail(protocol.ExitDenied, "branch %s accepts changes through merge requests only", ref)
59 return c.fail(protocol.ExitDenied, "branch %s accepts changes through merge requests only; push another branch and open one", ref)
6060 }
6161 // The server authors this commit, so it cannot sign it.
6262 if repo.Settings.RequireSignedCommits {
internal/control/org.go +1 −1
@@ -53,7 +53,7 @@ func orgAdmin(c *Ctx, name string) (store.Org, int) {
5353 return org, c.fail(protocol.ExitFailure, "%v", err)
5454 }
5555 if role != "admin" {
56 return org, c.fail(protocol.ExitDenied, "only admins of %s can do that", name)
56 return org, c.fail(protocol.ExitDenied, "only admins of %s can do that; ask one", name)
5757 }
5858 return org, -1
5959}
internal/control/orglabel.go +1 −1
@@ -56,7 +56,7 @@ func orgReader(c *Ctx, name string) (store.Org, []int64, int) {
5656 return org, nil, c.fail(protocol.ExitFailure, "%v", err)
5757 }
5858 if role == "" && len(readable) == 0 {
59 return org, nil, c.fail(protocol.ExitDenied, "labels and milestones of %s are visible to its members", name)
59 return org, nil, c.fail(protocol.ExitDenied, "labels and milestones of %s are visible to its members; ask an admin to add you", name)
6060 }
6161 return org, readable, -1
6262}
internal/control/snippet.go +1 −1
@@ -93,7 +93,7 @@ func snippetRef(c *Ctx, id string, write bool) (store.Snippet, int) {
9393 return sn, c.fail(protocol.ExitNotFound, "no snippet %q", id)
9494 }
9595 if write && !policy.CanWriteSnippet(c.User, sn) {
96 return sn, c.fail(protocol.ExitDenied, "snippet %s belongs to %s", id, sn.OwnerName)
96 return sn, c.fail(protocol.ExitDenied, "snippet %s belongs to %s; only they can change it", id, sn.OwnerName)
9797 }
9898 return sn, -1
9999}
internal/control/teams.go +2 −2
@@ -53,7 +53,7 @@ func orgAdminRef(c *Ctx, name string) (store.Org, int) {
5353 return org, c.fail(protocol.ExitFailure, "%v", err)
5454 }
5555 if role != "admin" {
56 return org, c.fail(protocol.ExitDenied, "only admins of %s can manage teams", name)
56 return org, c.fail(protocol.ExitDenied, "only admins of %s can manage teams; ask one to add you", name)
5757 }
5858 return org, -1
5959}
@@ -71,7 +71,7 @@ func orgMemberRef(c *Ctx, name string) (store.Org, int) {
7171 return org, c.fail(protocol.ExitFailure, "%v", err)
7272 }
7373 if role == "" {
74 return org, c.fail(protocol.ExitDenied, "teams of %s are visible to its members", name)
74 return org, c.fail(protocol.ExitDenied, "teams of %s are visible to its members; ask an admin to add you", name)
7575 }
7676 return org, -1
7777}