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 } // A seat that kept it whole (another tag names its digest) ends it here: recorded kept, never asked again. if l, done := a.removeOne(ctx, e); done { delete(left, at) } else if l != "" { left[at] = l } } 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) }