Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion scripts/validate-members.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -417,9 +417,13 @@ if (landscape) {
message: `membership source ID set mismatch (missing: ${missing.join(', ') || 'none'}; unexpected: ${unexpected.join(', ') || 'none'})`,
});
}
// collectedAt is the one field of this block the site renders
// (DirectoryFreshness), and the generator copies it from the snapshot root
// rather than from snapshot.source like the other two.
if (
data.sources?.landscape?.revision !== landscape.source?.revision ||
data.sources?.landscape?.sourceUrl !== landscape.source?.sourceUrl
data.sources?.landscape?.sourceUrl !== landscape.source?.sourceUrl ||
data.sources?.landscape?.collectedAt !== landscape.collectedAt
) {
errors.push({
path: 'members.json.sources.landscape',
Expand Down
11 changes: 4 additions & 7 deletions tests/e2e-data-fixture-validity.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,10 @@ const EXPECTED = new Map([
[
'data-variant/members.json',
{
// Recorded as an empty delta because that is what the gate does today,
// not because the shape is benign: DirectoryFreshness renders
// sources.landscape.collectedAt and validate-members.mjs never reads it,
// unlike every sibling validator's document-level timestamp. If that gap
// is closed this entry gains the new error, which is the signal.
why: 'makes sources.landscape.collectedAt unparseable to degrade the DirectoryFreshness note; validate-members.mjs does not check that field',
adds: [],
why: 'makes sources.landscape.collectedAt unparseable to degrade the DirectoryFreshness note; validate-members.mjs holds that field to the snapshot like revision and sourceUrl',
adds: [
'members.json.sources.landscape: landscape provenance must match enduser-landscape.json',
],
},
],
[
Expand Down
31 changes: 29 additions & 2 deletions tests/validate-members-membership.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ function member(id, membershipStatus, membershipSources = []) {
};
}

const COLLECTED_AT = '2026-10-01T00:00:00.000Z';

function snapshot(sourceIds) {
return {
generated: true,
collectedAt: '2026-10-01T00:00:00.000Z',
collectedAt: COLLECTED_AT,
source: {
repository: 'https://github.com/cncf/landscape',
revision: REVISION,
Expand All @@ -54,7 +56,7 @@ function snapshot(sourceIds) {
};
}

function run(members, landscape) {
function run(members, landscape, provenance = {}) {
return runScriptWithFixtures(SCRIPT, {
'data/members.json': JSON.stringify({
description: 'directory',
Expand All @@ -64,6 +66,8 @@ function run(members, landscape) {
repository: 'https://github.com/cncf/landscape',
revision: REVISION,
sourceUrl: SOURCE_URL,
collectedAt: COLLECTED_AT,
...provenance,
},
},
schema: {},
Expand Down Expand Up @@ -172,6 +176,29 @@ test('rejects duplicate, missing, unexpected, and stale landscape provenance', (
assert.match(result.stderr, /landscape provenance must match/);
});

// The generator writes revision, sourceUrl and collectedAt into
// sources.landscape from one snapshot in one statement, and collectedAt is
// the only one of the three DirectoryFreshness renders. A members.json that
// kept the snapshot's revision but a different sync time (a partial
// regeneration, a merge that took one side) states a date the data does not
// support, so it is held to the snapshot like the other two.
test('rejects a collectedAt that differs from the snapshot it claims', () => {
const memberSource = source('member-source', 'member');
const members = [member('one', 'member', [memberSource])];
const landscape = snapshot(['member-source']);
for (const collectedAt of [
'1999-01-01T00:00:00.000Z',
'not a timestamp',
null,
undefined,
]) {
const result = run(members, landscape, { collectedAt });
assert.equal(result.status, 1, `collectedAt ${String(collectedAt)}`);
assert.match(result.stderr, /landscape provenance must match/);
}
assert.equal(run(members, landscape).status, 0);
});

test('requires membership fields for modern landscape-backed output', () => {
const legacyShape = member('legacy-shape', 'unknown');
delete legacyShape.membershipStatus;
Expand Down
Loading