package versions import ( "context" "fmt" "sort" "strings" "time" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" ) // The registry seats a version is published to (design 51 *The seats*), by what is published. const ( SeatImages = "container-image-registry" SeatArtifacts = "oci-artifact-registry" SeatGoPackages = "go-package-registry" SeatNPM = "npm-package-registry" ) // SeatOf is the registry seat an artifact of kind in language is published to, and the kind the builder numbers it as; // ok is false for what is not numbered (an upstream copy is mirrored by its pinned digest, design 51 *Publishing*). func SeatOf(kind, language string) (seat, numberedAs string, ok bool) { switch kind { case KindImage: return SeatImages, KindImage, true case KindBundle, KindArchive: return SeatArtifacts, kind, true case KindPackage: switch language { case "go": return SeatGoPackages, KindPackage, true case "typescript", "javascript", "node": return SeatNPM, KindPackage, true } } return "", "", false } // NameOf is the name a module's image, bundle or archive is numbered under: `/`, the name the seat // answers it by (ADR 0605 §9's `//@…`). A package is numbered under its name in its own // ecosystem instead (Numberer.Names). func NameOf(module, artifact string) string { return module + "/" + artifact } // PackageNames answers a package's name in its own ecosystem (design 32: a Go package's module path, an npm package's // package.json name) for the module and artifact it is built from, as the indexer's packages state holds it; known is // false when the state names none for it. An error is a state that cannot be read. type PackageNames func(ctx context.Context, module, artifact string) (name string, known bool, err error) // TagPrefix is the git tag prefix of a package in the module's directory path: `go/v` for mesh-sdk's `go`, `v` at the // root (design 51: the builder tags each Go version `go/v0.1.`). func TagPrefix(path string) string { path = strings.Trim(path, "/") if path == "" || path == "." { return "v" } return path + "/v" } // ClaimArtifact is one artifact of a trunk build to number. type ClaimArtifact struct { Artifact, Kind, Language string } // BuildClaim is a trunk build at its claim: what Numberer.Number numbers. type BuildClaim struct { Module, Path, Repository, Fingerprint, Commit, Series string // Revision is the source revision of the merge (a package is numbered per revision); Merge its sequence, for order. Revision string Merge uint64 Artifacts []ClaimArtifact // Tag numbers a check's build of a head a tag ask names (design 51 *Tagged versions*): its packages only, each a // tagged version kept with Repository for its branch's end; Head is the head, said in numbered. Empty for a trunk build. Tag, Head string // Done are the digests of the build's done key, by artifact, when the claim is a look at a build already done: what it // left unnumbered (a package whose name was not known yet, a floor that was silent) is numbered with them, so // version.numbered carries the digest its build module publishes late. Nil for a build still to make. Done map[string]string } // Numbering is what the claim got. type Numbering struct { // Versions are each artifact's version, by artifact; an artifact built unnumbered (its floor not known) has none. Versions map[string]string // Unnumbered are the artifacts built and published by digest, numbered once their floor answers (ADR 0617 §1). Unnumbered []string // Wait is why the build waits: a package whose floor is not known (design 51 *The floor*). Nothing is claimed. Wait string // Said are the builder's narrations of each decision, for its process log and its build log (build.said). Said []Narration // Names are the names each numbered artifact was numbered under, by artifact (NameOf, or a package's name in its // ecosystem). Names map[string]string } // NameOf is the name artifact of module was numbered under in this numbering. func (g Numbering) NameOf(module, artifact string) string { if name := g.Names[artifact]; name != "" { return name } return NameOf(module, artifact) } // Narration is one line the builder says in its build log (design 51: the build log says a version was kept, and // why), and what it narrates. type Narration struct{ Line, Why string } // What a narration of the build log narrates (the build.said entry's why). const ( SaidNumbered = "numbered" SaidKept = "kept" SaidWaiting = "waiting" SaidUnnumbered = "unnumbered" SaidUnanswered = "tag ask unanswered" SaidOldFormClosed = "old-form-closed" ) // narrationOf is a decision's narration. func narrationOf(name string, d Decision) Narration { why := SaidNumbered switch { case d.Keep: why = SaidKept case d.Wait: why = SaidWaiting case d.Unnumbered: why = SaidUnnumbered } return Narration{Line: name + ": " + d.Why, Why: why} } // Numberer numbers trunk builds at their claim (design 51 *The versions*, DLV-B2, B3): it asks a name's floor only // when versions holds nothing for it, records a silent floor as a wait (FloorWaits, woken by its source's event), and // says each new number (Said, the event version.numbered). type Numberer struct { Versions Versions Floor FloorSources Waits *FloorWaits // Holder is the module holding a seat, whose health condition's clear wakes a silent seat source; nil names none. Holder func(seat string) string // Said is told each version newly numbered; nil tells nobody. Said func(builds.VersionNumbered) // Names answers a package's name in its ecosystem; nil numbers a package as NameOf names it. Names PackageNames // Publishing is told each version Built recorded that is still numbered, its holder's word not heard yet: the // builder arms its publish bound (publish.go); nil arms none. Publishing func(ctx context.Context, e Entry) Now func() time.Time } // Number numbers every numbered artifact of c. An error is a store that cannot be read or written: the claim is then // built as before, unnumbered, and said. func (n *Numberer) Number(ctx context.Context, c BuildClaim) (Numbering, error) { out := Numbering{Versions: map[string]string{}, Names: map[string]string{}} arts := append([]ClaimArtifact(nil), c.Artifacts...) sort.Slice(arts, func(i, j int) bool { return arts[i].Artifact < arts[j].Artifact }) for _, a := range arts { seat, kind, ok := SeatOf(a.Kind, a.Language) if !ok || (c.Tag != "" && kind != KindPackage) { continue } name := NameOf(c.Module, a.Artifact) if kind == KindPackage && n.Names != nil { // A package is numbered, asked for its floor and published under its name in its ecosystem (design 32), the // name the holder's builder_versions question and the seat's versions name it by. named, known, err := n.Names(ctx, c.Module, a.Artifact) if err != nil || !known { why := "the indexer's packages state names no name of it in its ecosystem yet (it writes one at the module's next trunk merge)" if err != nil { why = "the indexer's packages state cannot be read: " + err.Error() } out.Unnumbered = append(out.Unnumbered, a.Artifact) out.Said = append(out.Said, Narration{Line: name + ": built unnumbered: " + why, Why: SaidUnnumbered}) continue } name = named } out.Names[a.Artifact] = name entries, err := n.Versions.Of(ctx, name) if err != nil { return out, err } var floor Floor var why map[string]string if len(entries) == 0 { floor, why = n.askFloor(ctx, seat, name, c) } d, e, err := n.Versions.Number(ctx, Claim{Name: name, Kind: kind, Series: c.Series, Module: c.Module, Fingerprint: c.Fingerprint, Commit: c.Commit, Revision: c.Revision, Digest: c.Done[a.Artifact], Seat: seat, Merge: c.Merge, Floor: floor, Tag: c.Tag, Repository: c.Repository}) if err != nil { return out, err } out.Said = append(out.Said, narrationOf(name, d)) switch { case d.Wait: n.wait(name, seat, floor, why) out.Wait = name + ": " + d.Why return out, nil case d.Unnumbered: n.wait(name, seat, floor, why) out.Unnumbered = append(out.Unnumbered, a.Artifact) continue } if n.Waits != nil { n.Waits.Answered(name) } out.Versions[a.Artifact] = d.Version if !d.Keep && n.Said != nil { n.Said(NumberedOf(e, c.Head)) } } return out, nil } func (n *Numberer) askFloor(ctx context.Context, seat, name string, c BuildClaim) (Floor, map[string]string) { return n.Floor.Ask(ctx, FloorOf{Seat: seat, Name: name, Repository: c.Repository, Prefix: TagPrefix(c.Path)}) } func (n *Numberer) wait(name, seat string, f Floor, why map[string]string) { if n.Waits == nil { return } holder := "" if n.Holder != nil { holder = n.Holder(seat) } now := time.Now() if n.Now != nil { now = n.Now() } n.Waits.Wait(name, seat, holder, f, why, now) } // Built records the digest each numbered artifact was built with, on an entry that holds none yet or was never published: what the holder's // builder_versions question compares (ADR 0605 §4). func (n *Numberer) Built(ctx context.Context, c BuildClaim, got Numbering, digests map[string]string) error { var errs []string for artifact, version := range got.Versions { d := digests[artifact] if d == "" { continue } e, err := n.Versions.Mark(ctx, got.NameOf(c.Module, artifact), version, func(e *Entry) { // The new digest replaces the recorded one only while no published was said for the number (design 51 // *Tools of the builder*): a rebuild of a gone file under the same number. if e.Digest == "" || e.State == Numbered || e.State == Refused { e.Digest = d } }) if err != nil { errs = append(errs, err.Error()) continue } if e.State == Numbered && n.Publishing != nil { n.Publishing(ctx, e) } } if len(errs) > 0 { sort.Strings(errs) return fmt.Errorf("the built digests could not be recorded: %s", strings.Join(errs, "; ")) } return nil }