package versions import ( "context" "encoding/json" "errors" "fmt" "sort" "strings" "time" "git.novox.be/novox/mesh-sdk/go/delivery" ) // 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: /") 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-.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 " + "-" + t.Tag + ". 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 += "; " + delivery.VersionNumberedEvent + " 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 }