diff --git a/modules/mesh-config/cmd/mesh-config/takeover.go b/modules/mesh-config/cmd/mesh-config/takeover.go index dad88461..fbaba94d 100644 --- a/modules/mesh-config/cmd/mesh-config/takeover.go +++ b/modules/mesh-config/cmd/mesh-config/takeover.go @@ -350,6 +350,17 @@ func (c *Config) compare() (*Comparison, error) { c.mirrorDeclarations(&mirror) var diffs []Difference + // A placed module no catalog manifest names (mesh-wireguard, which the mesh ships computed) whose mirror key holds + // no config key declares nothing on both sides: nothing resolves from it, so nothing differs (issue 1953, L5d). + declaresNothing := map[string]bool{} + for module, d := range mirror.Declared { + if len(d.Config) == 0 { + if _, err := c.trunkDeclarations(module); err != nil { + declaresNothing[module] = true + } + } + } + // Each placed module's declarations: `declared` against the mirror's key for it. for _, module := range modulesOf(own.Assignments) { var ownRaw []byte @@ -364,6 +375,7 @@ func (c *Config) compare() (*Comparison, error) { mirrorRaw, _ = json.Marshal(normal(d.Config)) } switch { + case ownRaw == nil && declaresNothing[module]: case ownRaw == nil && mirrorRaw == nil: diffs = append(diffs, Difference{What: DiffDeclared, Module: module, Why: "neither side holds its declarations"}) case ownRaw == nil: @@ -412,9 +424,13 @@ func (c *Config) compare() (*Comparison, error) { if cerr != nil { diffs = append(diffs, Difference{What: DiffPull, Why: "the indexer's config candidates cannot be read: " + cerr.Error()}) } + settled := map[string]string{} // a pull request the forge says is no longer open → how it ended for _, k := range unionKeys(cands, own.Pulls) { a, aok := cands[k] b, bok := own.Pulls[k] + if c.pullSettled(firstNonEmpty(a.Pull, b.Pull), settled) { + continue // a merged or closed pull request is no candidate to agree on (the operator, 2026-11-01) + } switch { case !aok: diffs = append(diffs, Difference{What: DiffPull, Pull: b.Pull, Why: "the controller told its declarations and the indexer holds no candidate"}) @@ -456,6 +472,9 @@ func (c *Config) compare() (*Comparison, error) { diffs = append(diffs, Difference{What: DiffValue, Node: u.Node, Module: u.Module, Why: "not resolved from the mirror: " + u.Why}) } for _, u := range own.Unresolved { + if declaresNothing[u.Module] { + continue + } diffs = append(diffs, Difference{What: DiffValue, Node: u.Node, Module: u.Module, Why: "not resolved from its own source: " + u.Why}) } sort.SliceStable(diffs, func(i, j int) bool { @@ -474,6 +493,41 @@ func (c *Config) compare() (*Comparison, error) { return cmp, nil } +func firstNonEmpty(a, b string) string { + if a != "" { + return a + } + return b +} + +// pullSettled is whether the forge says a pull request merged or closed. Without a forge, or when it does not say, the +// pull request counts as open, so its row stays (failure is loud). +func (c *Config) pullSettled(pull string, memo map[string]string) bool { + if c.Forge == nil || pull == "" { + return false + } + if v, ok := memo[pull]; ok { + return v != "" + } + owner, repo, n, err := parsePull(pull) + if err != nil { + return false + } + open, merged, _, err := c.Forge.Pull(owner, repo, n) + switch { + case err != nil: + c.logf("[mesh-config] compare: the forge does not say whether %s is open (%v); its row stays", pull, err) + memo[pull] = "" + case open: + memo[pull] = "" + case merged: + memo[pull] = "merged" + default: + memo[pull] = "closed" + } + return memo[pull] != "" +} + // normal is declarations in one encoding: a null default as none. func normal(d map[string]Declaration) map[string]Declaration { out := map[string]Declaration{} diff --git a/modules/mesh-config/cmd/mesh-config/takeover_test.go b/modules/mesh-config/cmd/mesh-config/takeover_test.go index c66b21f8..e4f9cdd0 100644 --- a/modules/mesh-config/cmd/mesh-config/takeover_test.go +++ b/modules/mesh-config/cmd/mesh-config/takeover_test.go @@ -88,9 +88,18 @@ func TestCompareListsEveryDifferenceAndTheTakeOverWaitsForNone(t *testing.T) { {"a pull request only the indexer holds", func(t *testing.T, m *memStore) { candidate(t, m, "novox/mesh-catalog#9", "h1", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`) }, []string{"pull:novox/mesh-catalog#9"}}, + {"a candidate of a merged pull request is no difference", func(t *testing.T, m *memStore) { + candidate(t, m, "novox/mesh-catalog#11", "h1", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`) + candidate(t, m, "novox/mesh-catalog#10", "h2", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`) + }, []string{"pull:novox/mesh-catalog#10"}}, + {"a module no manifest names and the mirror declares nothing of", func(t *testing.T, m *memStore) { + m.assign(t, "g14", "mesh-wireguard") + m.mirror(t, MirrorDeclaredRead, "mesh-wireguard", `{"module":"mesh-wireguard","settings":{}}`) + }, nil}, } for _, tc := range cases { c, m, r := anAgreeingMesh(t) + c.Forge = &standForge{state: map[string][2]bool{"novox/mesh-catalog#11": {false, true}}} tc.change(t, m) cmp, err := c.Compare() if err != nil {