mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
One commit with exactly the tree of the reviewed head35b4509c0a, on main12c2574c9d, so two compiled programs added and removed on the branch leave no trace in history (issue 1577).
666 lines
25 KiB
Go
666 lines
25 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// A config value held for the open pull request that introduces its key (novox/hq ADR 0502, issue 1826).
|
|
//
|
|
// A key a pull request adds is declared by no registered manifest until it merges, so a value for it, which the
|
|
// pull request's composed check needs, was refused. The controller reads each pull request's manifests from the
|
|
// forge at its head and tells this module the config declarations the head adds or changes (PullDeclares); mesh-config
|
|
// reads no manifest itself (ADR 0495 §1). A proposal that names the pull request is judged against those, asked of the
|
|
// operator whatever the key's trust, and on Approve kept apart (KeptState) from the values it resolves. The
|
|
// controller asks for the approved held values of a check's pull requests (Held) and hands them to the composed
|
|
// check. At the merge the value is written as the module's own value on its node (PullMerged), where it resolves
|
|
// once the trunk declares the key; a pull request closed unmerged drops it (PullClosed). Nothing reaches a machine
|
|
// before the merge.
|
|
|
|
// The states of held values, and the actions their changes carry in the history and in `config-changed`.
|
|
const (
|
|
PullsState = "pulls"
|
|
KeptState = "kept"
|
|
|
|
ActionHold = "hold"
|
|
ActionDrop = "drop"
|
|
)
|
|
|
|
// pullName is a pull request as it is named: owner/repository#number.
|
|
var pullName = regexp.MustCompile(`^([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)#([0-9]+)$`)
|
|
|
|
func parsePull(s string) (owner, repo string, number int, err error) {
|
|
m := pullName.FindStringSubmatch(strings.TrimSpace(s))
|
|
if m == nil {
|
|
return "", "", 0, fmt.Errorf("%q is not a pull request: name it owner/repository#number", s)
|
|
}
|
|
n, _ := strconv.Atoi(m[3])
|
|
if n <= 0 {
|
|
return "", "", 0, fmt.Errorf("%q is not a pull request: its number is above zero", s)
|
|
}
|
|
return m[1], m[2], n, nil
|
|
}
|
|
|
|
// pullKey is a pull request's key in the states.
|
|
func pullKey(pull string) string {
|
|
return strings.NewReplacer("/", ".", "#", ".").Replace(strings.TrimSpace(pull))
|
|
}
|
|
|
|
// PullDecl is what the controller told: the config declarations a pull request's head adds or changes, by module.
|
|
type PullDecl struct {
|
|
Pull string `json:"pull"`
|
|
Head string `json:"head"`
|
|
Declares map[string]map[string]Declaration `json:"declares"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
// KeptValue is an approved held value, kept apart from the values mesh-config resolves: mesh-sdk's layout, which the
|
|
// checker and the assigner's composition check read (novox/hq ADR 0565 §5).
|
|
type KeptValue = delivery.KeptValue
|
|
|
|
// keptKey is a held value's key in `kept` (delivery.HeldValueKey), the pull request's key form kept for one that is
|
|
// not one.
|
|
func keptKey(pull, node, module, key string) string {
|
|
if k, err := delivery.HeldValueKey(pull, node, module, key); err == nil {
|
|
return k
|
|
}
|
|
return pullKey(pull) + "." + node + "." + module + "." + key
|
|
}
|
|
|
|
func keptStateKey(k KeptValue) string { return keptKey(k.Pull, k.Node, k.Module, k.Key) }
|
|
|
|
func keptLevel(k KeptValue) Level { return Level{Kind: LevelModule, Node: k.Node, Module: k.Module} }
|
|
|
|
// declDigest is the digest of what a declaration holds a value to: a held value stays only while it is the same.
|
|
func declDigest(d Declaration) string {
|
|
d.Description, d.Why, d.Kind, d.Runtime = "", "", "", false
|
|
if !d.HasDefault() {
|
|
d.Default = nil
|
|
} else {
|
|
d.Default = compact(d.Default)
|
|
}
|
|
raw, _ := json.Marshal(d)
|
|
sum := sha256.Sum256(raw)
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// PullForge is the forge as this module asks it of a pull request: its state, and whether it merged.
|
|
type PullForge interface {
|
|
Pull(owner, repo string, number int) (open, merged bool, head string, err error)
|
|
}
|
|
|
|
// pulls reads what is kept for pull requests, and the held values.
|
|
func (c *Config) readPulls(s *Sources) error {
|
|
s.Pulls, s.Kept = map[string]PullDecl{}, map[string]KeptValue{}
|
|
if err := c.each(PullsState, func(key string, raw []byte) error {
|
|
var p PullDecl
|
|
if err := json.Unmarshal(raw, &p); err != nil || p.Pull == "" {
|
|
return nil // not one this module wrote
|
|
}
|
|
s.Pulls[pullKey(p.Pull)] = p
|
|
return nil
|
|
}); err != nil {
|
|
return fmt.Errorf("the pull requests' declarations cannot be read: %w", err)
|
|
}
|
|
if err := c.each(KeptState, func(key string, raw []byte) error {
|
|
var k KeptValue
|
|
if err := json.Unmarshal(raw, &k); err != nil || k.Pull == "" {
|
|
return nil
|
|
}
|
|
s.Kept[key] = k
|
|
return nil
|
|
}); err != nil {
|
|
return fmt.Errorf("the held values cannot be read: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ---- pull-declares ---------------------------------------------------------------------------------------
|
|
|
|
// PullDeclaresAnswer is what pull-declares answers.
|
|
type PullDeclaresAnswer struct {
|
|
Said string `json:"said"`
|
|
Pull string `json:"pull"`
|
|
Head string `json:"head"`
|
|
Modules []string `json:"modules"`
|
|
Dropped []string `json:"dropped,omitempty"`
|
|
Unsaid []string `json:"unsaid,omitempty"`
|
|
}
|
|
|
|
// PullDeclares keeps what a pull request's head declares, from the controller alone, and judges again the values
|
|
// held for it: a value whose key is declared the same (by digest) and still passes is kept at the new head; any
|
|
// other is dropped, recorded and raised, and must be proposed again (ADR 0502 clause 6).
|
|
func (c *Config) PullDeclares(call Call) (*PullDeclaresAnswer, error) {
|
|
if call.Caller != ControllerCaller {
|
|
return nil, fmt.Errorf("pull-declares is the controller's: it reads a pull request's manifests and tells "+
|
|
"mesh-config what the head declares; %s is not the controller", call.Caller)
|
|
}
|
|
if sw, err := c.source(); err != nil {
|
|
return nil, err
|
|
} else if sw.Source == SourceIndexer {
|
|
return nil, errors.New("pull-declares is refused: since the take-over mesh-config reads a pull request's config " +
|
|
"declarations from the indexer's config-candidates (novox/hq ADR 0565 §4)")
|
|
}
|
|
if _, _, _, err := parsePull(call.Pull); err != nil {
|
|
return nil, err
|
|
}
|
|
head := strings.TrimSpace(call.Head)
|
|
if head == "" {
|
|
return nil, errors.New("name the head the declarations are read at")
|
|
}
|
|
raw, err := json.Marshal(call.Declares)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the declarations cannot be read: %v", err)
|
|
}
|
|
declares := map[string]map[string]Declaration{}
|
|
if call.Declares != nil {
|
|
if err := json.Unmarshal(raw, &declares); err != nil {
|
|
return nil, fmt.Errorf("declares is the modules' declarations, a key to a declaration for each module: %v", err)
|
|
}
|
|
}
|
|
for m, keys := range declares {
|
|
if !nodeOrModule.MatchString(m) {
|
|
return nil, fmt.Errorf("%q is not a module's name", m)
|
|
}
|
|
for k, d := range keys {
|
|
if !keyName.MatchString(k) {
|
|
return nil, fmt.Errorf("%q is not a config key", k)
|
|
}
|
|
if !d.HasDefault() {
|
|
d.Default = nil
|
|
}
|
|
keys[k] = d
|
|
}
|
|
}
|
|
pull := strings.TrimSpace(call.Pull)
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.declarePull(pull, head, declares)
|
|
}
|
|
|
|
// declarePull keeps what a pull request's head declares and judges again the values held for it; from the
|
|
// controller's pull-declares, or after the take-over from the indexer's config candidates. The caller holds the lock.
|
|
func (c *Config) declarePull(pull, head string, declares map[string]map[string]Declaration) (*PullDeclaresAnswer, error) {
|
|
if err := c.Store.Put(PullsState, pullKey(pull), PullDecl{Pull: pull, Head: head, Declares: declares,
|
|
At: c.now()}); err != nil {
|
|
return nil, fmt.Errorf("the declarations were not kept: %v", err)
|
|
}
|
|
a := &PullDeclaresAnswer{Pull: pull, Head: head, Modules: sortedKeys(declares)}
|
|
var s Sources
|
|
if err := c.readPulls(&s); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, k := range sortedKeys(s.Kept) {
|
|
kv := s.Kept[k]
|
|
if pullKey(kv.Pull) != pullKey(pull) {
|
|
continue
|
|
}
|
|
why := ""
|
|
d, declared := declares[kv.Module][kv.Key]
|
|
switch {
|
|
case !declared:
|
|
why = "the head no longer adds or changes its declaration"
|
|
case declDigest(d) != kv.Digest:
|
|
why = "the head declares the key otherwise than the value was approved against"
|
|
default:
|
|
var v any
|
|
_ = json.Unmarshal(kv.Value, &v)
|
|
if p := d.rule().check(v); len(p) > 0 {
|
|
why = "the value no longer passes: " + strings.Join(p, "; ")
|
|
}
|
|
}
|
|
if why != "" {
|
|
a.Unsaid = append(a.Unsaid, c.dropKept(kv, "the new head "+short(head)+": "+why)...)
|
|
a.Dropped = append(a.Dropped, fmt.Sprintf("%s for %s on %s: %s", kv.Key, kv.Module, kv.Node, why))
|
|
continue
|
|
}
|
|
if kv.Head != head {
|
|
kv.Head = head
|
|
if err := c.Store.Put(KeptState, k, kv); err != nil {
|
|
a.Unsaid = append(a.Unsaid, "the held value "+k+" was not moved to the new head: "+err.Error())
|
|
}
|
|
}
|
|
}
|
|
a.Said = fmt.Sprintf("keeps what %s declares at %s for %d module(s)", pull, short(head), len(declares))
|
|
if len(a.Dropped) > 0 {
|
|
a.Said += fmt.Sprintf("; %d held value(s) dropped", len(a.Dropped))
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// heldChange records a hold, a drop or a merge's set of a held value in the history, the hand-act log and
|
|
// config-changed; what could not be done is answered, not dropped.
|
|
func (c *Config) heldChange(kv KeptValue, action, why, approved string) []string {
|
|
level := keptLevel(kv)
|
|
ch := Change{Action: action, Level: &level, Key: kv.Key, Value: kv.Value, By: kv.By, Via: kv.Via, Why: why,
|
|
Approved: approved, Pull: kv.Pull, Head: kv.Head, At: c.now(), Moved: []Moved{}}
|
|
unsaid := c.record(level.ValueKey(kv.Key), ch)
|
|
cause := "a value held for " + kv.Pull
|
|
what := fmt.Sprintf("config %s %s of %s for %s, held for %s", action, kv.Key, kv.Module, kv.Node, kv.Pull)
|
|
if action != ActionDrop {
|
|
what += " (" + string(kv.Value) + ")"
|
|
}
|
|
if err := c.Store.HandAct(what, why, cause); err != nil {
|
|
unsaid = append(unsaid, "the hand-act log does not hold it: "+err.Error())
|
|
}
|
|
return unsaid
|
|
}
|
|
|
|
// dropKept removes a held value, saying why.
|
|
func (c *Config) dropKept(kv KeptValue, why string) []string {
|
|
if err := c.Store.Delete(KeptState, keptStateKey(kv)); err != nil {
|
|
return []string{"the held value " + keptStateKey(kv) + " was not dropped: " + err.Error()}
|
|
}
|
|
c.logf("[mesh-config] dropped %s of %s on %s held for %s: %s", kv.Key, kv.Module, kv.Node, kv.Pull, why)
|
|
return c.heldChange(kv, ActionDrop, "dropped, nothing was set: "+why, "")
|
|
}
|
|
|
|
// ---- held ------------------------------------------------------------------------------------------------
|
|
|
|
// HeldAnswer is what held answers: the approved held values of the pull requests asked, at the heads asked.
|
|
type HeldAnswer struct {
|
|
Held []HeldValue `json:"held"`
|
|
Skipped []HeldSkipped `json:"skipped"`
|
|
}
|
|
|
|
// HeldValue is one approved held value as it is handed to a composed check and listed.
|
|
type HeldValue struct {
|
|
Pull string `json:"pull"`
|
|
Head string `json:"head"`
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
Key string `json:"key"`
|
|
Value json.RawMessage `json:"value"`
|
|
Approved string `json:"approved"`
|
|
By string `json:"by,omitempty"`
|
|
Why string `json:"why,omitempty"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
// HeldSkipped is a pull request asked for whose held values are not answered, and why.
|
|
type HeldSkipped struct {
|
|
Pull string `json:"pull"`
|
|
Why string `json:"why"`
|
|
}
|
|
|
|
func heldValueOf(k KeptValue) HeldValue {
|
|
return HeldValue{Pull: k.Pull, Head: k.Head, Node: k.Node, Module: k.Module, Key: k.Key, Value: compact(k.Value),
|
|
Approved: k.Approved, By: k.By, Why: k.Why, At: k.At}
|
|
}
|
|
|
|
// Held answers the approved held values for the pull requests asked, at the heads asked, to the controller alone: a
|
|
// pull request at another head than the one kept is skipped and said (ADR 0502 clause 4).
|
|
func (c *Config) Held(call Call) (*HeldAnswer, error) {
|
|
if call.Caller != ControllerCaller {
|
|
return nil, fmt.Errorf("held is the controller's: it hands the held values to a composed check; %s is not "+
|
|
"the controller", call.Caller)
|
|
}
|
|
asked := map[string]string{}
|
|
var order []string
|
|
for _, p := range call.Pulls {
|
|
if _, _, _, err := parsePull(p.Pull); err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.TrimSpace(p.Head) == "" {
|
|
return nil, fmt.Errorf("name the head of %s", p.Pull)
|
|
}
|
|
if _, dup := asked[pullKey(p.Pull)]; !dup {
|
|
order = append(order, strings.TrimSpace(p.Pull))
|
|
}
|
|
asked[pullKey(p.Pull)] = strings.TrimSpace(p.Head)
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
var s Sources
|
|
if err := c.readPulls(&s); err != nil {
|
|
return nil, err
|
|
}
|
|
out := &HeldAnswer{Held: []HeldValue{}, Skipped: []HeldSkipped{}}
|
|
for _, p := range order {
|
|
head := asked[pullKey(p)]
|
|
var mine []KeptValue
|
|
for _, k := range sortedKeys(s.Kept) {
|
|
if pullKey(s.Kept[k].Pull) == pullKey(p) {
|
|
mine = append(mine, s.Kept[k])
|
|
}
|
|
}
|
|
for _, k := range mine {
|
|
if k.Head != head {
|
|
out.Skipped = append(out.Skipped, HeldSkipped{Pull: p, Why: fmt.Sprintf("%s for %s on %s is held "+
|
|
"at head %s, not %s", k.Key, k.Module, k.Node, short(k.Head), short(head))})
|
|
continue
|
|
}
|
|
out.Held = append(out.Held, heldValueOf(k))
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// HeldListed is the held values a listing shows, narrowed by module, node and key.
|
|
func heldListed(s Sources, call Call) []HeldValue {
|
|
out := []HeldValue{}
|
|
for _, k := range sortedKeys(s.Kept) {
|
|
kv := s.Kept[k]
|
|
if (call.Module == "" || kv.Module == call.Module) && (call.Node == "" || kv.Node == call.Node) &&
|
|
(call.Key == "" || kv.Key == call.Key) {
|
|
out = append(out, heldValueOf(kv))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ---- the proposal's rule -------------------------------------------------------------------------------
|
|
|
|
// heldRule is the rule a value held for a pull request is judged by: the pull request's declaration of the key for
|
|
// the module at its head, and the module's assignment to the node. Refused, saying what the pull request does
|
|
// declare, when it declares none.
|
|
func heldRule(s Sources, call Call, level Level) (rule, PullDecl, string, error) {
|
|
if level.Kind != LevelModule {
|
|
return rule{}, PullDecl{}, "", fmt.Errorf("a value held for a pull request is a module's own value on one "+
|
|
"node, and %s is not one: a predefined key a pull request adds is set after its merge (novox/hq ADR 0502)",
|
|
level)
|
|
}
|
|
pd, ok := s.Pulls[pullKey(call.Pull)]
|
|
if !ok {
|
|
return rule{}, PullDecl{}, "", fmt.Errorf("mesh-config holds no declaration for %s: the controller tells it "+
|
|
"what a pull request declares when its head is announced, and none has been", strings.TrimSpace(call.Pull))
|
|
}
|
|
d, ok := pd.Declares[level.Module][call.Key]
|
|
if !ok {
|
|
return rule{}, PullDecl{}, "", fmt.Errorf("%s does not add or change a value %q for %s at %s; it declares "+
|
|
"for %s: %s", pd.Pull, call.Key, level.Module, short(pd.Head), level.Module, joinKeys(pd.Declares[level.Module]))
|
|
}
|
|
if _, reg := s.Declared[level.Module]; !reg {
|
|
return rule{}, PullDecl{}, "", fmt.Errorf("%s is not a registered module, so a value cannot be held for it", level.Module)
|
|
}
|
|
if !assigned(s, level.Node, level.Module) {
|
|
return rule{}, PullDecl{}, "", fmt.Errorf("%s is not placed on %s, and a module's own value is set for a node "+
|
|
"it is placed on (novox/hq ADR 0348 §3); it is placed on: %s", level.Module, level.Node,
|
|
strings.Join(nodesOf(s, level.Module), ", "))
|
|
}
|
|
return d.rule(), pd, declDigest(d), nil
|
|
}
|
|
|
|
// keep holds an approved proposal's value apart. The caller holds the lock.
|
|
func (c *Config) keep(p Proposal) (string, error) {
|
|
var s Sources
|
|
if err := c.readPulls(&s); err != nil {
|
|
return "", err
|
|
}
|
|
pd, ok := s.Pulls[pullKey(p.Pull)]
|
|
if !ok {
|
|
return "", fmt.Errorf("mesh-config holds no declaration for %s any more", p.Pull)
|
|
}
|
|
d, ok := pd.Declares[p.Level.Module][p.Key]
|
|
if !ok || declDigest(d) != p.DeclDigest {
|
|
return "", fmt.Errorf("%s declares %s otherwise than the operator was shown, or no longer declares it: "+
|
|
"propose it again", p.Pull, p.Key)
|
|
}
|
|
was, had := s.Kept[keptKey(p.Pull, p.Level.Node, p.Level.Module, p.Key)]
|
|
if had != p.HadValue || valueDigest(was.Value, had) != p.WasDigest {
|
|
return "", fmt.Errorf("the value held for %s changed since the operator was shown the change: propose it "+
|
|
"again", p.Pull)
|
|
}
|
|
kv := KeptValue{Pull: p.Pull, Head: pd.Head, Digest: p.DeclDigest, Node: p.Level.Node, Module: p.Level.Module,
|
|
Key: p.Key, Value: p.Value, By: "the operator", Via: p.From, Why: p.Why, Approved: p.Approved, Proposal: p.ID,
|
|
At: c.now()}
|
|
if err := c.Store.Put(KeptState, keptStateKey(kv), kv); err != nil {
|
|
return "", fmt.Errorf("the value was not kept: %v", err)
|
|
}
|
|
unsaid := c.heldChange(kv, ActionHold, "held for "+p.Pull+" until it merges: "+p.Why, p.Approved)
|
|
said := fmt.Sprintf("%s of %s on %s is held at %s for %s until it merges", p.Key, p.Level.Module, p.Level.Node,
|
|
short(pd.Head), p.Pull)
|
|
if len(unsaid) > 0 {
|
|
said += " (" + strings.Join(unsaid, "; ") + ")"
|
|
}
|
|
return said, nil
|
|
}
|
|
|
|
// ---- the merge and the close ---------------------------------------------------------------------------
|
|
|
|
// PullMerged writes each value held for a pull request as the module's own value on its node, with the approval kept
|
|
// beside it; it resolves once the trunk declares the key. It answers a line for each.
|
|
func (c *Config) PullMerged(pull, head string) []string {
|
|
return c.settlePull(pull, true, head)
|
|
}
|
|
|
|
// PullClosed drops the values held for a pull request closed unmerged, and says that nothing was set.
|
|
func (c *Config) PullClosed(pull string) []string {
|
|
return c.settlePull(pull, false, "")
|
|
}
|
|
|
|
func (c *Config) settlePull(pull string, merged bool, head string) []string {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
var s Sources
|
|
if err := c.readPulls(&s); err != nil {
|
|
return []string{err.Error()}
|
|
}
|
|
var said []string
|
|
wrote := false
|
|
for _, k := range sortedKeys(s.Kept) {
|
|
kv := s.Kept[k]
|
|
if pullKey(kv.Pull) != pullKey(pull) {
|
|
continue
|
|
}
|
|
if !merged {
|
|
said = append(said, fmt.Sprintf("%s of %s on %s was held for %s, which closed unmerged: dropped, "+
|
|
"nothing was set", kv.Key, kv.Module, kv.Node, kv.Pull))
|
|
said = append(said, c.dropKept(kv, kv.Pull+" closed unmerged")...)
|
|
continue
|
|
}
|
|
// **Only a value held at the merged head, whose key that head declares as it was approved** (ADR 0502 clause 7).
|
|
why := ""
|
|
pd, known := s.Pulls[pullKey(pull)]
|
|
d, declared := pd.Declares[kv.Module][kv.Key]
|
|
switch {
|
|
case head == "":
|
|
why = "the merged head is not known, so the value cannot be held to it"
|
|
case !strings.EqualFold(head, kv.Head):
|
|
why = fmt.Sprintf("it is held at head %s and the pull request merged at %s", short(kv.Head), short(head))
|
|
case !known || !declared || declDigest(d) != kv.Digest:
|
|
why = "the merged head does not declare the key as the operator was shown"
|
|
}
|
|
if why != "" {
|
|
said = append(said, fmt.Sprintf("%s of %s on %s was held for %s, which merged: dropped, nothing was set (%s)",
|
|
kv.Key, kv.Module, kv.Node, kv.Pull, why))
|
|
said = append(said, c.dropKept(kv, kv.Pull+" merged, but "+why)...)
|
|
continue
|
|
}
|
|
vk := keptLevel(kv).ValueKey(kv.Key)
|
|
v := Value{Value: compact(kv.Value), By: kv.By, Via: kv.Via, Why: kv.Why + " (held for " + kv.Pull +
|
|
", merged)", Approved: kv.Approved, At: c.now()}
|
|
if err := c.Store.Put(ValuesState, vk, v); err != nil {
|
|
said = append(said, fmt.Sprintf("%s of %s on %s held for %s is not written yet: %v; it is tried "+
|
|
"again", kv.Key, kv.Module, kv.Node, kv.Pull, err))
|
|
continue
|
|
}
|
|
if err := c.Store.Delete(KeptState, k); err != nil {
|
|
said = append(said, "the held value "+k+" is written but not removed: "+err.Error())
|
|
}
|
|
wrote = true
|
|
said = append(said, c.heldChange(kv, "set", kv.Pull+" merged: the held value is the module's own value now",
|
|
kv.Approved)...)
|
|
said = append(said, fmt.Sprintf("%s of %s on %s held for %s is set: %s", kv.Key, kv.Module, kv.Node,
|
|
kv.Pull, kv.Value))
|
|
}
|
|
if wrote {
|
|
if s2, err := c.sources(); err == nil {
|
|
if _, err := c.writeResolved(s2); err != nil {
|
|
said = append(said, "the resolved values are not all written yet ("+err.Error()+"); the next "+
|
|
"resolution writes them")
|
|
}
|
|
}
|
|
}
|
|
left := 0
|
|
for _, k := range sortedKeys(s.Kept) {
|
|
if pullKey(s.Kept[k].Pull) != pullKey(pull) {
|
|
continue
|
|
}
|
|
if e, err := c.Store.Get(KeptState, k); err != nil || e != nil {
|
|
left++
|
|
}
|
|
}
|
|
if left == 0 {
|
|
if err := c.Store.Delete(PullsState, pullKey(pull)); err != nil {
|
|
said = append(said, "the declarations of "+pull+" were not removed: "+err.Error())
|
|
}
|
|
}
|
|
return said
|
|
}
|
|
|
|
// PullEvent is the forge's `pull.merged` or `pull.closed`, as far as this module reads it.
|
|
type PullEvent struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
Number int `json:"number"`
|
|
Merged bool `json:"merged,omitempty"`
|
|
// HeadSHA is the head the pull request merged at (the forge's own word).
|
|
HeadSHA string `json:"head_sha,omitempty"`
|
|
}
|
|
|
|
func (p PullEvent) name() string { return fmt.Sprintf("%s/%s#%d", p.Owner, p.Repo, p.Number) }
|
|
|
|
// HeardPull takes the forge's word that a pull request merged or closed. A close is asked of the forge when it can
|
|
// be, because a merged pull request may be announced as closed too.
|
|
func (c *Config) HeardPull(p PullEvent, merged bool) []string {
|
|
if p.Owner == "" || p.Repo == "" || p.Number <= 0 {
|
|
return nil
|
|
}
|
|
head := p.HeadSHA
|
|
if !merged && !p.Merged && c.Forge != nil {
|
|
// A close is told from a merge by the forge; when it cannot be asked nothing is dropped, and the five-minute
|
|
// look finds it.
|
|
open, m, h, err := c.Forge.Pull(p.Owner, p.Repo, p.Number)
|
|
if err != nil {
|
|
c.logf("[mesh-config] %s closed, and the forge does not say whether it merged (%v); the next look decides", p.name(), err)
|
|
return nil
|
|
}
|
|
if open {
|
|
return nil
|
|
}
|
|
merged = m
|
|
if head == "" {
|
|
head = h
|
|
}
|
|
}
|
|
return c.settlePull(p.name(), merged || p.Merged, head)
|
|
}
|
|
|
|
// SweepPulls looks at the forge for every pull request it keeps something for, so a merge or a close it did not hear
|
|
// is found: run with each five-minute resolution.
|
|
func (c *Config) SweepPulls() []string {
|
|
if c.Forge == nil {
|
|
return nil
|
|
}
|
|
c.mu.Lock()
|
|
var s Sources
|
|
err := c.readPulls(&s)
|
|
c.mu.Unlock()
|
|
if err != nil {
|
|
return []string{err.Error()}
|
|
}
|
|
seen := map[string]string{}
|
|
for _, p := range s.Pulls {
|
|
seen[pullKey(p.Pull)] = p.Pull
|
|
}
|
|
for _, k := range s.Kept {
|
|
seen[pullKey(k.Pull)] = k.Pull
|
|
}
|
|
var said []string
|
|
for _, key := range sortedKeys(seen) {
|
|
pull := seen[key]
|
|
owner, repo, n, err := parsePull(pull)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
open, merged, head, err := c.Forge.Pull(owner, repo, n)
|
|
if err != nil {
|
|
said = append(said, fmt.Sprintf("the forge does not say whether %s is open: %v", pull, err))
|
|
continue
|
|
}
|
|
if !open {
|
|
said = append(said, c.settlePull(pull, merged, head)...)
|
|
}
|
|
}
|
|
return said
|
|
}
|
|
|
|
// PullAt is a pull request at a head, as held asks about it.
|
|
type PullAt struct {
|
|
Pull string `json:"pull"`
|
|
Head string `json:"head"`
|
|
}
|
|
|
|
// toolForge asks the forge's holder through its tools.
|
|
type toolForge struct {
|
|
ask func(key string, body any) (json.RawMessage, error)
|
|
}
|
|
|
|
// Pull asks the forge's gitea_get_pull_request whether a pull request is open, and whether it merged.
|
|
func (f toolForge) Pull(owner, repo string, number int) (bool, bool, string, error) {
|
|
raw, err := f.ask("gitea.gitea_get_pull_request", map[string]any{"owner": owner, "repo": repo, "number": number})
|
|
if err != nil {
|
|
return false, false, "", err
|
|
}
|
|
answer, output, ok := unwrapAnswer(raw)
|
|
if !ok {
|
|
return false, false, "", fmt.Errorf("the forge refused gitea_get_pull_request: %s", output)
|
|
}
|
|
var a struct {
|
|
Pull struct {
|
|
State string `json:"state"`
|
|
Merged bool `json:"merged"`
|
|
// head is the branch name; the commit is head_sha (the forge module's mapPull).
|
|
HeadSHA string `json:"head_sha"`
|
|
} `json:"pull"`
|
|
}
|
|
if err := json.Unmarshal(answer, &a); err != nil || a.Pull.State == "" {
|
|
return false, false, "", fmt.Errorf("the forge's gitea_get_pull_request is not readable: %s", output)
|
|
}
|
|
return a.Pull.State == "open", a.Pull.Merged, a.Pull.HeadSHA, nil
|
|
}
|
|
|
|
// unwrapAnswer reads a tool's answer whatever wraps it: the protocol's content list, or {output, ok, answer}.
|
|
func unwrapAnswer(raw json.RawMessage) (json.RawMessage, string, bool) {
|
|
var s string
|
|
if json.Unmarshal(raw, &s) == nil {
|
|
return unwrapAnswer(json.RawMessage(s))
|
|
}
|
|
var m map[string]json.RawMessage
|
|
if json.Unmarshal(raw, &m) != nil {
|
|
return raw, string(raw), true
|
|
}
|
|
if content, has := m["content"]; has {
|
|
var items []struct {
|
|
Text string `json:"text"`
|
|
}
|
|
var e struct {
|
|
IsError bool `json:"isError"`
|
|
}
|
|
_ = json.Unmarshal(raw, &e)
|
|
if json.Unmarshal(content, &items) == nil && len(items) > 0 {
|
|
inner, out, ok := unwrapAnswer(json.RawMessage(items[0].Text))
|
|
return inner, out, ok && !e.IsError
|
|
}
|
|
}
|
|
var v struct {
|
|
Output string `json:"output"`
|
|
OK *bool `json:"ok"`
|
|
Answer json.RawMessage `json:"answer"`
|
|
}
|
|
if json.Unmarshal(raw, &v) == nil && v.OK != nil {
|
|
return v.Answer, v.Output, *v.OK
|
|
}
|
|
return raw, string(raw), true
|
|
}
|