Commit b022fedfc0

b022fedfc0aa15b78cc616037e75364406294911

parent: 7b644421d3

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 06:40 UTC

status set: ci/ is reserved for the instance's builds

Ref #258

Layout: unified · split

e2e/mrweb_test.go +1 −1
@@ -272,7 +272,7 @@ func TestMRListRows(t *testing.T) {
272 t.Fatalf("mr create: %s", errOut) 272 t.Fatalf("mr create: %s", errOut)
273 } 273 }
274 if _, errOut, code := inst.ssh(t, aliceKey, "", "status", "set", "alice/lib", sha, 274 if _, errOut, code := inst.ssh(t, aliceKey, "", "status", "set", "alice/lib", sha,
275 "--context", "ci/test", "--state", "success"); code != 0 { 275 "--context", "ext/test", "--state", "success"); code != 0 {
276 t.Fatalf("status set: %s", errOut) 276 t.Fatalf("status set: %s", errOut)
277 } 277 }
278 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "comment", "alice/lib", "1", 278 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "comment", "alice/lib", "1",
e2e/readonly_test.go +1 −1
@@ -72,7 +72,7 @@ func TestReadOnlyCommandsWriteNothing(t *testing.T) {
72 must("", "milestone", "create", "alice/app", "m1") 72 must("", "milestone", "create", "alice/app", "m1")
73 must("", "mr", "create", "alice/app", "--source", "feat", "--target", "main", "--title", "change") 73 must("", "mr", "create", "alice/app", "--source", "feat", "--target", "main", "--title", "change")
74 must("", "mr", "diff-comment", "alice/app", "1", "--path", "f.go", "--line", "3", "--message", "why") 74 must("", "mr", "diff-comment", "alice/app", "1", "--path", "f.go", "--line", "3", "--message", "why")
75 must("", "status", "set", "alice/app", sha, "--context", "ci/x", "--state", "success") 75 must("", "status", "set", "alice/app", sha, "--context", "ext/x", "--state", "success")
76 must("", "release", "create", "alice/app", "v1", "--title", "first") 76 must("", "release", "create", "alice/app", "v1", "--title", "first")
77 must("data\n", "release", "asset", "add", "alice/app", "v1", "a.txt") 77 must("data\n", "release", "asset", "add", "alice/app", "v1", "a.txt")
78 snippetOut := must("hello\n", "snippet", "create", "a.txt", "--json") 78 snippetOut := must("hello\n", "snippet", "create", "a.txt", "--json")
e2e/status_test.go +5
@@ -48,6 +48,11 @@ func TestCommitStatuses(t *testing.T) {
48 t.Fatal("reader reported a status") 48 t.Fatal("reader reported a status")
49 } 49 }
50 50
51 // ci/ is the instance's own: a writer is refused it (#258).
52 if _, errOut, code := inst.ssh(t, bobKey, "", "status", "set", "alice/svc", head, "--context", "ci/build", "--state", "success"); code != 4 || !strings.Contains(errOut, "reserved") {
53 t.Fatalf("writer posted a ci/ status: exit %d, %s", code, errOut)
54 }
55
51 // Bob (write) reports pending, then success: upsert, not duplicate. 56 // Bob (write) reports pending, then success: upsert, not duplicate.
52 if _, errOut, code := inst.ssh(t, bobKey, "", "status", "set", "alice/svc", head, 57 if _, errOut, code := inst.ssh(t, bobKey, "", "status", "set", "alice/svc", head,
53 "--context", "build", "--state", "pending", "--description", "'compiling'"); code != 0 { 58 "--context", "build", "--state", "pending", "--description", "'compiling'"); code != 0 {
internal/control/status.go +10 −2
@@ -16,13 +16,13 @@ func init() {
16 Summary: "report a commit status (CI)", 16 Summary: "report a commit status (CI)",
17 Usage: "status set <owner/name> <sha> --context <c> --state pending|success|failure|error [--description <d>] [--url <u>]", 17 Usage: "status set <owner/name> <sha> --context <c> --state pending|success|failure|error [--description <d>] [--url <u>]",
18 Flags: []Flag{ 18 Flags: []Flag{
19 {"--context", "<c>", "the check this status reports for", ""}, 19 {"--context", "<c>", "the check this status reports for; ci/ is reserved for the instance's builds", ""},
20 {"--state", "pending|success|failure|error", "the check's outcome", ""}, 20 {"--state", "pending|success|failure|error", "the check's outcome", ""},
21 {"--description", "<d>", "short text shown beside the state", ""}, 21 {"--description", "<d>", "short text shown beside the state", ""},
22 {"--url", "<u>", "link to the check's own output", ""}, 22 {"--url", "<u>", "link to the check's own output", ""},
23 }, 23 },
24 Examples: []string{ 24 Examples: []string{
25 "status set krz/gitbay a1b2c3d --context ci/build --state success", 25 "status set krz/gitbay a1b2c3d --context ext/lint --state success",
26 }, 26 },
27 Run: runStatusSet}) 27 Run: runStatusSet})
28 register(Command{Path: []string{"status", "list"}, 28 register(Command{Path: []string{"status", "list"},
@@ -68,6 +68,14 @@ func runStatusSet(c *Ctx, args []string) int {
68 if path == "" || sha == "" || context == "" || !validStatusState[state] { 68 if path == "" || sha == "" || context == "" || !validStatusState[state] {
69 return c.usage() 69 return c.usage()
70 } 70 }
71 // ci/<job> statuses are the build subsystem's: queued, reused,
72 // skipped and finished by the server itself. A writer who could post
73 // one could mark ci/test green on their own head before, or instead
74 // of, the build (#258). Case-folded, so CI/test is no way around it.
75 if strings.HasPrefix(strings.ToLower(context), "ci/") {
76 return c.fail(protocol.ExitDenied, "the ci/ prefix is reserved for the instance's builds; report under another name, such as ext/%s",
77 strings.TrimPrefix(strings.ToLower(context), "ci/"))
78 }
71 if url != "" && !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") { 79 if url != "" && !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") {
72 return c.fail(protocol.ExitUsage, "--url must be http(s)") 80 return c.fail(protocol.ExitUsage, "--url must be http(s)")
73 } 81 }
internal/control/status_test.go added +26
@@ -0,0 +1,26 @@
1package control
2
3import (
4 "strings"
5 "testing"
6
7 "gitbay.org/gitbay/internal/protocol"
8 "gitbay.org/gitbay/internal/store"
9)
10
11// ci/<job> statuses are the build subsystem's. A writer who could post
12// one could mark ci/test green on their own head before, or instead of,
13// the build (#258).
14func TestStatusSetRefusesReservedContext(t *testing.T) {
15 st, repo, uid := newQueueTestRepo(t)
16 for _, ctx := range []string{"ci/test", "CI/test", "ci/"} {
17 c, errOut := pruneCtx(st, t.TempDir(), store.User{ID: uid, Username: "alice"})
18 code := Dispatch(c, []string{"status", "set", repo.Path(), "abc1234", "--context", ctx, "--state", "success"})
19 if code != protocol.ExitDenied || !strings.Contains(errOut.String(), "reserved") {
20 t.Errorf("--context %s: exit %d, %s", ctx, code, errOut.String())
21 }
22 }
23 if has, err := st.RepoHasStatuses(repo.ID); err != nil || has {
24 t.Fatalf("a refused status was stored: %v %v", has, err)
25 }
26}