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) {
272272 t.Fatalf("mr create: %s", errOut)
273273 }
274274 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 {
276276 t.Fatalf("status set: %s", errOut)
277277 }
278278 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) {
7272 must("", "milestone", "create", "alice/app", "m1")
7373 must("", "mr", "create", "alice/app", "--source", "feat", "--target", "main", "--title", "change")
7474 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")
7676 must("", "release", "create", "alice/app", "v1", "--title", "first")
7777 must("data\n", "release", "asset", "add", "alice/app", "v1", "a.txt")
7878 snippetOut := must("hello\n", "snippet", "create", "a.txt", "--json")
e2e/status_test.go +5
@@ -48,6 +48,11 @@ func TestCommitStatuses(t *testing.T) {
4848 t.Fatal("reader reported a status")
4949 }
5050
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
5156 // Bob (write) reports pending, then success: upsert, not duplicate.
5257 if _, errOut, code := inst.ssh(t, bobKey, "", "status", "set", "alice/svc", head,
5358 "--context", "build", "--state", "pending", "--description", "'compiling'"); code != 0 {
internal/control/status.go +10 −2
@@ -16,13 +16,13 @@ func init() {
1616 Summary: "report a commit status (CI)",
1717 Usage: "status set <owner/name> <sha> --context <c> --state pending|success|failure|error [--description <d>] [--url <u>]",
1818 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", ""},
2020 {"--state", "pending|success|failure|error", "the check's outcome", ""},
2121 {"--description", "<d>", "short text shown beside the state", ""},
2222 {"--url", "<u>", "link to the check's own output", ""},
2323 },
2424 Examples: []string{
25 "status set krz/gitbay a1b2c3d --context ci/build --state success",
25 "status set krz/gitbay a1b2c3d --context ext/lint --state success",
2626 },
2727 Run: runStatusSet})
2828 register(Command{Path: []string{"status", "list"},
@@ -68,6 +68,14 @@ func runStatusSet(c *Ctx, args []string) int {
6868 if path == "" || sha == "" || context == "" || !validStatusState[state] {
6969 return c.usage()
7070 }
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 }
7179 if url != "" && !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") {
7280 return c.fail(protocol.ExitUsage, "--url must be http(s)")
7381 }
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}