The builder numbers, asks the floor of, records and tags a package under the name the indexer's series key names for its <module>/<artifact> (a Go module path, an npm package name; design 32), so builder_versions answers the holder's question by the name a registry seat publishes it under. A package the state names nothing for, or a state not readable, is built unnumbered and said; the rest of the claim is numbered. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
250 lines
9.1 KiB
Go
250 lines
9.1 KiB
Go
package versions
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// 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: `<module>/<artifact>`, the name the seat
|
|
// answers it by (ADR 0605 §9's `<registry>/<module>/<artifact>@…`). 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.<n>`).
|
|
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
|
|
}
|
|
|
|
// 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(NumberedBody)
|
|
// Names answers a package's name in its ecosystem; nil numbers a package as NameOf names it.
|
|
Names PackageNames
|
|
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, 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
|
|
}
|
|
if _, 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
|
|
}
|
|
}); err != nil {
|
|
errs = append(errs, err.Error())
|
|
}
|
|
}
|
|
if len(errs) > 0 {
|
|
sort.Strings(errs)
|
|
return fmt.Errorf("the built digests could not be recorded: %s", strings.Join(errs, "; "))
|
|
}
|
|
return nil
|
|
}
|