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 | 272 | t.Fatalf("mr create: %s", errOut) |
| 273 | 273 | } |
| 274 | 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 | 276 | t.Fatalf("status set: %s", errOut) |
| 277 | 277 | } |
| 278 | 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 | 72 | must("", "milestone", "create", "alice/app", "m1") |
| 73 | 73 | must("", "mr", "create", "alice/app", "--source", "feat", "--target", "main", "--title", "change") |
| 74 | 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 | 76 | must("", "release", "create", "alice/app", "v1", "--title", "first") |
| 77 | 77 | must("data\n", "release", "asset", "add", "alice/app", "v1", "a.txt") |
| 78 | 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 | 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 | 56 | // Bob (write) reports pending, then success: upsert, not duplicate. |
| 52 | 57 | if _, errOut, code := inst.ssh(t, bobKey, "", "status", "set", "alice/svc", head, |
| 53 | 58 | "--context", "build", "--state", "pending", "--description", "'compiling'"); code != 0 { |
internal/control/status.go
+10 −2
| @@ -16,13 +16,13 @@ func init() { |
| 16 | 16 | Summary: "report a commit status (CI)", |
| 17 | 17 | Usage: "status set <owner/name> <sha> --context <c> --state pending|success|failure|error [--description <d>] [--url <u>]", |
| 18 | 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 | 20 | {"--state", "pending|success|failure|error", "the check's outcome", ""}, |
| 21 | 21 | {"--description", "<d>", "short text shown beside the state", ""}, |
| 22 | 22 | {"--url", "<u>", "link to the check's own output", ""}, |
| 23 | 23 | }, |
| 24 | 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 | 27 | Run: runStatusSet}) |
| 28 | 28 | register(Command{Path: []string{"status", "list"}, |
| @@ -68,6 +68,14 @@ func runStatusSet(c *Ctx, args []string) int { |
| 68 | 68 | if path == "" || sha == "" || context == "" || !validStatusState[state] { |
| 69 | 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 | 79 | if url != "" && !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") { |
| 72 | 80 | return c.fail(protocol.ExitUsage, "--url must be http(s)") |
| 73 | 81 | } |
internal/control/status_test.go
added
+26
| @@ -0,0 +1,26 @@ |
| 1 | package control |
| 2 | |
| 3 | import ( |
| 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). |
| 14 | func 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 | } |