Repository navigation
Description
Finding
/members lists ExternalDNS twice in its "Filter by CNCF project"
dropdown — once as ExternalDNS and once as External-dns — and filtering
by ExternalDNS silently omits SNCF.
This is the defect PR #1991 ("list ExternalDNS once in the catalog project
filter", e2f5f26, 2026-10-29) fixed for /architectures. That PR normalised
data/architectures/catalog.json, but that file is an input to
scripts/generate-members.mjs, and the dependent artifact
data/members.json was not regenerated in the same change. The member
directory still carries the pre-#1991 spelling.
Observed on main (cbffa80) from the committed data these pages render:
node -e "const m=require('./data/members.json').members;
const s=new Set();m.forEach(x=>x.projects.forEach(p=>s.add(p)));
console.log('External-dns:',s.has('External-dns'),'ExternalDNS:',s.has('ExternalDNS'));
console.log('ExternalDNS ->',m.filter(x=>x.projects.includes('ExternalDNS')).map(x=>x.name));
console.log('External-dns ->',m.filter(x=>x.projects.includes('External-dns')).map(x=>x.name));"External-dns: true ExternalDNS: true
ExternalDNS -> [ 'Swisscom' ]
External-dns -> [ 'SNCF' ]
while data/architectures/catalog.json spells it ExternalDNS for both
sncf-onprem and swisscom-cloud-native-telco.
What a visitor sees:
| Page | Symptom |
|---|---|
/members → Filter by CNCF project |
two options for one project: ExternalDNS and External-dns |
/members → select ExternalDNS |
1 organization (Swisscom); SNCF is missing |
/members → SNCF card and profile |
project tag reads External-dns |
/architectures/sncf-onprem |
the same project reads ExternalDNS |
External-dns is also not the project's published name, so the two pages
disagree about what SNCF runs.
Root cause
data/members.json is generated from data/architectures/catalog.json and
data/awards.json, but nothing reconciles the committed artifact against
its committed inputs. Regenerating on main today is not a no-op:
npm run generate:membersproduces exactly one content change (everything else in the diff is Prettier
formatting, which the refresh lane applies):
sncf projects
committed: [… "External Secrets Operator", "External-dns", "Harbor" …]
regenerated: [… "External Secrets Operator", "ExternalDNS", "Harbor" …]
tests/generate-members.test.mjs exercises the generator against synthetic
fixtures, and tests/members-data.test.mjs pins only the member ordering
("generate-members.mjs emits members sorted by name; regenerate the file").
Neither compares the committed data/members.json with the committed
catalog, so a PR that edits the catalog without regenerating passes every
required check — which is what happened here.
The weekly Refresh end user members lane (47 3 * * 2) will overwrite the
stale value on its next run, but the duplicate dropdown option is live until
then, and the lane is not the gate: the same half-update will recur on the
next catalog edit.
Recommendation
One PR, two parts:
-
Refresh the artifact through its existing lane — run
npm run generate:membersandnpm run format(or trigger
.github/workflows/refresh-members.ymlviaworkflow_dispatch) and commit
the resultingdata/members.json. -
Add the missing reconciliation guard so this cannot recur, in
tests/members-data.test.mjsbeside the existing ordering test:test('member projects and industries match the committed catalog', () => { const catalog = readJson('data/architectures/catalog.json'); const bySlug = new Map(); for (const entry of catalog) { const list = bySlug.get(entry.slug) || []; list.push(entry); bySlug.set(entry.slug, list); } for (const member of members) { const entries = bySlug.get(member.slug) || []; if (entries.length === 0) continue; for (const field of ['projects', 'industries']) { const expected = [ ...new Set(entries.flatMap((entry) => entry[field] || [])), ].sort(); assert.deepEqual( [...member[field]].sort(), expected, `${member.id} ${field} drifted from data/architectures/catalog.json; run \`npm run generate:members\``, ); } } });
Run against
maintoday this fails with
sncf projects drifted from data/architectures/catalog.json, i.e. it
reproduces the defect; it passes once part 1 lands. A member with no
catalog entry (awards-only) is skipped, matching the generator, and the
comparison is order-insensitive so it stays independent of the separate
option-ordering question in [quality] Catalog filter dropdowns list lowercase project and industry names after every capitalised one #2013.Without this guard, the behaviour that regresses undetected is the one in
the table above: adata/architectures/catalog.jsonedit that does not
regeneratedata/members.jsonleaves/membersshowing a stale project
name and a duplicate filter option, and no required check notices.
Who can land this
Part 1 changes generated data and part 2 is test-only; the two must land
together or CI is red either way, so this is not split into two issues.
The quality lane opens PRs for tests/ only, so it cannot land part 1 —
this needs a human or a lane that may commit regenerated data. The commands
and the replacement text above are complete, so applying it is mechanical.
Priority
- Impact: medium — user-observable on
/members: a duplicated filter option,
a filter that omits a matching organization, and a project name that
contradicts/architectures/sncf-onprem. - Effort: low — one command plus one test.
Activity