mesh/check failed: g14: novox/mesh-catalog#437: checks/imagebuild/go.mod cannot be read to check its mesh-sdk pin: Gitea API /repos/novox/mesh-catalog…
mesh/repo-check head 332aa75afd19 merged into trunk 74baa5378716 beside mesh-controller#434@4357b368 — pass: its merge-check.sh passed, each part in its to…
mesh/merge-gate head 332aa75afd19 merged into trunk 74baa5378716 — pass: group fix/1826-held-config composed: every machine composes with the change as it …
mesh/delivery delivered
mesh/delivery-group group fix/1826-held-config delivered: every member is delivered
661 lines
24 KiB
Go
661 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// 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.
|
|
type KeptValue struct {
|
|
Pull string `json:"pull"`
|
|
Head string `json:"head"`
|
|
Digest string `json:"declaration"`
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
Key string `json:"key"`
|
|
Value json.RawMessage `json:"value"`
|
|
By string `json:"by"`
|
|
Via string `json:"via"`
|
|
Why string `json:"why"`
|
|
// Approved is who approved it, as the proposal kept it.
|
|
Approved string `json:"approved"`
|
|
Proposal string `json:"proposal"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
func keptKey(pull, node, module, key string) string {
|
|
return pullKey(pull) + "." + node + "." + module + "." + key
|
|
}
|
|
|
|
func (k KeptValue) stateKey() string { return keptKey(k.Pull, k.Node, k.Module, k.Key) }
|
|
|
|
func (k KeptValue) level() 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 _, _, _, 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()
|
|
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 := kv.level()
|
|
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, kv.stateKey()); err != nil {
|
|
return []string{"the held value " + kv.stateKey() + " 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 assigned to %s, and a module's own value is set for a node "+
|
|
"it is assigned to (novox/hq ADR 0348 §3); it is assigned to: %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, kv.stateKey(), 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 := kv.level().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
|
|
}
|