DLV-B6: a branch's end removes its tagged versions, acknowledged once every seat said removed; the checker's tag-wanted is kept in tag-asks
The builder hears the forge bridge's stream on a durable consumer: merged, a pull's closed and branch-deleted ask remove of each tagged version still held, and the bus gives the message again each minute until all are removed; past 15 minutes its finding tag-removal-not-done names the seats. The builder gains the state findings (ADR 0564 §2). Every forge message also wakes a floor whose bridge_tags was silent. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A branch's end removes its tagged versions (design 51 *Tagged versions*, DLV-B6): on the forge bridge's `merged`, a
|
||||
// pull's `closed` or `branch-deleted`, heard on the builder's durable consumer of the stream `forge`, the builder calls
|
||||
// each seat's `remove` and acknowledges the message only once every seat said `removed` (Heard marks the entry); until
|
||||
// then the bus gives it again each minute (RemoveAgainAfter) and, past RemovalFindingAfter, the builder's finding names
|
||||
// the seats still owing.
|
||||
const (
|
||||
RemoveAgainAfter = time.Minute
|
||||
RemovalFindingAfter = 15 * time.Minute
|
||||
// RemovalNotDone is the finding's kind.
|
||||
RemovalNotDone = "tag-removal-not-done"
|
||||
)
|
||||
|
||||
// BranchEnd is what one hearing of a branch's end did: the versions still not removed (name@version to its seat, with
|
||||
// the error of a remove that failed), and whether all are gone so the message is acknowledged.
|
||||
type BranchEnd struct {
|
||||
Left map[string]string
|
||||
Done bool
|
||||
}
|
||||
|
||||
// Seats are the seats still owing a removal, sorted, each once.
|
||||
func (b BranchEnd) Seats() []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, s := range b.Left {
|
||||
seat, _, _ := strings.Cut(s, ":")
|
||||
if !seen[seat] {
|
||||
seen[seat] = true
|
||||
out = append(out, seat)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Finding is the removal finding's words: what is left, since when.
|
||||
func (b BranchEnd) Finding(repository, branch string, since time.Time) string {
|
||||
var at []string
|
||||
for v, s := range b.Left {
|
||||
at = append(at, v+" on "+s)
|
||||
}
|
||||
sort.Strings(at)
|
||||
return fmt.Sprintf("the tagged versions of %s %s are not all removed since its end at %s: %s; the builder asks remove again each minute",
|
||||
repository, branch, since.UTC().Format(time.RFC3339), strings.Join(at, ", "))
|
||||
}
|
||||
|
||||
// BranchEnded removes repository's tagged versions of branch: every one not yet removed is asked of its seat again.
|
||||
// A store that cannot be read is the error: nothing is acknowledged then.
|
||||
func (a Acts) BranchEnded(ctx context.Context, repository, branch string) (BranchEnd, error) {
|
||||
all, err := a.Numberer.Versions.All(ctx)
|
||||
if err != nil {
|
||||
return BranchEnd{}, err
|
||||
}
|
||||
left := map[string]string{}
|
||||
for _, e := range Untagged(all, repository, branch) {
|
||||
at := e.Name + "@" + e.Version
|
||||
left[at] = e.Seat
|
||||
if a.Remove == nil {
|
||||
continue
|
||||
}
|
||||
if err := a.Remove(ctx, e.Seat, e); err != nil {
|
||||
left[at] = e.Seat + ": " + err.Error()
|
||||
}
|
||||
}
|
||||
return BranchEnd{Left: left, Done: len(left) == 0}, nil
|
||||
}
|
||||
|
||||
// TagWanted is the body of the checker's `tag-wanted` (design 51 *Tagged versions*): a group member's head to number
|
||||
// tagged versions for.
|
||||
type TagWanted struct {
|
||||
Repository string `json:"repository"`
|
||||
Branch string `json:"branch"`
|
||||
Head string `json:"head"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
}
|
||||
|
||||
// TagWantedAsk is the tag-asks value a tag-wanted becomes, as builder_tag writes it by hand: by the checker.
|
||||
func TagWantedAsk(t TagWanted, by string, at time.Time) (string, []byte, error) {
|
||||
return NewTagAsk(t.Repository, t.Branch, t.Head, "the checker's tag-wanted for a group member's check", by, at)
|
||||
}
|
||||
@@ -584,5 +584,22 @@ func TestTheBuildersVersions(t *testing.T) {
|
||||
if k := acts.TagAsked(context.Background(), a, at); k.State != TagAsked || k.Answer == "" {
|
||||
t.Errorf("a tag ask is not kept: %+v", k)
|
||||
}
|
||||
// DLV-B6: a branch's end asks remove of every tagged version not removed, and is done once each seat said removed.
|
||||
removed = nil
|
||||
end, err := acts.BranchEnded(context.Background(), "o/r", "feat/x")
|
||||
if err != nil || end.Done || len(removed) != 2 || !reflect.DeepEqual(end.Seats(), []string{"", SeatNPM}) || !strings.Contains(end.Finding("o/r", "feat/x", at), "n3@0.1.0-feat-x.1 on npm-package-registry: down") {
|
||||
t.Fatalf("BranchEnded = %+v %v, removed %v", end, err, removed)
|
||||
}
|
||||
for _, v := range [][2]string{{"n", "0.1.2-feat-x.1"}, {"n3", "0.1.0-feat-x.1"}} {
|
||||
if _, err := acts.Numberer.Heard(context.Background(), SeatNPM, HeardRemoved, "rm", Outcome{Name: v[0], Version: v[1], Reason: "tag-gone"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if end, _ := acts.BranchEnded(context.Background(), "o/r", "feat/x"); !end.Done {
|
||||
t.Errorf("every seat said removed, and the branch's end is not done: %+v", end)
|
||||
}
|
||||
if k, v, err := TagWantedAsk(TagWanted{Repository: "o/r", Branch: "feat/x", Head: "abc"}, "mesh-delivery-checker", at); err != nil || k != TagAskKey("o/r", "feat/x") || !strings.Contains(string(v), `"by":"mesh-delivery-checker"`) {
|
||||
t.Errorf("TagWantedAsk = %q %s %v", k, v, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user