mirror of
https://github.com/hanzoai/ci.git
synced 2026-10-20 15:28:05 +00:00
name the domain for a namespace it now shares
Hanzo CI/CD / cicd (push) Waiting to run
CI/CD / gate (push) Waiting to run
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:
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,9 +330,9 @@ func imageName(image string) string {
|
||||
// ───────────────────────────── snapshot ─────────────────────────────
|
||||
|
||||
type fleet struct {
|
||||
Services []Service `json:"services"`
|
||||
FetchedAt time.Time `json:"fetchedAt"`
|
||||
Err error `json:"-"`
|
||||
Services []Pipeline `json:"services"`
|
||||
FetchedAt time.Time `json:"fetchedAt"`
|
||||
Err error `json:"-"`
|
||||
}
|
||||
|
||||
func (f fleet) stale(after time.Duration) bool {
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -129,22 +129,22 @@ 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"`
|
||||
Repos int `json:"repos"`
|
||||
Orgs []string `json:"orgs"`
|
||||
FetchedAt time.Time `json:"fetchedAt"`
|
||||
Stale bool `json:"stale"`
|
||||
SourceErr string `json:"sourceErr"`
|
||||
type Executions struct {
|
||||
Executions []Execution `json:"runs"`
|
||||
Repos int `json:"repos"`
|
||||
Orgs []string `json:"orgs"`
|
||||
FetchedAt time.Time `json:"fetchedAt"`
|
||||
Stale bool `json:"stale"`
|
||||
SourceErr string `json:"sourceErr"`
|
||||
}
|
||||
|
||||
// Fleet is one row per service: what was written against what is running.
|
||||
type Fleet struct {
|
||||
Services []Service `json:"services"`
|
||||
Orgs []string `json:"orgs"`
|
||||
FetchedAt time.Time `json:"fetchedAt"`
|
||||
Stale bool `json:"stale"`
|
||||
SourceErr string `json:"sourceErr"`
|
||||
type Pipelines struct {
|
||||
Services []Pipeline `json:"services"`
|
||||
Orgs []string `json:"orgs"`
|
||||
FetchedAt time.Time `json:"fetchedAt"`
|
||||
Stale bool `json:"stale"`
|
||||
SourceErr string `json:"sourceErr"`
|
||||
}
|
||||
|
||||
func routes(cfg config, cache *runCache, board *fleetCache) *http.ServeMux {
|
||||
@@ -167,13 +167,13 @@ 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")),
|
||||
Repos: snap.Repos,
|
||||
Orgs: v.orgs(snap.Runs),
|
||||
FetchedAt: snap.FetchedAt,
|
||||
Stale: snap.stale(cfg.staleAfter),
|
||||
SourceErr: snap.errString(),
|
||||
writeJSON(w, http.StatusOK, Executions{
|
||||
Executions: v.visible(snap.Executions, r.URL.Query().Get("org")),
|
||||
Repos: snap.Repos,
|
||||
Orgs: v.orgs(snap.Executions),
|
||||
FetchedAt: snap.FetchedAt,
|
||||
Stale: snap.stale(cfg.staleAfter),
|
||||
SourceErr: snap.errString(),
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/v1/ci/fleet", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -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,10 +315,10 @@ func (r Run) Duration() time.Duration {
|
||||
}
|
||||
|
||||
type snapshot struct {
|
||||
Runs []Run `json:"runs"`
|
||||
Repos int `json:"repos"`
|
||||
FetchedAt time.Time `json:"fetchedAt"`
|
||||
Err error `json:"-"`
|
||||
Executions []Execution `json:"runs"`
|
||||
Repos int `json:"repos"`
|
||||
FetchedAt time.Time `json:"fetchedAt"`
|
||||
Err error `json:"-"`
|
||||
}
|
||||
|
||||
func (s snapshot) stale(after time.Duration) bool {
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -26,37 +26,37 @@ 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
|
||||
Orgs []string
|
||||
Org string
|
||||
Viewer string
|
||||
Sudo bool
|
||||
Repos int
|
||||
FetchedAt time.Time
|
||||
Age string
|
||||
Stale bool
|
||||
SourceErr string
|
||||
Source string
|
||||
Counts map[string]int
|
||||
Executions []Execution
|
||||
Orgs []string
|
||||
Org string
|
||||
Viewer string
|
||||
Sudo bool
|
||||
Repos int
|
||||
FetchedAt time.Time
|
||||
Age string
|
||||
Stale bool
|
||||
SourceErr string
|
||||
Source string
|
||||
Counts map[string]int
|
||||
}{
|
||||
Runs: runs,
|
||||
Orgs: v.orgs(snap.Runs),
|
||||
Org: org,
|
||||
Viewer: v.org,
|
||||
Sudo: v.sudo,
|
||||
Repos: snap.Repos,
|
||||
FetchedAt: snap.FetchedAt,
|
||||
Age: humanAge(snap.FetchedAt),
|
||||
Stale: snap.stale(cfg.staleAfter),
|
||||
SourceErr: snap.errString(),
|
||||
Source: cfg.gitBase,
|
||||
Counts: countByOutcome(runs),
|
||||
Executions: runs,
|
||||
Orgs: v.orgs(snap.Executions),
|
||||
Org: org,
|
||||
Viewer: v.org,
|
||||
Sudo: v.sudo,
|
||||
Repos: snap.Repos,
|
||||
FetchedAt: snap.FetchedAt,
|
||||
Age: humanAge(snap.FetchedAt),
|
||||
Stale: snap.stale(cfg.staleAfter),
|
||||
SourceErr: snap.errString(),
|
||||
Source: cfg.gitBase,
|
||||
Counts: countByOutcome(runs),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+14
-14
@@ -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"`
|
||||
Orgs []string `json:"orgs"`
|
||||
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,8 +231,8 @@ func TestFleetTenantCannotWiden(t *testing.T) {
|
||||
t.Fatalf("status=%d want 200", w.Code)
|
||||
}
|
||||
var got struct {
|
||||
Services []Service `json:"services"`
|
||||
Orgs []string `json:"orgs"`
|
||||
Services []Pipeline `json:"services"`
|
||||
Orgs []string `json:"orgs"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user