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
332 lines
15 KiB
Go
332 lines
15 KiB
Go
package versions
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// What builder_publish_again does with one fingerprint of a name (design 51 *Tools of the builder*), after a holder failed.
|
|
const (
|
|
// AgainPublish says version.numbered again for the recorded version: the build module publishes the held digest.
|
|
AgainPublish = "publish"
|
|
// AgainBuild builds the fingerprint again under the same number: the build's file is gone and no published was said
|
|
// for the number, so the new digest replaces the recorded one (Numberer.Built).
|
|
AgainBuild = "build"
|
|
// AgainRenumber takes a new number: a published was said for the number, so other bytes never go under it.
|
|
AgainRenumber = "renumber"
|
|
)
|
|
|
|
// Again is what builder_publish_again does, with the entry it acts on and why.
|
|
type Again struct {
|
|
Do string `json:"do"`
|
|
Entry Entry `json:"entry"`
|
|
Why string `json:"why"`
|
|
}
|
|
|
|
// PublishAgain decides builder_publish_again for fingerprint among a name's entries; built says whether the builder still
|
|
// holds the build's file (its done key). It refuses a fingerprint not numbered, and a version refused for other bytes
|
|
// than the ones recorded under it.
|
|
func PublishAgain(entries []Entry, fingerprint string, built bool) (Again, error) {
|
|
fingerprint = strings.TrimSpace(fingerprint)
|
|
if fingerprint == "" {
|
|
return Again{}, errors.New("fp is required: the fingerprint whose version is published again")
|
|
}
|
|
for _, e := range latestFirst(entries) {
|
|
if e.Tag != "" || e.Fingerprint != fingerprint {
|
|
continue
|
|
}
|
|
switch {
|
|
case e.State == Refused && e.RefusedDigest != "" && e.Digest != "" && e.RefusedDigest != e.Digest:
|
|
return Again{}, fmt.Errorf("%s@%s was refused for other bytes (%s, recorded %s): it is not published again; a new build takes a new number",
|
|
e.Name, e.Version, e.RefusedDigest, e.Digest)
|
|
case e.State == Published || e.State == Removed:
|
|
return Again{Do: AgainRenumber, Entry: e, Why: fmt.Sprintf("%s@%s was %s, so the fingerprint takes a new number", e.Name, e.Version, e.State)}, nil
|
|
case !built:
|
|
return Again{Do: AgainBuild, Entry: e, Why: fmt.Sprintf("the build's file is gone and no published was said for %s@%s: built again under the same number", e.Name, e.Version)}, nil
|
|
}
|
|
return Again{Do: AgainPublish, Entry: e, Why: fmt.Sprintf("%s@%s (%s) is said numbered again, so its build module publishes the held digest", e.Name, e.Version, e.State)}, nil
|
|
}
|
|
return Again{}, fmt.Errorf("fingerprint %s is not numbered for this name: builder_versions lists what is", fingerprint)
|
|
}
|
|
|
|
// The builder's state of tag asks (design 51 *Tagged versions*): one key per repository and branch, removed when the
|
|
// branch is merged or deleted or its pull request closed.
|
|
const (
|
|
TagAsksState = "tag-asks"
|
|
TagAsksBucket = "mesh-delivery-builder_tag-asks"
|
|
)
|
|
|
|
// The states of a tag ask.
|
|
const (
|
|
TagAsked = "asked"
|
|
TagNumbered = "numbered"
|
|
TagRemoving = "removing"
|
|
TagRemoved = "removed"
|
|
)
|
|
|
|
// TagAsk is one value of tag-asks: an ask of a tagged version for a branch's head, from the checker's tag-wanted or by
|
|
// hand (builder_tag), or by hand the removal of the branch's tagged versions (builder_untag, Untag).
|
|
type TagAsk struct {
|
|
V int `json:"v"`
|
|
Repository string `json:"repository"`
|
|
Branch string `json:"branch"`
|
|
Head string `json:"head,omitempty"`
|
|
Tag string `json:"tag"`
|
|
By string `json:"by,omitempty"`
|
|
Why string `json:"why,omitempty"`
|
|
At time.Time `json:"at"`
|
|
Untag bool `json:"untag,omitempty"`
|
|
// State, Versions (name to version), Left (what a removal still waits on, name@version to its seat) and Answer are
|
|
// the builder's.
|
|
State string `json:"state,omitempty"`
|
|
Versions map[string]string `json:"versions,omitempty"`
|
|
Left map[string]string `json:"left,omitempty"`
|
|
Answer string `json:"answer,omitempty"`
|
|
Answered time.Time `json:"answered,omitzero"`
|
|
}
|
|
|
|
// TagAskKey is the key of a repository's branch in tag-asks.
|
|
func TagAskKey(repository, branch string) string {
|
|
return EscapeToken(repository) + "." + EscapeToken(branch)
|
|
}
|
|
|
|
// NewTagAsk is the value builder_tag writes: its key and the value. The tag is the branch's, cleaned (CleanTag).
|
|
func NewTagAsk(repository, branch, head, why, by string, at time.Time) (string, []byte, error) {
|
|
return newTagAsk(repository, branch, head, why, by, at, false)
|
|
}
|
|
|
|
// NewUntag is the value builder_untag writes: its key and the value.
|
|
func NewUntag(repository, branch, why, by string, at time.Time) (string, []byte, error) {
|
|
return newTagAsk(repository, branch, "", why, by, at, true)
|
|
}
|
|
|
|
func newTagAsk(repository, branch, head, why, by string, at time.Time, untag bool) (string, []byte, error) {
|
|
repository, branch, head, why = strings.TrimSpace(repository), strings.TrimSpace(branch), strings.TrimSpace(head), strings.TrimSpace(why)
|
|
switch {
|
|
case repository == "" || !strings.Contains(repository, "/"):
|
|
return "", nil, errors.New("repository is required: <owner>/<name>")
|
|
case branch == "":
|
|
return "", nil, errors.New("branch is required")
|
|
case !untag && head == "":
|
|
return "", nil, errors.New("head is required: the commit the tagged version is built from")
|
|
case why == "":
|
|
return "", nil, errors.New("say why (why): a hand act is recorded")
|
|
}
|
|
data, err := json.Marshal(TagAsk{V: 1, Repository: repository, Branch: branch, Head: head, Tag: CleanTag(branch), By: by, Why: why,
|
|
At: at.UTC(), Untag: untag, State: TagAsked})
|
|
return TagAskKey(repository, branch), data, err
|
|
}
|
|
|
|
// DecodeTagAsk reads a tag-asks value.
|
|
func DecodeTagAsk(data []byte) (TagAsk, error) {
|
|
var a TagAsk
|
|
if err := json.Unmarshal(data, &a); err != nil {
|
|
return a, err
|
|
}
|
|
if a.V != 1 {
|
|
return a, fmt.Errorf("a tag-asks value of version %d, not 1", a.V)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// TagAsksAnswer is what builder_tag_asks answers: the open asks, oldest first.
|
|
type TagAsksAnswer struct {
|
|
Asks []TagAsk `json:"asks"`
|
|
}
|
|
|
|
// AnswerTagAsks is builder_tag_asks over the state read, only repository's when it is given.
|
|
func AnswerTagAsks(r Reader, repository string) (TagAsksAnswer, error) {
|
|
keys, err := r.Keys()
|
|
if err != nil {
|
|
return TagAsksAnswer{}, fmt.Errorf("%s cannot be listed: %w", TagAsksState, err)
|
|
}
|
|
out := TagAsksAnswer{Asks: []TagAsk{}}
|
|
for _, k := range keys {
|
|
raw, ok, err := r.Get(k)
|
|
if err != nil {
|
|
return TagAsksAnswer{}, fmt.Errorf("%s.%s: %w", TagAsksState, k, err)
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
a, err := DecodeTagAsk(raw)
|
|
if err != nil {
|
|
return TagAsksAnswer{}, fmt.Errorf("%s.%s holds what is not a tag ask: %w", TagAsksState, k, err)
|
|
}
|
|
if repository == "" || a.Repository == repository {
|
|
out.Asks = append(out.Asks, a)
|
|
}
|
|
}
|
|
sort.Slice(out.Asks, func(i, j int) bool { return out.Asks[i].At.Before(out.Asks[j].At) })
|
|
return out, nil
|
|
}
|
|
|
|
// Untagged are repository's tagged versions of tag that are not removed (a group's branch spans repositories, each
|
|
// ending apart): what a branch's end, or builder_untag, removes through
|
|
// each one's seat (design 51 *Tagged versions*). A version with no tag is never among them.
|
|
func Untagged(entries []Entry, repository, tag string) []Entry {
|
|
tag = CleanTag(tag)
|
|
var out []Entry
|
|
for _, e := range entries {
|
|
if e.Tag != "" && e.Repository == repository && CleanTag(e.Tag) == tag && e.State != Removed {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return InOrder(out)
|
|
}
|
|
|
|
// The tools' descriptions.
|
|
const (
|
|
PublishAgainDescription = "Publish one fingerprint's version again after its holder failed (design 51): with the build's file held, the " +
|
|
"builder says version.numbered again and the build module publishes the held digest; with the file gone, it builds the fingerprint " +
|
|
"again under the same number, the new digest replacing the recorded one, since no published was said for it; after a published (or " +
|
|
"a removal) it takes a new number. It refuses a fingerprint not numbered and a version the holder refused for other bytes. A hand " +
|
|
"act, recorded with its caller and why in the builder's log."
|
|
FloorAgainDescription = "Ask the floor of one name again now, by hand (design 51 *The floor*): the seat's versions, the forge bridge's " +
|
|
"bridge_tags and the indexer's indexer_requirements, as the builder asks them by itself when a silent source says something. " +
|
|
"Answers which sources were silent and whether the floor is now known. A name the builder's versions already hold needs no floor: " +
|
|
"refused, saying so. A hand act."
|
|
TagDescription = "Ask a tagged version for a branch's head by hand, as the checker's tag-wanted asks it (design 51 *Tagged versions*): " +
|
|
"the builder keeps the ask in its state tag-asks until the branch is merged or deleted, or its pull request closed, and numbers " +
|
|
"each package the head builds below the next trunk version (0.1.15-<tag>.2). A hand act."
|
|
TagAsksDescription = "The open tag asks of the builder's state tag-asks, oldest first: repository, branch, head, tag, who asked and why, " +
|
|
"its state (asked, numbered, removing, removed), the versions numbered for it and, while its tagged versions are removed, the " +
|
|
"seats that have not yet said removed. With repository, that repository's only. Read-only."
|
|
UntagDescription = "Remove a branch's tagged versions by hand, as its merge, its pull request's close or its deletion does (design 51 " +
|
|
"*Tagged versions*): the builder calls each seat's remove with the reason tag-gone and marks each version removed once its seat says " +
|
|
"removed. Answers the versions it removes and the seats still waited on. A version with no tag is never removed. A hand act."
|
|
)
|
|
|
|
// Acts are the builder's hand acts on its versions, answered by its program (the tools bundle writes them to reasks).
|
|
type Acts struct {
|
|
Numberer *Numberer
|
|
// Done says whether the build key of module and fingerprint is done: the build's file is held.
|
|
Done func(ctx context.Context, module, fp string) (bool, error)
|
|
// BuildAgain builds a failed key again (builder_build_again's path): its answer, or why it refused.
|
|
BuildAgain func(ctx context.Context, module, fp, why, by string) (answer string, refused bool)
|
|
// Reconsider looks at every module again, so a claim asks a name's floor.
|
|
Reconsider func(ctx context.Context)
|
|
// Remove calls a seat's verb remove for one tagged version, reason tag-gone.
|
|
Remove func(ctx context.Context, seat string, e Entry) error
|
|
// DropOldForm drops a retired node from the old form's count by hand (builder_old_form): its answer, or why not.
|
|
DropOldForm func(ctx context.Context, node, by, why string, at time.Time) (string, error)
|
|
}
|
|
|
|
// TagAsked answers one unanswered value of tag-asks. An untag calls remove on each of the branch's tagged versions'
|
|
// seats and leaves each in Left until its seat says removed (Heard, on the seat's events); a call that fails stays in
|
|
// Left with its error, asked again by the branch's end. A tag ask is kept: its versions are numbered when the head's
|
|
// packages are claimed for the group's check.
|
|
func (a Acts) TagAsked(ctx context.Context, t TagAsk, now time.Time) TagAsk {
|
|
t.Answered = now.UTC()
|
|
if !t.Untag {
|
|
t.State = TagAsked
|
|
t.Answer = "kept: each package the head builds is numbered " + "<next>-" + t.Tag + ".<n> when its build is claimed for the check"
|
|
return t
|
|
}
|
|
all, err := a.Numberer.Versions.All(ctx)
|
|
if err != nil {
|
|
t.Answer = "nothing removed: " + err.Error()
|
|
return t
|
|
}
|
|
t.Left, t.Versions = map[string]string{}, map[string]string{}
|
|
for _, e := range Untagged(all, t.Repository, t.Tag) {
|
|
t.Versions[e.Name] = e.Version
|
|
at := e.Name + "@" + e.Version
|
|
t.Left[at] = e.Seat
|
|
if err := a.Remove(ctx, e.Seat, e); err != nil {
|
|
t.Left[at] = e.Seat + ": " + err.Error()
|
|
}
|
|
}
|
|
if len(t.Left) == 0 {
|
|
t.State, t.Left, t.Answer = TagRemoved, nil, "the branch holds no tagged version: nothing to remove"
|
|
return t
|
|
}
|
|
t.State = TagRemoving
|
|
t.Answer = fmt.Sprintf("remove asked of %d tagged versions; each is marked removed when its seat says removed", len(t.Left))
|
|
return t
|
|
}
|
|
|
|
// PublishAgain is builder_publish_again of name's fingerprint: what it did, the version, and its answer; an error refuses.
|
|
func (a Acts) PublishAgain(ctx context.Context, name, fp, why, by string, at time.Time) (Again, string, error) {
|
|
module, _, _ := strings.Cut(name, "/")
|
|
entries, err := a.Numberer.Versions.Of(ctx, name)
|
|
if err != nil {
|
|
return Again{}, "", err
|
|
}
|
|
done, err := a.Done(ctx, module, fp)
|
|
if err != nil {
|
|
return Again{}, "", fmt.Errorf("the build key of %s at %s cannot be read: %w", module, fp, err)
|
|
}
|
|
ag, err := PublishAgain(entries, fp, done)
|
|
if err != nil {
|
|
return Again{}, "", err
|
|
}
|
|
e := ag.Entry
|
|
if ag.Do == AgainRenumber {
|
|
_, ne, err := a.Numberer.Versions.Number(ctx, Claim{Name: e.Name, Kind: e.Kind, Series: e.Series, Module: e.Module, Fingerprint: e.Fingerprint,
|
|
Commit: e.Commit, Revision: e.Revision, Seat: e.Seat, Merge: e.Merge, Fresh: true})
|
|
if err != nil {
|
|
return Again{}, "", err
|
|
}
|
|
ag.Entry = ne
|
|
}
|
|
answer := ag.Why
|
|
if ag.Do == AgainBuild || (ag.Do == AgainRenumber && !done) {
|
|
said, refused := a.BuildAgain(ctx, module, fp, why, by)
|
|
if refused {
|
|
return Again{}, "", fmt.Errorf("%s; and the build again was refused: %s", ag.Why, said)
|
|
}
|
|
answer += "; " + said
|
|
}
|
|
if ag.Do == AgainPublish || (ag.Do == AgainRenumber && done) {
|
|
if a.Numberer.Said != nil {
|
|
b := NumberedOf(ag.Entry, "")
|
|
b.Again = at.UTC().Format(time.RFC3339Nano)
|
|
a.Numberer.Said(b)
|
|
}
|
|
answer += "; " + NumberedEvent + " said for " + ag.Entry.Version
|
|
}
|
|
return ag, answer, nil
|
|
}
|
|
|
|
// FloorAgain is builder_floor_again of name: the floor counts only for a name the versions hold nothing of.
|
|
func (a Acts) FloorAgain(ctx context.Context, name string) (string, error) {
|
|
entries, err := a.Numberer.Versions.Of(ctx, name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(entries) > 0 {
|
|
return "", fmt.Errorf("%s holds %d versions: its floor counts only for its first number, so nothing is asked", name, len(entries))
|
|
}
|
|
answer := name + " waits on no floor in this builder: the floor is asked at its next claim"
|
|
if a.Numberer.Waits != nil {
|
|
for _, w := range a.Numberer.Waits.Waiting() {
|
|
if w.Name == name {
|
|
answer = fmt.Sprintf("the floor of %s is asked again (%s were silent)", name, strings.Join(w.Silent, ", "))
|
|
}
|
|
}
|
|
a.Numberer.Waits.Wake(WakeByHand, "", "", name)
|
|
}
|
|
a.Reconsider(ctx)
|
|
return answer + "; builder_versions shows its version once numbered", nil
|
|
}
|
|
|
|
// TagFor is the cleaned tag a kept tag ask names for repository's head: the ask a check's build of that head is
|
|
// numbered under (design 51 *Tagged versions*). An untag, or an ask whose branch ended, names none.
|
|
func TagFor(asks []TagAsk, repository, head string) (string, bool) {
|
|
for _, t := range asks {
|
|
if t.Untag || t.State == TagRemoving || t.State == TagRemoved || t.Head == "" {
|
|
continue
|
|
}
|
|
if t.Repository == repository && t.Head == head {
|
|
return t.Tag, t.Tag != ""
|
|
}
|
|
}
|
|
return "", false
|
|
}
|