name the domain for a namespace it now shares
Hanzo CI/CD / cicd (push) Waiting to run
CI/CD / gate (push) Waiting to run

These types are published through the composed cloud document, whose
schema namespace is FLAT and global: `Service` appears in fifty-three
other app documents, `Run` and `Head` in ten each, and two names that
differ only in case are worse still — every generator PascalCases, so an
SDK binds whichever it read last or refuses to compile.

Three of the five are better names besides:

  Version  -> Artifact   a tag and a digest identifying built bytes is
                         not a version; nothing here is semver.
  Head     -> Tip        its own comment already said "the tip of the
                         default branch".
  Service  -> Pipeline   the row is not a workload, it is the chain
                         behind one: head, built, declared, running.
  Run      -> Execution   } free names for the two that only collided
  Build    -> Check       }

The wire does not move: JSON comes from the tags and those are
untouched, which the shape test asserts. What changes is the name a
generated client gives the class.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-30 20:20:43 -07:00
co-authored by Hanzo Dev
parent c6a81f5b1b
commit c36ea712a8
12 changed files with 202 additions and 202 deletions
+5 -5
View File
@@ -21,7 +21,7 @@ func renderFleet(w http.ResponseWriter, snap fleet, v viewer, org string, cfg co
services := v.services(snap.Services, org)
data := struct {
Services []Service
Services []Pipeline
Orgs []string
Org string
Viewer string
@@ -52,7 +52,7 @@ func renderFleet(w http.ResponseWriter, snap fleet, v viewer, org string, cfg co
}
}
func countByDrift(services []Service) map[string]int {
func countByDrift(services []Pipeline) map[string]int {
c := map[string]int{unbuilt: 0, unshipped: 0, unsynced: 0, untested: 0, "current": 0}
for _, s := range services {
if s.current() {
@@ -68,7 +68,7 @@ func countByDrift(services []Service) map[string]int {
// tone maps a service to the one colour its row carries. Anything that stops
// what we wrote from running reads the same, because from outside they are the
// same fact: this service is not serving the code we last merged.
func tone(s Service) string {
func tone(s Pipeline) string {
for _, d := range s.Drift {
if d == unbuilt || d == unsynced {
return "failure"
@@ -102,7 +102,7 @@ func since(t time.Time) string {
// A version tag is a few characters; a commit tag is forty, and one of those in
// a column is enough to push every other column off the page. Both are cut to
// the width at which a release is still identifiable.
func short(v Version) string {
func short(v Artifact) string {
if v.Tag != "" {
return clip(v.Tag, 16)
}
@@ -142,7 +142,7 @@ var fleetTmpl = template.Must(template.New("fleet").Funcs(template.FuncMap{
"tone": tone,
"explain": explain,
"sha": shortSHA,
"has": func(s Service, d string) bool {
"has": func(s Pipeline, d string) bool {
for _, x := range s.Drift {
if x == d {
return true
+3 -3
View File
@@ -123,7 +123,7 @@ func (c *cluster) running(ctx context.Context) ([]live, error) {
Name: w.Metadata.Name,
Namespace: w.Metadata.Namespace,
Image: image,
Version: v,
Artifact: v,
Ready: ready,
Want: want,
})
@@ -164,8 +164,8 @@ func (c *cluster) getJSON(ctx context.Context, path string, out any) error {
// because the tag is what a human reads and the digest is what is pulled. Both
// are kept: comparing on the digest is what makes "declared equals running" a
// statement about bytes rather than about a label that can be moved.
func splitImage(ref string) (string, Version) {
var v Version
func splitImage(ref string) (string, Artifact) {
var v Artifact
if i := strings.LastIndex(ref, "@"); i > 0 {
v.Digest, ref = ref[i+1:], ref[:i]
}
+3 -3
View File
@@ -22,9 +22,9 @@ import "strings"
// saying what is declared must never infer it.
// declared returns the image repository and the version pinned to it.
func declared(body string) (string, Version) {
func declared(body string) (string, Artifact) {
var (
v Version
v Artifact
image string
inside bool
)
@@ -60,7 +60,7 @@ func declared(body string) (string, Version) {
}
}
if image == "" {
return "", Version{}
return "", Artifact{}
}
return image, v
}
+15 -15
View File
@@ -10,13 +10,13 @@ import "testing"
func TestDeclaredReadsTheFleetsShapes(t *testing.T) {
cases := []struct {
name, body, image string
want Version
want Artifact
}{{
name: "tag and digest, the shape of almost every service",
body: "replicas: 1\nimage:\n repository: ghcr.io/hanzoai/cloud\n" +
" tag: v1.801.548\n digest: sha256:8830c2b1\n pullPolicy: IfNotPresent\n",
image: "ghcr.io/hanzoai/cloud",
want: Version{Tag: "v1.801.548", Digest: "sha256:8830c2b1"},
want: Artifact{Tag: "v1.801.548", Digest: "sha256:8830c2b1"},
}, {
// The pin a human reads and the bytes the kubelet pulls are written in
// one edit, and comments explaining a release sit between them.
@@ -26,32 +26,32 @@ func TestDeclaredReadsTheFleetsShapes(t *testing.T) {
" # Measured from outside the cluster.\n" +
" tag: v1.801.357\n digest: sha256:aabb\n",
image: "ghcr.io/hanzoai/cloud",
want: Version{Tag: "v1.801.357", Digest: "sha256:aabb"},
want: Artifact{Tag: "v1.801.357", Digest: "sha256:aabb"},
}, {
name: "pinned by digest alone",
body: "image:\n repository: ghcr.io/hanzoai/iam\n digest: sha256:ccdd\n pullPolicy: Always\n",
image: "ghcr.io/hanzoai/iam",
want: Version{Digest: "sha256:ccdd"},
want: Artifact{Digest: "sha256:ccdd"},
}, {
name: "pinned by tag alone",
body: "image:\n repository: ghcr.io/hanzoai/bot-gateway\n tag: v0.3.1\n",
image: "ghcr.io/hanzoai/bot-gateway",
want: Version{Tag: "v0.3.1"},
want: Artifact{Tag: "v0.3.1"},
}, {
name: "quoted",
body: "image:\n repository: \"ghcr.io/hanzoai/a\"\n tag: '0.1.0'\n",
image: "ghcr.io/hanzoai/a",
want: Version{Tag: "0.1.0"},
want: Artifact{Tag: "0.1.0"},
}, {
name: "a comment after the value is not the value",
body: "image:\n repository: ghcr.io/hanzoai/a # the published name\n tag: v1.0.0 # cut by hand\n",
image: "ghcr.io/hanzoai/a",
want: Version{Tag: "v1.0.0"},
want: Artifact{Tag: "v1.0.0"},
}, {
name: "the block ends at the first dedent",
body: "image:\n repository: ghcr.io/hanzoai/a\n tag: v1.0.0\ningress:\n tag: not-an-image\n",
image: "ghcr.io/hanzoai/a",
want: Version{Tag: "v1.0.0"},
want: Artifact{Tag: "v1.0.0"},
}}
for _, tc := range cases {
@@ -97,16 +97,16 @@ func TestDeclaredRefusesWhatItDoesNotUnderstand(t *testing.T) {
func TestSplitImageKeepsBothNames(t *testing.T) {
cases := []struct {
ref, image string
want Version
want Artifact
}{
{"ghcr.io/hanzoai/cloud:v1.801.548@sha256:8830c2b1", "ghcr.io/hanzoai/cloud",
Version{Tag: "v1.801.548", Digest: "sha256:8830c2b1"}},
{"ghcr.io/hanzoai/cloud:v1.801.548", "ghcr.io/hanzoai/cloud", Version{Tag: "v1.801.548"}},
{"ghcr.io/hanzoai/cloud@sha256:8830", "ghcr.io/hanzoai/cloud", Version{Digest: "sha256:8830"}},
{"ghcr.io/hanzoai/cloud", "ghcr.io/hanzoai/cloud", Version{}},
Artifact{Tag: "v1.801.548", Digest: "sha256:8830c2b1"}},
{"ghcr.io/hanzoai/cloud:v1.801.548", "ghcr.io/hanzoai/cloud", Artifact{Tag: "v1.801.548"}},
{"ghcr.io/hanzoai/cloud@sha256:8830", "ghcr.io/hanzoai/cloud", Artifact{Digest: "sha256:8830"}},
{"ghcr.io/hanzoai/cloud", "ghcr.io/hanzoai/cloud", Artifact{}},
// A colon before the last slash is a registry port, not a tag.
{"oci.hanzo.ai:5000/hanzo/a:v1", "oci.hanzo.ai:5000/hanzo/a", Version{Tag: "v1"}},
{"oci.hanzo.ai:5000/hanzo/a", "oci.hanzo.ai:5000/hanzo/a", Version{}},
{"oci.hanzo.ai:5000/hanzo/a:v1", "oci.hanzo.ai:5000/hanzo/a", Artifact{Tag: "v1"}},
{"oci.hanzo.ai:5000/hanzo/a", "oci.hanzo.ai:5000/hanzo/a", Artifact{}},
}
for _, tc := range cases {
image, got := splitImage(tc.ref)
+31 -31
View File
@@ -41,15 +41,15 @@ const (
// Version is one image as the registry answers for it: the tag a human reads and
// the digest the kubelet pulls. Both are carried because they answer different
// questions — a tag can be moved onto other bytes, a digest cannot.
type Version struct {
type Artifact struct {
Tag string `json:"tag"`
Digest string `json:"digest"`
}
func (v Version) known() bool { return v.Tag != "" || v.Digest != "" }
func (v Artifact) known() bool { return v.Tag != "" || v.Digest != "" }
// Build is what Hanzo Git did with one commit.
type Build struct {
type Check struct {
// State is success | failure | running | absent. `absent` is not a kind of
// failure and is kept apart from one: a failing run is a build that ran and
// said no, while an absent run is Hanzo Git never having constructed a run
@@ -77,32 +77,32 @@ type Build struct {
Verdict bool `json:"verdict"`
}
func (b Build) passed() bool { return b.State == "success" }
func (b Check) passed() bool { return b.State == "success" }
// green is passed AND proved. A run that reported success without executing its
// tests is not a green build, and this is the one place that distinction is made.
func (b Build) green() bool { return b.passed() && (!b.Verdict || b.Tested) }
func (b Check) green() bool { return b.passed() && (!b.Verdict || b.Tested) }
// Head is the tip of the default branch — what we wrote.
type Head struct {
type Tip struct {
SHA string `json:"sha"`
Title string `json:"title"`
At time.Time `json:"at"`
Build Build `json:"build"`
Check Check `json:"build"`
}
// Service is one declared workload and the chain behind it.
type Service struct {
type Pipeline struct {
Name string `json:"name"` // cloud
Namespace string `json:"namespace"` // hanzo
Image string `json:"image"` // ghcr.io/hanzoai/cloud
Org string `json:"org"` // Hanzo Git owner; empty when the repo is unresolved
Repo string `json:"repo"` // hanzo-inc/cloud
Head Head `json:"head"`
Built Version `json:"built"`
Declared Version `json:"declared"`
Running Version `json:"running"`
Tip Tip `json:"head"`
Built Artifact `json:"built"`
Declared Artifact `json:"declared"`
Running Artifact `json:"running"`
Ready int `json:"ready"`
Want int `json:"want"`
@@ -121,13 +121,13 @@ type Service struct {
}
// current reports a service whose four values agree.
func (s Service) current() bool { return len(s.Drift) == 0 }
func (s Pipeline) current() bool { return len(s.Drift) == 0 }
// alike reports whether two versions name the same image, and whether that could
// be decided at all. The second result is what keeps an unread source from
// rendering as a difference: a value we failed to fetch is not a value that
// disagrees, and a board that conflates them cries wolf on its own outages.
func alike(a, b Version) (same, decided bool) {
func alike(a, b Artifact) (same, decided bool) {
switch {
case a.Digest != "" && b.Digest != "":
return a.Digest == b.Digest, true
@@ -187,7 +187,7 @@ func after(a, b string) (yes, decided bool) {
// assess names every broken arrow in the line. Each test is written so that an
// undecidable comparison adds nothing.
func (s *Service) assess() {
func (s *Pipeline) assess() {
s.Drift = nil
// The cluster is not running what universe declares: Hanzo CD is behind,
@@ -211,7 +211,7 @@ func (s *Service) assess() {
// A passing build that did not execute its tests. Kept separate from the
// three version comparisons because the versions all agree in this case —
// the line is intact and what flowed down it was never proved.
if s.Head.Build.passed() && s.Head.Build.Verdict && !s.Head.Build.Tested {
if s.Tip.Check.passed() && s.Tip.Check.Verdict && !s.Tip.Check.Tested {
s.Drift = append(s.Drift, untested)
}
}
@@ -223,7 +223,7 @@ type pin struct {
Name string
Namespace string
Image string
Version Version
Artifact Artifact
At time.Time
}
@@ -232,7 +232,7 @@ type live struct {
Name string
Namespace string
Image string
Version Version
Artifact Artifact
Ready int
Want int
}
@@ -241,8 +241,8 @@ type live struct {
type link struct {
Repo string
Org string
Head Head
Built Version
Tip Tip
Built Artifact
Behind int
Since time.Time
}
@@ -258,13 +258,13 @@ type link struct {
// So the namespace and name carry the row, the cluster is matched on the same
// pair, and the IMAGE is what joins to Hanzo Git — several services legitimately
// share one repo, and then they share its head and its last build too.
func assemble(pins []pin, lives []live, links map[string]link) []Service {
func assemble(pins []pin, lives []live, links map[string]link) []Pipeline {
at := func(namespace, name string) string { return namespace + "/" + name }
byName := make(map[string]*Service, len(pins))
byName := make(map[string]*Pipeline, len(pins))
for _, p := range pins {
byName[at(p.Namespace, p.Name)] = &Service{
byName[at(p.Namespace, p.Name)] = &Pipeline{
Name: p.Name, Namespace: p.Namespace, Image: p.Image,
Declared: p.Version, PinnedAt: p.At,
Declared: p.Artifact, PinnedAt: p.At,
}
}
for _, l := range lives {
@@ -287,15 +287,15 @@ func assemble(pins []pin, lives []live, links map[string]link) []Service {
if same, decided := alike(s.Running, s.Declared); decided && same {
continue
}
s.Running, s.Ready, s.Want = l.Version, l.Ready, l.Want
s.Running, s.Ready, s.Want = l.Artifact, l.Ready, l.Want
}
for _, s := range byName {
if c, ok := links[s.Image]; ok {
s.Repo, s.Org, s.Head, s.Built, s.Behind, s.Since = c.Repo, c.Org, c.Head, c.Built, c.Behind, c.Since
s.Repo, s.Org, s.Tip, s.Built, s.Behind, s.Since = c.Repo, c.Org, c.Tip, c.Built, c.Behind, c.Since
}
}
out := make([]Service, 0, len(byName))
out := make([]Pipeline, 0, len(byName))
for _, s := range byName {
s.assess()
out = append(out, *s)
@@ -330,7 +330,7 @@ func imageName(image string) string {
// ───────────────────────────── snapshot ─────────────────────────────
type fleet struct {
Services []Service `json:"services"`
Services []Pipeline `json:"services"`
FetchedAt time.Time `json:"fetchedAt"`
Err error `json:"-"`
}
@@ -380,7 +380,7 @@ func (c *fleetCache) put(f fleet) {
// own: a tenant is always narrowed to its own org slug, which is never empty, so
// an unattributed row matches no tenant. Tenancy we could not establish is
// therefore never granted to a tenant by default.
func (v viewer) services(all []Service, want string) []Service {
func (v viewer) services(all []Pipeline, want string) []Pipeline {
want = strings.TrimSpace(want)
if !v.sudo {
if want != "" && !strings.EqualFold(want, v.org) {
@@ -388,7 +388,7 @@ func (v viewer) services(all []Service, want string) []Service {
}
want = v.org
}
out := make([]Service, 0, len(all))
out := make([]Pipeline, 0, len(all))
for _, s := range all {
if want != "" && !strings.EqualFold(s.Org, want) {
continue
@@ -401,7 +401,7 @@ func (v viewer) services(all []Service, want string) []Service {
// orgsOfServices lists the orgs present in a service list. It is called with a
// list that has ALREADY passed v.services, so a tenant's nav is built from a
// tenant's rows and cannot name an org whose rows were filtered out.
func orgsOfServices(services []Service) []string {
func orgsOfServices(services []Pipeline) []string {
seen := map[string]bool{}
for _, s := range services {
if s.Org != "" {
+44 -44
View File
@@ -20,12 +20,12 @@ import (
// to be one of the values read — comparing the other three to each other cannot
// see this at all.
func TestAgreeingVersionsAreNotHealth(t *testing.T) {
v := Version{Tag: "v1.801.548", Digest: "sha256:8830c2b1"}
s := Service{
v := Artifact{Tag: "v1.801.548", Digest: "sha256:8830c2b1"}
s := Pipeline{
Name: "cloud", Image: "ghcr.io/hanzoai/cloud", Repo: "hanzo-inc/cloud",
Built: v, Declared: v, Running: v,
Behind: 5, Since: time.Now().Add(-7 * time.Hour),
Head: Head{SHA: "727d2934", Build: Build{State: "failure", Job: "gate"}},
Tip: Tip{SHA: "727d2934", Check: Check{State: "failure", Job: "gate"}},
}
s.assess()
@@ -49,7 +49,7 @@ func TestAgreeingVersionsAreNotHealth(t *testing.T) {
// `failure` for both. Only the first means nothing shipped, so the artifact — not
// the run's conclusion — is what decides whether a commit was built.
func TestShippedRunStillCountsAsBuilt(t *testing.T) {
red := Run{ID: 1, Status: "completed", Conclusion: "failure"}
red := Execution{ID: 1, Status: "completed", Conclusion: "failure"}
shipped := verdict(red, []job{
{Name: "gate", Conclusion: "success"},
@@ -82,12 +82,12 @@ func TestShippedRunStillCountsAsBuilt(t *testing.T) {
// look for a log that does not exist is its own delay.
func TestAbsentIsNotFailure(t *testing.T) {
old := time.Now().Add(-time.Hour)
if got := verdict(Run{}, nil, old); got.State != "absent" {
if got := verdict(Execution{}, nil, old); got.State != "absent" {
t.Errorf("no run for an hour-old commit = %q; want absent", got.State)
}
// A commit that has only just landed has no run yet because Hanzo Git is
// still constructing one. Calling that absent would alarm on every push.
if got := verdict(Run{}, nil, time.Now()); got.State != "running" {
if got := verdict(Execution{}, nil, time.Now()); got.State != "running" {
t.Errorf("no run for a commit pushed seconds ago = %q; want running", got.State)
}
}
@@ -113,7 +113,7 @@ func steps(pairs ...string) []struct {
// TestPassingWithoutTestsIsNotGreen covers a run that reports success while the
// step that would have proved anything did not execute.
func TestPassingWithoutTestsIsNotGreen(t *testing.T) {
green := Run{ID: 1, Status: "completed", Conclusion: "success"}
green := Execution{ID: 1, Status: "completed", Conclusion: "success"}
skipped := verdict(green, []job{
{Name: "cicd", Conclusion: "success", Steps: steps(testStep, "skipped")},
{Name: "image", Conclusion: "success"},
@@ -129,7 +129,7 @@ func TestPassingWithoutTestsIsNotGreen(t *testing.T) {
t.Error("a passing run that never executed its tests reads as green")
}
s := Service{Repo: "hanzoai/ci", Head: Head{Build: skipped}}
s := Pipeline{Repo: "hanzoai/ci", Tip: Tip{Check: skipped}}
s.assess()
if !hasDrift(s, untested) {
t.Errorf("drift = %v; want %s", s.Drift, untested)
@@ -140,7 +140,7 @@ func TestPassingWithoutTestsIsNotGreen(t *testing.T) {
// commit was proved elsewhere. It is a claim about another run, so this one still
// did not test, and the page says so rather than drawing it green.
func TestOptingOutOfTestsIsRecorded(t *testing.T) {
green := Run{ID: 1, Status: "completed", Conclusion: "success"}
green := Execution{ID: 1, Status: "completed", Conclusion: "success"}
out := verdict(green, []job{{Name: "cicd", Conclusion: "success",
Steps: steps(testOptOut+" (declared by caller)", "success")},
{Name: "image", Conclusion: "success"}}, time.Now())
@@ -158,7 +158,7 @@ func TestOptingOutOfTestsIsRecorded(t *testing.T) {
// that knew only one shape would report every repo built the other way as never
// having produced anything.
func TestArtifactIsReadFromEitherShape(t *testing.T) {
green := Run{ID: 1, Status: "completed", Conclusion: "success"}
green := Execution{ID: 1, Status: "completed", Conclusion: "success"}
byJob := verdict(green, []job{{Name: "image", Conclusion: "success"}}, time.Now())
if !byJob.passed() {
@@ -185,11 +185,11 @@ func TestArtifactIsReadFromEitherShape(t *testing.T) {
// this fleet is most of them.
func TestSilenceAboutAnArtifactIsNotFailure(t *testing.T) {
quiet := []job{{Name: "cicd", Conclusion: "success", Steps: steps("Lint", "success")}}
if got := verdict(Run{ID: 1, Status: "completed", Conclusion: "success"}, quiet, time.Now()); !got.passed() {
if got := verdict(Execution{ID: 1, Status: "completed", Conclusion: "success"}, quiet, time.Now()); !got.passed() {
t.Errorf("green run with no build step = %q; want success", got.State)
}
red := []job{{Name: "cicd", Conclusion: "failure", Steps: steps("Lint", "failure")}}
if got := verdict(Run{ID: 1, Status: "completed", Conclusion: "failure"}, red, time.Now()); got.passed() {
if got := verdict(Execution{ID: 1, Status: "completed", Conclusion: "failure"}, red, time.Now()); got.passed() {
t.Error("a failed run with no build step reads as built")
}
}
@@ -200,14 +200,14 @@ func TestSilenceAboutAnArtifactIsNotFailure(t *testing.T) {
func TestUnreadableValueIsNotDrift(t *testing.T) {
cases := []struct {
name string
declared, running Version
declared, running Artifact
}{
{"nothing read at all", Version{}, Version{}},
{"cluster unread", Version{Tag: "v1.2.3", Digest: "sha256:aa"}, Version{}},
{"universe unread", Version{}, Version{Tag: "v1.2.3", Digest: "sha256:aa"}},
{"nothing read at all", Artifact{}, Artifact{}},
{"cluster unread", Artifact{Tag: "v1.2.3", Digest: "sha256:aa"}, Artifact{}},
{"universe unread", Artifact{}, Artifact{Tag: "v1.2.3", Digest: "sha256:aa"}},
}
for _, tc := range cases {
s := Service{Declared: tc.declared, Running: tc.running}
s := Pipeline{Declared: tc.declared, Running: tc.running}
s.assess()
if hasDrift(s, unsynced) {
t.Errorf("%s: drift = %v; an unread value must not read as a difference", tc.name, s.Drift)
@@ -219,9 +219,9 @@ func TestUnreadableValueIsNotDrift(t *testing.T) {
// other bytes; a digest cannot, so when both sides publish one it settles the
// question and a matching tag does not paper over different bytes.
func TestDigestDecidesOverTag(t *testing.T) {
s := Service{
Declared: Version{Tag: "v1.0.0", Digest: "sha256:aaaa"},
Running: Version{Tag: "v1.0.0", Digest: "sha256:bbbb"},
s := Pipeline{
Declared: Artifact{Tag: "v1.0.0", Digest: "sha256:aaaa"},
Running: Artifact{Tag: "v1.0.0", Digest: "sha256:bbbb"},
}
s.assess()
if !hasDrift(s, unsynced) {
@@ -241,7 +241,7 @@ func TestDigestDecidesOverTag(t *testing.T) {
// The other direction says nothing: a pin naming a release no known build
// produced is a build older than the window this reads, not a missing deploy.
func TestUnshippedIsDirectional(t *testing.T) {
ahead := Service{Built: Version{Tag: "v1.801.549"}, Declared: Version{Tag: "v1.801.548"}}
ahead := Pipeline{Built: Artifact{Tag: "v1.801.549"}, Declared: Artifact{Tag: "v1.801.548"}}
ahead.assess()
if !hasDrift(ahead, unshipped) {
t.Errorf("drift = %v; a proved image the pin never named is exactly %s", ahead.Drift, unshipped)
@@ -250,7 +250,7 @@ func TestUnshippedIsDirectional(t *testing.T) {
t.Errorf("tone = %q; an unshipped build is a caution, not an outage", tone(ahead))
}
behind := Service{Built: Version{Tag: "v1.801.357"}, Declared: Version{Tag: "v1.801.548"}}
behind := Pipeline{Built: Artifact{Tag: "v1.801.357"}, Declared: Artifact{Tag: "v1.801.548"}}
behind.assess()
if hasDrift(behind, unshipped) {
t.Errorf("drift = %v; a pin ahead of the newest build we can see claims nothing", behind.Drift)
@@ -267,7 +267,7 @@ func TestUnorderableTagsMakeNoClaim(t *testing.T) {
{"latest", "v1.2.3"},
{"", "v1.2.3"},
} {
s := Service{Built: Version{Tag: tc.built}, Declared: Version{Tag: tc.declared}}
s := Pipeline{Built: Artifact{Tag: tc.built}, Declared: Artifact{Tag: tc.declared}}
s.assess()
if hasDrift(s, unshipped) {
t.Errorf("built=%q declared=%q: drift = %v; neither orders, so nothing may be claimed",
@@ -311,14 +311,14 @@ func TestOrderOfVersions(t *testing.T) {
func TestOneImageIsSeveralServices(t *testing.T) {
static := "ghcr.io/hanzoai/static"
pins := []pin{
{Name: "cdn", Namespace: "hanzo", Image: static, Version: Version{Tag: "v0.3.0"}},
{Name: "tabs", Namespace: "hanzo", Image: static, Version: Version{Tag: "v0.5.9"}},
{Name: "zen-landing", Namespace: "zen", Image: static, Version: Version{Tag: "0.4.1"}},
{Name: "cdn", Namespace: "hanzo", Image: static, Artifact: Artifact{Tag: "v0.3.0"}},
{Name: "tabs", Namespace: "hanzo", Image: static, Artifact: Artifact{Tag: "v0.5.9"}},
{Name: "zen-landing", Namespace: "zen", Image: static, Artifact: Artifact{Tag: "0.4.1"}},
}
lives := []live{
{Name: "cdn", Namespace: "hanzo", Image: static, Version: Version{Tag: "v0.3.0"}, Ready: 1, Want: 1},
{Name: "tabs", Namespace: "hanzo", Image: static, Version: Version{Tag: "v0.5.9"}, Ready: 1, Want: 1},
{Name: "zen-landing", Namespace: "zen", Image: static, Version: Version{Tag: "0.4.1"}, Ready: 1, Want: 1},
{Name: "cdn", Namespace: "hanzo", Image: static, Artifact: Artifact{Tag: "v0.3.0"}, Ready: 1, Want: 1},
{Name: "tabs", Namespace: "hanzo", Image: static, Artifact: Artifact{Tag: "v0.5.9"}, Ready: 1, Want: 1},
{Name: "zen-landing", Namespace: "zen", Image: static, Artifact: Artifact{Tag: "0.4.1"}, Ready: 1, Want: 1},
}
// One repo builds the image all three run, so all three share its chain.
links := map[string]link{static: {Repo: "hanzoai/static", Org: "hanzoai"}}
@@ -346,11 +346,11 @@ func TestOneImageIsSeveralServices(t *testing.T) {
func TestSidecarOnTheSameImageDoesNotHideThePin(t *testing.T) {
studio := "ghcr.io/hanzoai/studio"
pins := []pin{{Name: "studio", Namespace: "hanzo", Image: studio,
Version: Version{Tag: "v0.19.31"}}}
Artifact: Artifact{Tag: "v0.19.31"}}}
// Sidecar last, so a last-writer-wins read takes the pinned-back one.
lives := []live{
{Name: "studio", Namespace: "hanzo", Image: studio, Version: Version{Tag: "v0.19.31"}, Ready: 1, Want: 1},
{Name: "studio", Namespace: "hanzo", Image: studio, Version: Version{Tag: "v0.19.26"}, Ready: 1, Want: 1},
{Name: "studio", Namespace: "hanzo", Image: studio, Artifact: Artifact{Tag: "v0.19.31"}, Ready: 1, Want: 1},
{Name: "studio", Namespace: "hanzo", Image: studio, Artifact: Artifact{Tag: "v0.19.26"}, Ready: 1, Want: 1},
}
got := assemble(pins, lives, map[string]link{})
@@ -371,11 +371,11 @@ func TestSidecarOnTheSameImageDoesNotHideThePin(t *testing.T) {
// service.
func TestRunningMatchesOnNameAndImage(t *testing.T) {
pins := []pin{{Name: "cloud", Namespace: "hanzo", Image: "ghcr.io/hanzoai/cloud",
Version: Version{Tag: "v1.801.548", Digest: "sha256:8830"}}}
Artifact: Artifact{Tag: "v1.801.548", Digest: "sha256:8830"}}}
lives := []live{
{Name: "cloud", Namespace: "hanzo", Image: "docker.io/library/redis", Version: Version{Tag: "7"}},
{Name: "cloud", Namespace: "hanzo", Image: "docker.io/library/redis", Artifact: Artifact{Tag: "7"}},
{Name: "cloud", Namespace: "hanzo", Image: "ghcr.io/hanzoai/cloud",
Version: Version{Tag: "v1.801.548", Digest: "sha256:8830"}, Ready: 1, Want: 1},
Artifact: Artifact{Tag: "v1.801.548", Digest: "sha256:8830"}, Ready: 1, Want: 1},
}
got := assemble(pins, lives, map[string]link{})
if len(got) != 1 {
@@ -394,9 +394,9 @@ func TestRunningMatchesOnNameAndImage(t *testing.T) {
func TestDriftingSortsFirstAndOldestOnTop(t *testing.T) {
old, recent := time.Now().Add(-9*time.Hour), time.Now().Add(-20*time.Minute)
pins := []pin{
{Name: "calm", Namespace: "hanzo", Image: "a", Version: Version{Tag: "v1"}},
{Name: "recent", Namespace: "hanzo", Image: "b", Version: Version{Tag: "v1"}},
{Name: "old", Namespace: "hanzo", Image: "c", Version: Version{Tag: "v1"}},
{Name: "calm", Namespace: "hanzo", Image: "a", Artifact: Artifact{Tag: "v1"}},
{Name: "recent", Namespace: "hanzo", Image: "b", Artifact: Artifact{Tag: "v1"}},
{Name: "old", Namespace: "hanzo", Image: "c", Artifact: Artifact{Tag: "v1"}},
}
links := map[string]link{
"a": {Repo: "o/calm"},
@@ -418,7 +418,7 @@ func TestDriftingSortsFirstAndOldestOnTop(t *testing.T) {
// leaves the version unnamed; reading that as "nothing has built" would report
// every such repo as behind by its whole history.
func TestUntaggedBuildIsStillABuild(t *testing.T) {
s := Service{Repo: "hanzoai/ci", Behind: 0, Built: Version{}, Declared: Version{Tag: "v0.2.0"}}
s := Pipeline{Repo: "hanzoai/ci", Behind: 0, Built: Artifact{}, Declared: Artifact{Tag: "v0.2.0"}}
s.assess()
if hasDrift(s, unbuilt) {
t.Errorf("drift = %v; head built, it merely carries no tag", s.Drift)
@@ -437,22 +437,22 @@ func TestUntaggedBuildIsStillABuild(t *testing.T) {
// artifact, so this is enforced where that count is made rather than by a clock
// here.
func TestBuildInFlightIsNotDrift(t *testing.T) {
inFlight := Service{Repo: "hanzoai/ui", Behind: 0,
Head: Head{Build: Build{State: "running"}}}
inFlight := Pipeline{Repo: "hanzoai/ui", Behind: 0,
Tip: Tip{Check: Check{State: "running"}}}
inFlight.assess()
if !inFlight.current() {
t.Errorf("drift = %v; a build still running is not a service that failed to ship", inFlight.Drift)
}
stopped := Service{Repo: "hanzoai/pay", Behind: 1, Since: time.Now().Add(-2 * time.Hour),
Head: Head{Build: Build{State: "failure", Job: "cicd"}}}
stopped := Pipeline{Repo: "hanzoai/pay", Behind: 1, Since: time.Now().Add(-2 * time.Hour),
Tip: Tip{Check: Check{State: "failure", Job: "cicd"}}}
stopped.assess()
if !hasDrift(stopped, unbuilt) {
t.Errorf("drift = %v; a build that stopped without an artifact is %s", stopped.Drift, unbuilt)
}
}
func hasDrift(s Service, d string) bool {
func hasDrift(s Pipeline, d string) bool {
for _, x := range s.Drift {
if x == d {
return true
+13 -13
View File
@@ -100,7 +100,7 @@ func (g *gitSource) pins(ctx context.Context, repo string) (map[string]pin, erro
Name: strings.TrimSuffix(f.Name, ".yaml"),
Namespace: space.Name,
Image: image,
Version: v,
Artifact: v,
At: at,
}
mu.Unlock()
@@ -225,7 +225,7 @@ func (g *gitSource) locate(ctx context.Context, p pin) (repo, branch string, err
q := url.Values{"q": {name}, "limit": {"30"}}
if err := g.getJSON(ctx, "/v1/repos/search?"+q.Encode(), &body); err == nil {
for _, r := range body.Data {
if _, n := splitFullName(r.FullName); n == name && g.holds(ctx, r.FullName, p.Version.Tag) {
if _, n := splitFullName(r.FullName); n == name && g.holds(ctx, r.FullName, p.Artifact.Tag) {
return g.remember(p.Image, r.FullName, r.Branch)
}
}
@@ -276,13 +276,13 @@ func (g *gitSource) link(ctx context.Context, repo, branch string) (link, error)
if err != nil || len(commits) == 0 {
return c, err
}
c.Head = Head{SHA: commits[0].SHA, Title: commits[0].Title, At: commits[0].At}
c.Tip = Tip{SHA: commits[0].SHA, Title: commits[0].Title, At: commits[0].At}
runs, err := g.runs(ctx, repo, 20)
if err != nil {
return c, err
}
byCommit := map[string]Run{}
byCommit := map[string]Execution{}
for _, r := range runs {
// The first run listed for a commit is its newest attempt.
if _, seen := byCommit[r.SHA]; !seen {
@@ -305,7 +305,7 @@ func (g *gitSource) link(ctx context.Context, repo, branch string) (link, error)
for i := 0; i < window; i++ {
b := g.built(ctx, repo, byCommit[shortSHA(commits[i].SHA)], commits[i].At)
if i == 0 {
c.Head.Build = b
c.Tip.Check = b
}
if b.passed() {
built = i
@@ -328,7 +328,7 @@ func (g *gitSource) link(ctx context.Context, repo, branch string) (link, error)
// which the page shows as unknown — while the commit still counts as built,
// so an untagged build must not read as nothing having been built at all.
if built >= 0 {
c.Built = Version{Tag: tags[commits[built].SHA]}
c.Built = Artifact{Tag: tags[commits[built].SHA]}
}
return c, nil
}
@@ -396,15 +396,15 @@ func (g *gitSource) tags(ctx context.Context, repo string, limit int) (map[strin
return m, nil
}
func (g *gitSource) built(ctx context.Context, repo string, r Run, at time.Time) Build {
func (g *gitSource) built(ctx context.Context, repo string, r Execution, at time.Time) Check {
if r.ID == 0 {
return verdict(Run{}, nil, at)
return verdict(Execution{}, nil, at)
}
jobs, err := g.jobs(ctx, repo, r.ID)
if err != nil {
// The run is known even when its jobs are not; report what the run says
// rather than nothing.
return Build{Number: r.Number, URL: r.URL, At: r.StartedAt, State: outcome(r)}
return Check{Number: r.Number, URL: r.URL, At: r.StartedAt, State: outcome(r)}
}
return verdict(r, jobs, at)
}
@@ -419,18 +419,18 @@ func (g *gitSource) built(ctx context.Context, repo string, r Run, at time.Time)
//
// It is separated from the fetch because the decision is the part worth pinning:
// which job failed, and whether an artifact exists, is what the page turns on.
func verdict(r Run, jobs []job, at time.Time) Build {
func verdict(r Execution, jobs []job, at time.Time) Check {
if r.ID == 0 {
// No run for this commit. A push that has only just landed is Hanzo Git
// still constructing one; older than that, the run is genuinely absent —
// a workflow that could not be parsed or a reference that would not
// resolve, which leaves no failed run to open.
if time.Since(at) < settling {
return Build{State: "running"}
return Check{State: "running"}
}
return Build{State: "absent"}
return Check{State: "absent"}
}
b := Build{Number: r.Number, URL: r.URL, At: r.StartedAt, State: outcome(r)}
b := Check{Number: r.Number, URL: r.URL, At: r.StartedAt, State: outcome(r)}
// A repo publishes its image one of two ways, and both are read: a pipeline
// with a job of its own for the artifact, or the reusable's lane, which
+20 -20
View File
@@ -129,8 +129,8 @@ func Main() {
//
// Both carry the SAME four facts about the fetch itself, because a board whose
// staleness a reader cannot see is a board that lies quietly.
type Runs struct {
Runs []Run `json:"runs"`
type Executions struct {
Executions []Execution `json:"runs"`
Repos int `json:"repos"`
Orgs []string `json:"orgs"`
FetchedAt time.Time `json:"fetchedAt"`
@@ -139,8 +139,8 @@ type Runs struct {
}
// Fleet is one row per service: what was written against what is running.
type Fleet struct {
Services []Service `json:"services"`
type Pipelines struct {
Services []Pipeline `json:"services"`
Orgs []string `json:"orgs"`
FetchedAt time.Time `json:"fetchedAt"`
Stale bool `json:"stale"`
@@ -167,10 +167,10 @@ func routes(cfg config, cache *runCache, board *fleetCache) *http.ServeMux {
return
}
snap := cache.get()
writeJSON(w, http.StatusOK, Runs{
Runs: v.visible(snap.Runs, r.URL.Query().Get("org")),
writeJSON(w, http.StatusOK, Executions{
Executions: v.visible(snap.Executions, r.URL.Query().Get("org")),
Repos: snap.Repos,
Orgs: v.orgs(snap.Runs),
Orgs: v.orgs(snap.Executions),
FetchedAt: snap.FetchedAt,
Stale: snap.stale(cfg.staleAfter),
SourceErr: snap.errString(),
@@ -183,7 +183,7 @@ func routes(cfg config, cache *runCache, board *fleetCache) *http.ServeMux {
}
snap := board.get()
services := v.services(snap.Services, r.URL.Query().Get("org"))
writeJSON(w, http.StatusOK, Fleet{
writeJSON(w, http.StatusOK, Pipelines{
Services: services,
Orgs: orgsOfServices(services),
FetchedAt: snap.FetchedAt,
@@ -278,7 +278,7 @@ func envInt(k string, def int) int {
// Run is the projection of a Hanzo Git workflow run this dashboard shows. It is
// deliberately a SUBSET: the upstream object carries a dozen more fields, and
// copying them all would make this a second schema to maintain against theirs.
type Run struct {
type Execution struct {
ID int64 `json:"id"`
Org string `json:"org"`
Repo string `json:"repo"`
@@ -307,7 +307,7 @@ type Run struct {
// Duration is zero-valued rather than negative when a run has not finished —
// callers render "running", and a negative duration would print as one.
func (r Run) Duration() time.Duration {
func (r Execution) Duration() time.Duration {
if r.StartedAt.IsZero() || r.EndedAt.IsZero() || r.EndedAt.Before(r.StartedAt) {
return 0
}
@@ -315,7 +315,7 @@ func (r Run) Duration() time.Duration {
}
type snapshot struct {
Runs []Run `json:"runs"`
Executions []Execution `json:"runs"`
Repos int `json:"repos"`
FetchedAt time.Time `json:"fetchedAt"`
Err error `json:"-"`
@@ -350,7 +350,7 @@ func (c *runCache) get() snapshot {
func (c *runCache) put(s snapshot) {
c.mu.Lock()
defer c.mu.Unlock()
if s.Err != nil && len(s.Runs) == 0 && len(c.snap.Runs) > 0 {
if s.Err != nil && len(s.Executions) == 0 && len(c.snap.Executions) > 0 {
prev := c.snap
prev.Err = s.Err
c.snap = prev
@@ -459,7 +459,7 @@ type apiRun struct {
} `json:"actor"`
}
func (g *gitSource) runs(ctx context.Context, fullName string, limit int) ([]Run, error) {
func (g *gitSource) runs(ctx context.Context, fullName string, limit int) ([]Execution, error) {
var body struct {
WorkflowRuns []apiRun `json:"workflow_runs"`
}
@@ -468,9 +468,9 @@ func (g *gitSource) runs(ctx context.Context, fullName string, limit int) ([]Run
return nil, err
}
org, repo := splitFullName(fullName)
out := make([]Run, 0, len(body.WorkflowRuns))
out := make([]Execution, 0, len(body.WorkflowRuns))
for _, r := range body.WorkflowRuns {
out = append(out, Run{
out = append(out, Execution{
ID: r.ID,
Org: org,
Repo: repo,
@@ -510,7 +510,7 @@ func poll(ctx context.Context, logger *slog.Logger, src *gitSource, cache *runCa
const workers = 6
var (
mu sync.Mutex
all []Run
all []Execution
errs []string
wg sync.WaitGroup
)
@@ -544,7 +544,7 @@ func poll(ctx context.Context, logger *slog.Logger, src *gitSource, cache *runCa
wg.Wait()
sort.Slice(all, func(i, j int) bool { return all[i].StartedAt.After(all[j].StartedAt) })
cache.put(snapshot{Runs: all, Repos: len(names) - len(errs), FetchedAt: time.Now().UTC()})
cache.put(snapshot{Executions: all, Repos: len(names) - len(errs), FetchedAt: time.Now().UTC()})
logger.Info("refreshed", "repos", len(names), "withRuns", len(names)-len(errs), "runs", len(all))
}
@@ -601,11 +601,11 @@ func parseTime(s string) time.Time {
return t.UTC()
}
func filterByOrg(runs []Run, org string) []Run {
func filterByOrg(runs []Execution, org string) []Execution {
if org == "" {
return runs
}
out := make([]Run, 0, len(runs))
out := make([]Execution, 0, len(runs))
for _, r := range runs {
if r.Org == org {
out = append(out, r)
@@ -614,7 +614,7 @@ func filterByOrg(runs []Run, org string) []Run {
return out
}
func orgsOf(runs []Run) []string {
func orgsOf(runs []Execution) []string {
seen := map[string]bool{}
for _, r := range runs {
if r.Org != "" {
+9 -9
View File
@@ -26,13 +26,13 @@ import (
// asked to be careful with it, because a template that can see everything is one
// edit away from showing it.
func renderRuns(w http.ResponseWriter, snap snapshot, v viewer, org string, cfg config) {
runs := v.visible(snap.Runs, org)
runs := v.visible(snap.Executions, org)
if len(runs) > 200 {
runs = runs[:200]
}
data := struct {
Runs []Run
Executions []Execution
Orgs []string
Org string
Viewer string
@@ -45,8 +45,8 @@ func renderRuns(w http.ResponseWriter, snap snapshot, v viewer, org string, cfg
Source string
Counts map[string]int
}{
Runs: runs,
Orgs: v.orgs(snap.Runs),
Executions: runs,
Orgs: v.orgs(snap.Executions),
Org: org,
Viewer: v.org,
Sudo: v.sudo,
@@ -66,7 +66,7 @@ func renderRuns(w http.ResponseWriter, snap snapshot, v viewer, org string, cfg
}
// countByOutcome buckets runs for the summary strip.
func countByOutcome(runs []Run) map[string]int {
func countByOutcome(runs []Execution) map[string]int {
c := map[string]int{"success": 0, "failure": 0, "running": 0, "cancelled": 0}
for _, r := range runs {
c[outcome(r)]++
@@ -85,7 +85,7 @@ func countByOutcome(runs []Run) map[string]int {
// fleet cancellations are the single largest category (superseded pushes cancel
// the in-flight run), and a board that shows them as broken is a board nobody
// trusts, which is worse than no board.
func outcome(r Run) string {
func outcome(r Execution) string {
if !strings.EqualFold(r.Status, "completed") {
return "running" // queued | in_progress | waiting | blocked
}
@@ -135,7 +135,7 @@ func humanDur(d time.Duration) string {
// no toggle, so it states its scheme once and means it.
var tmpl = template.Must(template.New("ci").Funcs(template.FuncMap{
"outcome": outcome,
"dur": func(r Run) string { return humanDur(r.Duration()) },
"dur": func(r Execution) string { return humanDur(r.Duration()) },
"ago": humanAge,
"css": pageCSS,
}).Parse(`<!doctype html>
@@ -173,14 +173,14 @@ var tmpl = template.Must(template.New("ci").Funcs(template.FuncMap{
These rows are the last good read, not current state.
</div>{{end}}
{{if .Runs}}
{{if .Executions}}
<table>
<thead><tr>
<th>Repository</th><th>Workflow</th><th class="hide-sm">Commit</th>
<th class="hide-sm">Actor</th><th>Started</th><th>Took</th>
</tr></thead>
<tbody>
{{range .Runs}}
{{range .Executions}}
<tr>
<td><span class="dot {{outcome .}}"></span><a href="{{.URL}}"><span class="org">{{.Org}}/</span><span class="repo">{{.Repo}}</span></a></td>
<td>{{.Workflow}} <span class="mono">#{{.Number}}</span><div class="title">{{.Title}}</div></td>
+4 -4
View File
@@ -4,9 +4,9 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"sort"
"net/http/httptest"
"regexp"
"sort"
"strings"
"testing"
)
@@ -58,7 +58,7 @@ func TestDashboardCSSNamesNoColours(t *testing.T) {
// trusted to be careful with a snapshot it can see all of.
func TestRenderedPageShowsOnlyTheViewersOrg(t *testing.T) {
w := httptest.NewRecorder()
renderRuns(w, snapshot{Runs: testRuns(), Repos: 3}, viewer{org: "lux"}, "", config{})
renderRuns(w, snapshot{Executions: testRuns(), Repos: 3}, viewer{org: "lux"}, "", config{})
body := w.Body.String()
if !strings.Contains(body, ">lux/<") {
@@ -88,8 +88,8 @@ func TestRenderedPageShowsOnlyTheViewersOrg(t *testing.T) {
// trusted.
func TestTheOperationsKeepTheirWireShape(t *testing.T) {
for name, v := range map[string]any{
"runs": Runs{},
"fleet": Fleet{},
"runs": Executions{},
"fleet": Pipelines{},
} {
b, err := json.Marshal(v)
if err != nil {
+2 -2
View File
@@ -57,7 +57,7 @@ func resolveViewer(r *http.Request, adminOrg string) (viewer, bool) {
// only ever narrow the result. A lux viewer asking for `?org=hanzo` gets an
// empty list, not hanzo's builds — the parameter selects among what you may
// already see, it never reaches for more.
func (v viewer) visible(runs []Run, want string) []Run {
func (v viewer) visible(runs []Execution, want string) []Execution {
want = strings.TrimSpace(want)
if v.sudo {
// The fleet view: every org, narrowed by the requested one if given.
@@ -72,7 +72,7 @@ func (v viewer) visible(runs []Run, want string) []Run {
// orgs lists the org tabs this viewer may choose between. A tenant gets exactly
// its own org — rendering the full org list to a tenant would leak the set of
// orgs that build on the platform even though their runs are correctly hidden.
func (v viewer) orgs(runs []Run) []string {
func (v viewer) orgs(runs []Execution) []string {
if v.sudo {
return orgsOf(runs)
}
+12 -12
View File
@@ -15,8 +15,8 @@ import (
// The properties asserted here are the ones that made it a leak, not merely the
// ones that make the new code work.
func testRuns() []Run {
return []Run{
func testRuns() []Execution {
return []Execution{
{Org: "hanzo", Repo: "cloud", Workflow: "build", Status: "completed", Conclusion: "success"},
{Org: "lux", Repo: "node", Workflow: "build", Status: "completed", Conclusion: "failure"},
{Org: "zoo", Repo: "app", Workflow: "test", Status: "in_progress"},
@@ -127,7 +127,7 @@ func TestTenantOrgListIsNotTheFleetList(t *testing.T) {
// asserted.
func TestRunsEndpointScopesEndToEnd(t *testing.T) {
cache := &runCache{}
cache.put(snapshot{Runs: testRuns(), Repos: 3})
cache.put(snapshot{Executions: testRuns(), Repos: 3})
cfg := config{adminOrg: "admin"}
h := func(w http.ResponseWriter, r *http.Request) {
@@ -137,8 +137,8 @@ func TestRunsEndpointScopesEndToEnd(t *testing.T) {
}
snap := cache.get()
writeJSON(w, http.StatusOK, map[string]any{
"runs": v.visible(snap.Runs, r.URL.Query().Get("org")),
"orgs": v.orgs(snap.Runs),
"runs": v.visible(snap.Executions, r.URL.Query().Get("org")),
"orgs": v.orgs(snap.Executions),
})
}
@@ -160,14 +160,14 @@ func TestRunsEndpointScopesEndToEnd(t *testing.T) {
h(w, r)
var got struct {
Runs []Run `json:"runs"`
Executions []Execution `json:"runs"`
Orgs []string `json:"orgs"`
}
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("decode: %v", err)
}
if len(got.Runs) != 0 {
t.Errorf("lux asking ?org=hanzo got %+v; want none", got.Runs)
if len(got.Executions) != 0 {
t.Errorf("lux asking ?org=hanzo got %+v; want none", got.Executions)
}
if len(got.Orgs) != 1 || got.Orgs[0] != "lux" {
t.Errorf("orgs=%v; want [lux]", got.Orgs)
@@ -177,8 +177,8 @@ func TestRunsEndpointScopesEndToEnd(t *testing.T) {
// ─────────────────── the fleet surface, under the same rule ───────────────────
func testServices() []Service {
return []Service{
func testServices() []Pipeline {
return []Pipeline{
{Name: "cloud", Namespace: "hanzo", Image: "ghcr.io/hanzoai/cloud", Org: "hanzo", Repo: "hanzo-inc/cloud"},
{Name: "node", Namespace: "hanzo", Image: "ghcr.io/luxfi/node", Org: "lux", Repo: "luxfi/node"},
{Name: "app", Namespace: "hanzo", Image: "ghcr.io/zooai/app", Org: "zoo", Repo: "zooai/app"},
@@ -221,7 +221,7 @@ func TestFleetRefusesWithoutTheHeader(t *testing.T) {
func TestFleetTenantCannotWiden(t *testing.T) {
mux := routes(config{adminOrg: "admin"}, &runCache{}, testBoard(t))
ask := func(t *testing.T, org, want string) []Service {
ask := func(t *testing.T, org, want string) []Pipeline {
t.Helper()
r := httptest.NewRequest(http.MethodGet, "/v1/ci/fleet?org="+want, nil)
r.Header.Set(orgHeader, org)
@@ -231,7 +231,7 @@ func TestFleetTenantCannotWiden(t *testing.T) {
t.Fatalf("status=%d want 200", w.Code)
}
var got struct {
Services []Service `json:"services"`
Services []Pipeline `json:"services"`
Orgs []string `json:"orgs"`
}
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {