The builder reads tag-asks for the candidate's repository and head (versions.TagFor), numbers only its packages under the tag below the next version, keeps each with its repository for the branch's end, and says numbered with the head; the same fingerprint claimed again keeps its number (design 51 *Tagged versions*). Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
187 lines
6.4 KiB
Go
187 lines
6.4 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 artifact is numbered under: `<module>/<artifact>`, the name the seat answers it by
|
|
// (ADR 0605 §9's `<registry>/<module>/<artifact>@…`).
|
|
func NameOf(module, artifact string) string { return module + "/" + artifact }
|
|
|
|
// 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 log.
|
|
Said []string
|
|
}
|
|
|
|
// 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)
|
|
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{}}
|
|
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)
|
|
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, name+": "+d.Why)
|
|
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, 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
|
|
}
|