Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/settings.go
T
jschoubben 32ad59fc64
mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/check pass 32ad59fc6402 on 609013a2638f | unit 2010 run, 2322 kept, 5 skipped | integration 2032 run, 2341 kept, 9 skipped | over bound: unit, in…
mesh-delivery-gate and gitea: a setting check-gates-merge; false asks a reviewed pull request's Approve and merges it without waiting for mesh/check
The operator, 2026-10-25 22:56: "run the check locally only, but not as a gate, it loses us too much time". Tonight each
merge ask waited 3-7 minutes for mesh/check, and every merge into main refused the next approved merge for a check judged
on the trunk before (mesh-delivery#52). Both settings default to true, so nothing changes until the operator sets them.
False: the gate makes a reviewed pull request ready and asks its Approve with no verdict, a trunk's move puts nothing out
of date, a verdict is kept and shown, and a group still merges in one pass; the merge tool merges a head the Approve
names whatever its mesh/check says. delivery_status and show say the mode.
2026-10-25 23:07:50 +01:00

109 lines
3.7 KiB
Go

package main
// The gate's settings (novox/hq ADR 0505 §4, §5), merged by the mesh into one JSON file the env names. Each has a
// default here, so a gate started without the file, or with a value it cannot read, keeps the decided bound and says
// why once.
import (
"encoding/json"
"errors"
"fmt"
"os"
"time"
)
const (
// DefaultRefusalFloor is the least a refusal the gate is already answering (the trunk moved and its check was
// asked again) waits before it is said as stalled, when the setting refusal-stall-floor-minutes gives none.
DefaultRefusalFloor = 10 * time.Minute
// DefaultAuthorBound is how long a delivery whose cure is its author's new head waits for one before the operator
// hears of it, when the setting author-new-head-minutes gives none (ADR 0505 §5).
DefaultAuthorBound = 2 * time.Hour
)
// Settings are the operator's values for this module.
type Settings struct {
RefusalFloorMinutes *int `json:"refusal-stall-floor-minutes"`
AuthorBoundMinutes *int `json:"author-new-head-minutes"`
// CheckGatesMerge is whether mesh/check gates the merge (the default, true). False (the operator, 2026-10-25 22:56:
// "run the check locally only, but not as a gate, it loses us too much time"): a reviewed pull request is ready and its
// Approve asked without a verdict, a trunk's move puts nothing out of date, and a verdict is shown, never waited on.
CheckGatesMerge *bool `json:"check-gates-merge"`
}
// readSettings reads the settings file; a value that is not a whole number of minutes, one or more, is refused.
func readSettings(path string) (Settings, error) {
var s Settings
if path == "" {
return s, errors.New("started without a settings file")
}
raw, err := os.ReadFile(path)
if err != nil {
return s, err
}
var loose map[string]any
if err := json.Unmarshal(raw, &loose); err != nil {
return s, fmt.Errorf("the settings file is not JSON: %v", err)
}
for name, into := range map[string]**int{"refusal-stall-floor-minutes": &s.RefusalFloorMinutes,
"author-new-head-minutes": &s.AuthorBoundMinutes} {
switch v := loose[name].(type) {
case nil:
case float64:
n := int(v)
if v < 1 || v != float64(n) {
return Settings{}, fmt.Errorf("%s is not a whole number of minutes, one or more", name)
}
*into = &n
default:
return Settings{}, fmt.Errorf("%s is not a number", name)
}
}
switch v := loose["check-gates-merge"].(type) {
case nil:
case bool:
s.CheckGatesMerge = &v
default:
return Settings{}, errors.New("check-gates-merge is not true or false")
}
return s, nil
}
// checkGates is whether mesh/check gates the merge: true unless the setting check-gates-merge is false.
func (h *Holder) checkGates() bool {
if h.Settings == nil {
return true
}
v := h.Settings().CheckGatesMerge
return v == nil || *v
}
// factsAt are the facts a settle reads at now: the moment, and whether the check gates the merge.
func (h *Holder) factsAt(now time.Time) Facts {
return Facts{Now: now, Ungated: !h.checkGates()}
}
// minutesOr is a setting's minutes, or the default when it gives none.
func minutesOr(n *int, def time.Duration) time.Duration {
if n == nil {
return def
}
return time.Duration(*n) * time.Minute
}
// refusalFloor is the least an answered refusal waits before it is said as stalled (ADR 0505 §4).
func (h *Holder) refusalFloor() time.Duration {
if h.Settings == nil {
return DefaultRefusalFloor
}
return minutesOr(h.Settings().RefusalFloorMinutes, DefaultRefusalFloor)
}
// authorBound is how long a delivery that needs its author's new head waits before the operator hears of it (§5).
func (h *Holder) authorBound() time.Duration {
if h.Settings == nil {
return DefaultAuthorBound
}
return minutesOr(h.Settings().AuthorBoundMinutes, DefaultAuthorBound)
}