The window opens while a counted node-engine reads, of some kind, none of the newest form sdk/forms knows, and closes when none does: said once as version.old-form-closed, and a node holding it seven days is old-form-held about it. It steps on a statement's change, the placement, a hand drop and each build key written, never on a timer. A retired node dropped by hand leaves the count of the forms a build key keeps (design 51). internal/versions' own Forms goes: which forms a key keeps is sdk/forms'. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
318 lines
14 KiB
Go
318 lines
14 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
|
|
}
|