Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/pullkey.go
T
jschoubben 454fb8a918
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1792777718506798022 (novox/mesh-catalog at e8300455 into main,…
mesh/delivery-group group feat/r4g merged: every member is merged
mesh/check checking 454fb8a918ad on 3bec357968da | novox/mesh-catalog#464 in group feat/r4g
Write a pull request's check verdict into its key after the key is created: the status event can be taken first (R4g, issue 1887)
2026-10-23 19:42:54 +02:00

158 lines
6.1 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// The pull request's key (novox/hq ADR 0406 §4, issue 975): mesh-delivery keeps one key per pull request on the bus,
// holding its head, the trunk commit its current check is asked against and its verdict, written by compare-and-set.
// It writes a new head when the forge says one (`pull.updated`, which carries the head and the trunk commit it was
// announced against), and a new trunk commit only when the check is asked again against one — the merge tool's ask is
// that same announcement. A merge of a group's member, and a trunk move that leaves an inactive pull request unasked,
// are announced as nothing (the gitea module announces `pull.updated` only for a head and for the merge tool's ask), so
// they write nothing. A verdict for that head and trunk commit writes the verdict. The build seat reads the key.
// PullKey is what the bus keeps of one pull request.
type PullKey struct {
Head string `json:"head"`
Trunk string `json:"trunk,omitempty"`
Verdict string `json:"verdict,omitempty"`
}
// PullKeys keeps the pull requests' keys: Change reads the key, applies the change to what it read and writes the result
// by compare-and-set when the change says to write; a write another writer won reads the key again and applies again.
type PullKeys interface {
Change(key string, apply func(PullKey) (PullKey, bool)) error
}
// pullKeyName is a pull request's key: owner.repo.number, with any dot in a name made an underscore.
func pullKeyName(owner, repo string, number int) string {
clean := func(s string) string { return strings.NewReplacer(".", "_", "/", "_").Replace(s) }
return fmt.Sprintf("%s.%s.%d", clean(owner), clean(repo), number)
}
// KeepPull writes the key of a pull request whose head was announced, when the head or the trunk commit it was
// announced against is not what the key holds; a new head or trunk clears the verdict.
func (h *Holder) KeepPull(p PullEvent) {
if h.Pulls == nil || p.Number == 0 || p.HeadSHA == "" {
return
}
err := h.Pulls.Change(pullKeyName(p.Owner, p.Repo, p.Number), func(k PullKey) (PullKey, bool) {
if sameCommit(k.Head, p.HeadSHA) && k.Head != "" && (p.BaseSHA == "" || sameCommit(k.Trunk, p.BaseSHA)) {
return k, false
}
return PullKey{Head: p.HeadSHA, Trunk: p.BaseSHA}, true
})
if err != nil {
h.Logf("[mesh-delivery] the key of %s/%s#%d could not be written: %v", p.Owner, p.Repo, p.Number, err)
h.again("a pull request's key that could not be written")
return
}
// The head's mesh/check may have been heard before this key held the head: the two events are taken on goroutines of
// their own, and KeepCheckVerdict then found no head to write a verdict to. The verdict the delivery holds for this head
// and trunk is written now; read after the write above landed, it cannot miss one taken before that write.
verdict := h.checkVerdictOf(p.repository(), p.HeadSHA, p.BaseSHA)
if verdict == "" {
return
}
err = h.Pulls.Change(pullKeyName(p.Owner, p.Repo, p.Number), func(k PullKey) (PullKey, bool) {
if !sameCommit(k.Head, p.HeadSHA) || !sameCommit(k.Trunk, p.BaseSHA) || k.Verdict != "" {
return k, false
}
k.Verdict = verdict
return k, true
})
if err != nil {
h.Logf("[mesh-delivery] the verdict in the key of %s/%s#%d could not be written: %v", p.Owner, p.Repo, p.Number, err)
h.again("a pull request's key that could not be written")
}
}
// checkVerdictOf is the verdict of the mesh/check the delivery of a head holds, when it judged this head on this trunk
// commit; empty otherwise. Outside the lock.
func (h *Holder) checkVerdictOf(repository, head, trunk string) string {
h.mu.Lock()
defer h.mu.Unlock()
d := h.deliveries[IDOf(strings.ToLower(repository), head)]
if d == nil || d.Check == nil || d.Check.Status != ctxCheck || !sameCommit(d.Check.Head, head) || !sameCommit(d.Check.Trunk, trunk) {
return ""
}
return d.Check.Gate
}
// KeepCheckVerdict writes the verdict of a head's mesh/check into the key of its pull request, when the key holds that head
// and the trunk commit the check line names (issue 1302; the controller's verdict wrote it before issue 1318). Outside the lock.
func (h *Holder) KeepCheckVerdict(e ForgeStatusEvent) {
if h.Pulls == nil || e.Context != ctxCheck || e.Description == "" || e.SHA == "" {
return
}
verdict := map[string]string{"success": "pass", "warning": "warning", "failure": "fail", "error": "error"}[e.State]
_, trunk := checkLine(e.Description)
if verdict == "" || trunk == "" {
return // pending is no verdict; a line naming no trunk commit is judged against none
}
h.mu.Lock()
number := 0
if d := h.deliveries[IDOf(strings.ToLower(e.Owner+"/"+e.Repo), e.SHA)]; d != nil {
number = d.Number
}
h.mu.Unlock()
if number == 0 {
return
}
err := h.Pulls.Change(pullKeyName(e.Owner, e.Repo, number), func(k PullKey) (PullKey, bool) {
if !sameCommit(k.Head, e.SHA) || !sameCommit(k.Trunk, trunk) || k.Verdict == verdict {
return k, false
}
k.Verdict = verdict
return k, true
})
if err != nil {
h.Logf("[mesh-delivery] the verdict in the key of %s/%s#%d could not be written: %v", e.Owner, e.Repo, number, err)
h.again("a pull request's key that could not be written")
}
}
// kvPulls keeps the keys in the module's own state `pulls`.
type kvPulls struct{}
// changeAttempts is how often a lost compare-and-set is read and applied again before it is said.
const changeAttempts = 10
func (kvPulls) Change(key string, apply func(PullKey) (PullKey, bool)) error {
state := stdio.State("pulls")
for range changeAttempts {
e, err := state.Get(key)
if err != nil {
return err
}
var k PullKey
if e != nil {
if err := json.Unmarshal(e.Value, &k); err != nil {
return err
}
}
next, write := apply(k)
if !write {
return nil
}
if e == nil {
_, err = state.Create(key, next)
} else {
_, err = state.Update(key, next, e.Revision)
}
if err == nil {
return nil
}
if !errors.Is(err, stdio.ErrChanged) {
return err
}
}
return fmt.Errorf("%s was written by another writer first %d times: %w", key, changeAttempts, stdio.ErrChanged)
}