Compare commits

..
Author SHA1 Message Date
jschoubben 75156fce00 gitea: forge-account reads the forge's own host key file before scanning its sshd (issue 1939)
mesh/check pass 75156fce007e on 25176e1ccdb3 | unit 31 run, 4403 kept | integration 0 run, 4476 kept | over bound: unit | composition passed in 83ms o…
mesh/delivery ready: it delivers once merged
2026-10-24 12:50:20 +02:00
11 changed files with 70 additions and 296 deletions
@@ -65,6 +65,7 @@ func configFromEnv(getenv func(string) string) (Config, error) {
HostFile: getenv("MESH_FORGE_HOST_FILE"),
OriginsFile: getenv("MESH_GITEA_ORIGINS_FILE"),
Port: port,
HostKeyFile: getenv("MESH_GITEA_HOST_KEY_FILE"), // optional
Forge: forgehttp.Client{URL: strings.TrimRight(getenv("MESH_GITEA_URL"), "/"), User: getenv("MESH_GITEA_ADMIN_USER"),
PasswordFile: getenv("MESH_GITEA_ADMIN_PASSWORD_FILE"), Timeout: 20 * time.Second},
Runner: execRunner{},
@@ -44,10 +44,13 @@ type Config struct {
// origins, one per line.
HostFile, OriginsFile string
// Port is where the forge's sshd is published on the mesh.
Port int
Forge forgehttp.Client
Runner Runner
Log func(format string, a ...any)
Port int
// HostKeyFile is the forge's own ed25519 host key (the .pub file in its data directory). It is read first; when it
// is unset or unreadable the forge's sshd is asked with ssh-keyscan instead (issue 1939).
HostKeyFile string
Forge forgehttp.Client
Runner Runner
Log func(format string, a ...any)
}
// Service answers the consumers of the provision `forge-account` (novox/hq ADR 0399, ADR 0400; issue 955): for each
@@ -448,14 +451,30 @@ func (s *Service) httpsBase() (string, error) {
return "", fmt.Errorf("%s names no https origin", s.cfg.OriginsFile)
}
// scan reads the forge's ed25519 host key from its own sshd and writes it as the machines' known_hosts line.
// scan gives the forge's ed25519 host key as the machines' known_hosts line. The key the forge itself holds is read
// from its file first; only when that cannot be read is the forge's sshd asked over the network, and when that fails
// too (the forge's container is restarting) the key read before is kept, as a host key does not change (issue 1939).
func (s *Service) scan(ctx context.Context) (string, error) {
host, err := s.hostName()
if err != nil {
return "", err
}
if s.cfg.HostKeyFile != "" {
raw, ferr := os.ReadFile(s.cfg.HostKeyFile)
if ferr == nil {
if line, perr := knownHostLine("127.0.0.1 "+strings.TrimSpace(string(raw)), host, s.cfg.Port); perr == nil {
return line, nil
}
}
}
out, err := s.cfg.Runner.Run(ctx, "ssh-keyscan", "-t", "ed25519", "-p", fmt.Sprint(s.cfg.Port), "127.0.0.1")
if err != nil {
s.mu.Lock()
kept := s.known
s.mu.Unlock()
if kept != "" {
return kept, nil
}
return "", fmt.Errorf("the forge's ssh host key cannot be read: %w", err)
}
return knownHostLine(string(out), host, s.cfg.Port)
@@ -696,3 +696,42 @@ func TestNothingIsSaidForAPerNodeUserWithoutMeshKeys(t *testing.T) {
}
}
}
// The forge's own host key file is read first: no scan over the network is needed (issue 1939).
func TestTheHostKeyIsReadFromTheForgesOwnFile(t *testing.T) {
r := newRig(t)
f := filepath.Join(t.TempDir(), "ssh_host_ed25519_key.pub")
if err := os.WriteFile(f, []byte("ssh-ed25519 AAAAC3FILEKEY forge@host\n"), 0o644); err != nil {
t.Fatal(err)
}
r.svc.cfg.HostKeyFile = f
r.runner.err = errors.New("sshd down")
got, err := r.svc.scan(context.Background())
if err != nil || got != "[novox.internal]:222 ssh-ed25519 AAAAC3FILEKEY" {
t.Fatalf("scan = %q, %v", got, err)
}
if r.runner.args != nil {
t.Errorf("the forge's sshd was asked: %v", r.runner.args)
}
}
// A file that cannot be read falls back to the forge's sshd.
func TestAnUnreadableHostKeyFileFallsBackToTheScan(t *testing.T) {
r := newRig(t)
r.svc.cfg.HostKeyFile = filepath.Join(t.TempDir(), "missing.pub")
got, err := r.svc.scan(context.Background())
if err != nil || got != "[novox.internal]:222 ssh-ed25519 AAAAC3HOSTKEY" {
t.Fatalf("scan = %q, %v", got, err)
}
}
// While the forge restarts, a key read before is kept: the sshd is not up, the key has not changed.
func TestARestartingForgeKeepsTheKeyReadBefore(t *testing.T) {
r := newRig(t)
r.svc.known = "[novox.internal]:222 ssh-ed25519 AAAAC3HOSTKEY"
r.runner.err = errors.New("connection refused")
got, err := r.svc.scan(context.Background())
if err != nil || got != r.svc.known {
t.Fatalf("scan = %q, %v", got, err)
}
}
+1 -5
View File
@@ -589,11 +589,7 @@ if (gitea) {
};
// **The forge's events first, the polls as the safety net** (novox/hq issue 729): while verified deliveries arrive the
// polls slow to every five minutes and announce only what no event said (Pace); otherwise they keep 30 s.
// **Not awaited** (novox/hq issue 1726): the receiver's set-up asks the forge's admin API, and the runtime kills a bundle
// whose import has not ended in its 10 s handshake and never starts it again; a forge slow to answer while every bundle
// of the node starts left gitea_commit_status, gitea_merge_pull_request and gitea_events served nowhere. Until it is
// listening, Pace keeps the polls at 30 s, so nothing is missed meanwhile; listenForForge says its own failure.
void listenForForge(client);
await listenForForge(client);
// Main takes no push and only the tools' account merges, in every repository, kept and not only when a token is
// minted: at the start and every ten minutes after, so a repository made later gets it too (issue 948, ADR 0398 §2).
// Only while the setting keep-main-rules is true (mainrules.ts); false, the default, leaves every repository's rule as set.
+1
View File
@@ -537,6 +537,7 @@
"MESH_GITEA_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret",
"MESH_GITEA_ORIGINS_FILE": "${dir:mesh-state}/origins",
"MESH_GITEA_SSH_PORT": "222",
"MESH_GITEA_HOST_KEY_FILE": "${dir:data}/ssh/ssh_host_ed25519_key.pub",
"MESH_FORGE_HOST_FILE": "/etc/gitea/forge-host"
}
}
-50
View File
@@ -109,53 +109,3 @@ test("a bundle whose read of the checker's owed is granted late writes the lates
assert.match(got.statuses[0], /"state":"success".*pass at 4/);
assert.match(err, /mesh-delivery-checker\.owed could not be bound/, "the refusal is said");
});
// **The bundle's import does not wait on the forge** (novox/hq issue 1726, 2026-10-24 12:37). The webhook receiver's set-up
// asks the forge's admin API for its system webhook; awaited at the top of index.ts, a forge slow to answer (busy while
// every bundle of the node starts) held the import past node-tools' 10 s handshake, the bundle was killed and never
// started again, and gitea_commit_status, gitea_merge_pull_request and gitea_events were served nowhere: the forge
// bridge's calls found no responders and no new pull request was announced. Here the forge stand-in takes every
// request and never answers: the import resolves at once all the same, with the bundle's tools registered.
test("importing the bundle resolves, its tools registered, while the forge never answers the webhook's set-up", async () => {
const index = pathToFileURL(join(here, "..", "dist", "index.js")).href;
const script = `
const http = await import("node:http");
const fs = await import("node:fs");
const os = await import("node:os");
const path = await import("node:path");
const forge = http.createServer(() => {});
await new Promise((r) => forge.listen(0, "127.0.0.1", r));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "gitea-1726-"));
fs.writeFileSync(path.join(dir, "admin.password"), "pw");
process.env.MESH_GITEA_URL = "http://127.0.0.1:" + forge.address().port;
process.env.MESH_GITEA_STATE_DIR = dir;
process.env.MESH_GITEA_ADMIN_PASSWORD_FILE = path.join(dir, "admin.password");
process.env.MESH_GITEA_HOOK_PORT = "0";
process.env.MESH_GITEA_HOOK_HOST = "127.0.0.1";
const { useBroker } = await import("@novox/mesh-sdk/messaging");
useBroker(() => ({
subscribe: async () => () => {}, publish: async () => {}, request: async () => { throw new Error("no responders"); },
handle: async () => () => {}, close: async () => {},
}));
await import(${JSON.stringify(index)});
const { collectTools } = await import("@novox/mesh-sdk/tools");
console.log(JSON.stringify({ imported: true, tools: collectTools().flatMap((r) => r.tools.map((t) => t.name)) }));
process.exit(0);`;
const env = { ...process.env, MESH_GITEA_TOKEN: "x", MESH_GITEA_ADMIN_USER: "admin", MESH_GITEA_HOOK_URL: "http://127.0.0.1:1" };
for (const k of ["MESH_SERVED_MODULE", "MESH_GITEA_ADMIN_PASSWORD", "MESH_GITEA_CONFIG_FILE"]) delete env[k];
const child = spawn(process.execPath, ["--input-type=module", "-e", script], { cwd: join(here, ".."), env, stdio: ["ignore", "pipe", "pipe"] });
let out = "";
let err = "";
child.stdout.on("data", (d) => (out += d));
child.stderr.on("data", (d) => (err += d));
const ended = new Promise<void>((r) => child.on("exit", () => r()));
const timer = setTimeout(() => child.kill("SIGKILL"), 3000);
await ended;
clearTimeout(timer);
const line = out.split("\n").find((l) => l.startsWith("{"));
assert.ok(line, `the import did not resolve within 3 s while the forge did not answer; stdout: ${out.slice(0, 400)} stderr: ${err.slice(0, 600)}`);
const got = JSON.parse(line!) as { tools: string[] };
for (const t of ["gitea_events", "gitea_commit_status", "gitea_merge_pull_request"]) {
assert.ok(got.tools.includes(t), `${t} is registered: ${got.tools}`);
}
});
@@ -54,9 +54,6 @@ type Holder struct {
seatMu sync.Mutex
seatWhy string
// catchUp asks the next look to weigh again the newest merge of each repository against source.main (SourceCatchUp).
catchUp bool
mu sync.Mutex
deliveries map[string]*Delivery
groups map[string]*Group
@@ -1848,12 +1845,6 @@ func (h *Holder) adopt(w Walk) {
now := h.Now()
d := &Delivery{ID: IDOf(w.Repository, w.Commit), Repository: strings.ToLower(w.Repository), Commit: w.Commit,
Base: w.Branch, MergedAs: w.Commit, Created: now}
// Dated once, by the walk it is adopted from, which follows the merge: a later walk it follows (linkWalk) never redates
// it, so it never looks newer than a merge heard after it (issue 1726, review).
d.MergedAt = w.Created.UTC()
if w.Created.IsZero() {
d.MergedAt = now
}
if old := h.deliveries[d.ID]; old != nil {
if old.State.Final() {
// Never revived: an ended delivery is not the walk's; the walk, held by nothing, is listed by
@@ -1,161 +0,0 @@
package main
import (
"errors"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
)
// Issue 1726 (2026-10-24, mesh-catalog #471 and #472): the checker takes a repository's trunk head only from source.main
// (mesh-delivery modules/mesh-delivery-checker main.go), and the gate moves source.main only on a merge it hears. While
// gitea's watcher was dead, the operator merged #471; the restarted watcher's poll announced its pull.merged while the gate
// was starting, before it could tell whether it held the seat, and the gate acknowledged it unread. The gate then adopted the
// controller's walk of the merge commit, which has no merge time, so every older merge of the repository looked newer and
// source.main was never moved: the checker judged #472 on the trunk before #471, and the gate refused its merge.
const (
i1726Repo = "novox/mesh-catalog"
i1726Head1 = "aaaa000000000000000000000000000000000001"
i1726Merge1 = "dddd000000000000000000000000000000000002"
i1726Merge2 = "bbbb000000000000000000000000000000000003"
)
// 1726c/AC1: a merge into main whose pull.merged the gate never heard, caught up as the controller's walk of its merge
// commit, moves source.main as the forge's word of it would.
func TestAMergeCaughtUpAsAnAdoptedWalkMovesSourceMain(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr(i1726Repo, 470, i1726Head1, "fix/a", "modules/app/a.go"))
w.h.PullMerged(i888MergedAt(i1726Repo, 470, i1726Head1, i1726Merge1, w.now.Add(-time.Hour)))
w.settleAll()
if w.source.main == nil || w.source.main.Heads[i1726Repo] != i1726Merge1 {
t.Fatalf("the heard merge did not move source.main: %+v", w.source.main)
}
// #471's pull.merged is lost; the controller walks its merge commit and the gate adopts the walk.
walk := aWalk("plan-471", i1726Repo, i1726Merge2, false)
walk.Created = w.now.Add(-time.Minute)
walk.State = walkRolling
w.ctl.put(walk)
if err := w.h.Reconcile(); err != nil {
t.Fatal(err)
}
w.settleAll()
if got := w.source.main.Heads[i1726Repo]; got != i1726Merge2 {
t.Fatalf("source.main holds %s at %s after the walk of %s was adopted; wanted the adopted merge", i1726Repo, got, i1726Merge2)
}
}
// 1726c/AC2: a merge already marked in the source without being written (as the gate before this fix left #471) is written
// when the gate next holds the seat; one older than the head source.main holds never moves it back.
func TestHoldingTheSeatWritesTheNewestMergeOfEachRepositoryNotYetInSourceMain(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr(i1726Repo, 470, i1726Head1, "fix/a", "modules/app/a.go"))
w.h.PullMerged(i888MergedAt(i1726Repo, 470, i1726Head1, i1726Merge1, w.now.Add(-time.Hour)))
w.settleAll()
walk := aWalk("plan-471", i1726Repo, i1726Merge2, false)
walk.Created = w.now.Add(-time.Minute)
walk.State = walkRolling
w.ctl.put(walk)
if err := w.h.Reconcile(); err != nil {
t.Fatal(err)
}
w.settleAll()
// What the gate before this fix left: the adopted merge marked in the source, source.main still at the merge before.
if d := w.delivery(IDOf(i1726Repo, i1726Merge2)); !d.InSource {
t.Fatalf("the adopted merge is not marked in the source: %+v", d)
}
w.source.main = &sdk.Source{V: sdk.Version, Group: "before", Heads: map[string]string{i1726Repo: i1726Merge1, "novox/other": "b0"}}
w.settleAll()
if got := w.source.main.Heads[i1726Repo]; got != i1726Merge1 {
t.Fatalf("source.main moved to %s before the gate held the seat again", got)
}
w.h.SourceCatchUp()
w.settleAll()
if got := w.source.main.Heads[i1726Repo]; got != i1726Merge2 || w.source.main.Heads["novox/other"] != "b0" {
t.Fatalf("source.main is %+v after the catch-up; wanted %s at %s and the other kept", w.source.main.Heads, i1726Repo, i1726Merge2)
}
n := len(w.source.moves)
w.h.SourceCatchUp()
w.settleAll()
if len(w.source.moves) != n {
t.Fatalf("a second catch-up moved source.main again: %d moves, then %d", n, len(w.source.moves))
}
o := newWorld(t)
o.source.main = &sdk.Source{V: sdk.Version, Group: "newer", Heads: map[string]string{i1726Repo: i1726Merge2}}
o.h.PullUpdated(pr(i1726Repo, 471, "cccc000000000000000000000000000000000001", "fix/b", "modules/app/b.go"))
o.h.PullMerged(i888MergedAt(i1726Repo, 471, "cccc000000000000000000000000000000000001", i1726Merge2, o.now.Add(-time.Minute)))
o.h.PullMerged(i888MergedAt(i1726Repo, 470, i1726Head1, i1726Merge1, o.now.Add(-time.Hour)))
o.settleAll()
o.h.SourceCatchUp()
o.settleAll()
if got := o.source.main.Heads[i1726Repo]; got != i1726Merge2 {
t.Fatalf("the catch-up moved source.main back to %s", got)
}
}
// seatUnknown is a seat whose occupants cannot be read yet.
type seatUnknown struct{}
func (seatUnknown) Occupants() ([]string, error) {
return nil, errors.New("the watches have not delivered yet")
}
// seatTaken is a seat today's module still stands on.
type seatTaken struct{}
func (seatTaken) Occupants() ([]string, error) { return []string{"assigned on elsewhere"}, nil }
// 1726c/AC3: an event that reaches the gate before it can tell whether it holds the seat is offered again, never
// acknowledged unread; one that reaches a gate whose seat another holds is acknowledged and left, as before.
func TestAnEventBeforeTheGateKnowsWhetherItHoldsTheSeatIsOfferedAgain(t *testing.T) {
w := newWorld(t)
w.h.Seat = seatUnknown{}
_ = w.h.SeatChanged()
ev := stdio.Envelope{Key: "gitea.pull.merged", Body: mustJSON(i888MergedAt(i1726Repo, 471, i1726Head1, i1726Merge2, w.now))}
if err := handle(w.h, ev); err == nil {
t.Fatal("an event heard before the gate could tell whether it holds the seat was taken: acknowledged unread")
}
w.h.Seat = seatTaken{}
_ = w.h.SeatChanged()
if err := handle(w.h, ev); err != nil {
t.Fatalf("an event heard while today's module holds the seat was offered again: %v", err)
}
}
// 1726c/AC4 (review round 1): an adopted merge that later follows a newer walk keeps the date of the walk it was adopted
// from, so a merge heard from the forge after it stays in source.main through a catch-up.
func TestAnAdoptedMergeFollowingANewerWalkNeverMovesSourceMainBack(t *testing.T) {
w := newWorld(t)
walk := aWalk("plan-471", i1726Repo, i1726Merge2, false)
walk.Created = w.now.Add(-10 * time.Minute)
walk.State = walkRolling
w.ctl.put(walk)
if err := w.h.Reconcile(); err != nil {
t.Fatal(err)
}
w.settleAll()
if got := w.source.main.Heads[i1726Repo]; got != i1726Merge2 {
t.Fatalf("the adopted merge did not move source.main: %s", got)
}
const later = "eeee000000000000000000000000000000000004"
w.h.PullUpdated(pr(i1726Repo, 473, "ffff000000000000000000000000000000000001", "fix/c", "modules/app/c.go"))
w.h.PullMerged(i888MergedAt(i1726Repo, 473, "ffff000000000000000000000000000000000001", later, w.now.Add(-5*time.Minute)))
w.settleAll()
if got := w.source.main.Heads[i1726Repo]; got != later {
t.Fatalf("the later forge merge did not move source.main: %s", got)
}
again := aWalk("plan-471-again", i1726Repo, i1726Merge2, false)
again.Created = w.now.Add(-time.Minute)
again.State = walkRolling
w.h.mu.Lock()
w.h.linkWalk(w.h.deliveries[IDOf(i1726Repo, i1726Merge2)], again, i1726Merge2)
w.h.mu.Unlock()
w.h.SourceCatchUp()
w.settleAll()
if got := w.source.main.Heads[i1726Repo]; got != later {
t.Fatalf("the catch-up moved source.main back to the adopted merge %s after it followed a newer walk", got)
}
}
@@ -147,7 +147,6 @@ func run(h *Holder, l *listening, occupancy *busOccupancy) {
func readOnHolding(h *Holder) {
h.wantForge()
h.hearAsks()
h.SourceCatchUp() // a merge missed or marked unwritten meanwhile reaches source.main, and so the checker (issue 1726)
h.Poke()
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
err := h.Reconcile()
@@ -163,11 +162,6 @@ func readOnHolding(h *Holder) {
// handle takes one event, then asks for a look at every step the event makes possible (Work, React). An event that cannot be read is
// said and acknowledged: read again it would fail again, and the controller's walks are read back by comparison anyway.
func handle(h *Holder, e stdio.Envelope) error {
if h.Undecided() {
// Not known yet whether this gate holds the seat (starting, or carrying today's state): offered again in a few
// seconds, so an event heard at a restart is not lost (issue 1726).
return fmt.Errorf("%s: the gate cannot tell yet whether it holds the seat mesh-delivery; offered again", e.Key)
}
if !h.Holding() {
return nil // not the holder of the seat: the event is acknowledged and taken by no one (seat.go)
}
@@ -433,20 +433,10 @@ func gateMerges(d *Delivery) bool {
// and the checker judges on the trunk source.main names, so source.main moves too. A merge replayed as history, or older than
// a merge of its repository already heard, is never written: source.main never moves backwards. Under the lock; the writing
// happens outside it.
//
// A merge caught up after a gap (issue 1726) is settled the same way: the controller's walk of a merge commit whose
// pull.merged the gate never heard is adopted with the walk's time as its merge's (adopt, mergedWhen), and once each time the
// gate comes to hold the seat (SourceCatchUp) the newest merge of each repository it holds is weighed again though it is
// marked in the source, so a merge marked there and never written reaches source.main, and with it the checker.
func (h *Holder) settleForgeMerges() []func() {
var out []func()
again := map[*Delivery]bool{}
if h.catchUp {
h.catchUp = false
again = h.newestInSource()
}
for _, d := range h.deliveries {
if d.MergedAs == "" || (d.InSource && !again[d]) || gateMerges(d) {
if d.MergedAs == "" || d.InSource || gateMerges(d) {
continue
}
if d.Base != MergeBranch || d.History != "" || h.newerMergeOf(d) {
@@ -454,7 +444,7 @@ func (h *Holder) settleForgeMerges() []func() {
h.keep(d)
continue
}
id, repository, base, commit, at, group := d.ID, d.Repository, d.Base, d.MergedAs, mergedWhen(d), firstOf(d.Branch, d.Repository)
id, repository, base, commit, at, group := d.ID, d.Repository, d.Base, d.MergedAs, d.MergedAt, firstOf(d.Branch, d.Repository)
out = append(out, func() {
err := h.Source.Move(func(prev *sdk.Source) (sdk.Source, bool) {
next := sdk.Source{Group: group, Heads: map[string]string{repository: commit}}
@@ -495,7 +485,7 @@ func (h *Holder) settleForgeMerges() []func() {
// (Ready.MergeCommit, Ready.Merged), which comes first for a merge the gate handed. Empty when it is not merged.
func mergeOf(d *Delivery) (string, time.Time) {
if d.MergedAs != "" {
return d.MergedAs, mergedWhen(d)
return d.MergedAs, d.MergedAt
}
if d.Ready != nil && d.Ready.Merged != nil && d.Ready.MergeCommit != "" {
return d.Ready.MergeCommit, *d.Ready.Merged
@@ -507,7 +497,7 @@ func mergeOf(d *Delivery) (string, time.Time) {
func (h *Holder) newerMergeOf(d *Delivery) bool {
for _, o := range h.deliveries {
if commit, at := mergeOf(o); o != d && o.Repository == d.Repository && o.Base == d.Base && commit != "" &&
!sameCommit(commit, d.MergedAs) && at.After(mergedWhen(d)) {
!sameCommit(commit, d.MergedAs) && at.After(d.MergedAt) {
return true
}
}
@@ -528,44 +518,3 @@ func (h *Holder) olderHead(repository, base, head string, at time.Time) bool {
}
return at.After(h.Now().Add(-keptFinal))
}
// mergedWhen is when a merged delivery's merge landed: its merge time (the forge's, or for a merge adopted from the
// controller's walk the walk's creation, set once in adopt), else when the gate made the delivery. A zero time made every
// other merge of the repository look newer, so an adopted merge never moved source.main (issue 1726).
func mergedWhen(d *Delivery) time.Time {
if !d.MergedAt.IsZero() {
return d.MergedAt
}
return d.Created
}
// SourceCatchUp asks the next look to weigh again, once, the newest merge into the trunk of each repository though it is
// marked in the source (issue 1726): what the gate missed while it did not hold the seat, or marked without writing, reaches
// source.main. The write never moves a repository back (olderHead), and one already there is not written again.
func (h *Holder) SourceCatchUp() {
h.mu.Lock()
h.catchUp = true
h.mu.Unlock()
h.Poke()
}
// newestInSource is, for each repository, its newest merge into the trunk heard from the forge or caught up from a walk,
// marked in the source and not history. Under the lock.
func (h *Holder) newestInSource() map[*Delivery]bool {
newest := map[string]*Delivery{}
for _, d := range h.deliveries {
if d.MergedAs == "" || !d.InSource || gateMerges(d) || d.Base != MergeBranch || d.History != "" {
continue
}
if o := newest[d.Repository]; o == nil || mergedWhen(d).After(mergedWhen(o)) {
newest[d.Repository] = d
}
}
out := map[*Delivery]bool{}
for _, d := range newest {
if !h.newerMergeOf(d) {
out[d] = true
}
}
return out
}
@@ -105,11 +105,6 @@ func (h *Holder) SeatChanged() error {
// Holding is whether the gate acts. A holder with no Seat to ask always holds (the tests'; main sets one).
func (h *Holder) Holding() bool { return h.Seat == nil || h.seatHeld.Load() }
// Undecided is whether the gate cannot tell yet whether it holds the seat: it does not hold it, and no reason it waits was
// found (the watches have not delivered, or today's state is being carried). An event heard then is offered again, never
// acknowledged unread (issue 1726: a merge announced at a restart was lost so, and source.main never moved).
func (h *Holder) Undecided() bool { return !h.Holding() && h.SeatWhy() == "" }
// setSeatWhy keeps why the gate waits and says whether it changed.
func (h *Holder) setSeatWhy(why string) bool {
h.seatMu.Lock()