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
7 changes: 7 additions & 0 deletions docs/site/docs/evidence-link-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ The checker reads only local files and Git objects. It verifies that:
- every linked repository path exists at that exact tag or commit
- every root-relative documentation route exists at the selected source commit

The exact source tag of the newest validated release manifest is the only

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the required DCO sign-off

The reviewed commit message has no Signed-off-by: trailer, so it violates the repository's mandatory DCO policy and will be rejected by the DCO gate; recreate the commit with git commit -s.

AGENTS.md reference: AGENTS.md:L274-L274

Useful? React with 👍 / 👎.

pre-publication exception. While that tag does not exist, repository evidence
at the future tag is checked against the selected source commit. This lets
protected pre-tag CI verify the source that will receive the immutable tag
without creating the tag early. Any other missing tag still fails. Once the
release tag exists, the checker resolves and verifies that tag directly.

The release workflow passes its resolved tag commit with `--source-ref`, so
current-documentation routes are verified against the same source that the
release uses. The checker has no network fallback. A shallow or incomplete
Expand Down
41 changes: 37 additions & 4 deletions docs/site/scripts/check-evidence-links.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,27 @@ function gitObjectExists(repoRoot, object, gitCommand) {
);
}

export function candidateTagFromValidationOutput(output) {
const match = /^validated .+: [A-Za-z0-9][A-Za-z0-9._-]{0,63} (\d+\.\d+\.\d+)\s*$/.exec(
output,
);
return match ? `v${match[1]}` : undefined;
}

function currentReleaseCandidateTag(repoRoot) {
const validator = resolve(repoRoot, 'release/scripts/registry-release');
const result = spawnSync(validator, ['validate-current'], {
cwd: repoRoot,
encoding: 'utf8',
env: { ...process.env, GIT_NO_LAZY_FETCH: '1' },
stdio: 'pipe',
});
if (result.status !== 0) {
return undefined;
}
return candidateTagFromValidationOutput(result.stdout);
}

function safePathParts(parts) {
try {
return parts.map((part) => decodeURIComponent(part));
Expand All @@ -148,7 +169,12 @@ function validRepositoryPath(parts) {
);
}

function checkRepositoryEvidence(repoRoot, rawUrl, gitCommand) {
function checkRepositoryEvidence(
repoRoot,
rawUrl,
gitCommand,
{ candidateTag, sourceRef } = {},
) {
let url;
try {
url = new URL(rawUrl);
Expand Down Expand Up @@ -189,7 +215,10 @@ function checkRepositoryEvidence(repoRoot, rawUrl, gitCommand) {
}

if (!gitObjectExists(repoRoot, `${commitish}^{commit}`, gitCommand)) {
return `references missing Git commit or tag ${ref}`;
if (ref !== candidateTag || !gitObjectExists(repoRoot, `${sourceRef}^{commit}`, gitCommand)) {
return `references missing Git commit or tag ${ref}`;
}
commitish = sourceRef;
Comment on lines 217 to +221

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Distinguish a missing tag from a non-commit tag

When the candidate ref exists but cannot peel to a commit, such as when refs/tags/v9.9.9 points to a blob or its target commit is absent from an incomplete checkout, this lookup fails and the branch silently substitutes sourceRef. I reproduced this with a blob tag: the check accepted evidence at v9.9.9 against HEAD even though that tag already existed, leaving the claim unanchored at its stated tag; check whether the tag ref itself exists separately and reject an existing tag that cannot resolve to a commit.

AGENTS.md reference: docs/site/AGENTS.md:L25-L28

Useful? React with 👍 / 👎.

}
const path = repositoryPath.join('/');
if (!gitObjectExists(repoRoot, `${commitish}^{commit}:${path}`, gitCommand)) {
Expand Down Expand Up @@ -244,6 +273,7 @@ export function checkEvidenceLinks({
dataDir = resolve(scriptDir, '../src/data'),
sourceRef = 'HEAD',
gitCommand = 'git',
candidateTag,
} = {}) {
const errors = [];
let evidence;
Expand All @@ -256,7 +286,7 @@ export function checkEvidenceLinks({
for (const item of evidence) {
const error = item.url.startsWith('/')
? checkCurrentDocsEvidence(repoRoot, sourceRef, item.url, gitCommand)
: checkRepositoryEvidence(repoRoot, item.url, gitCommand);
: checkRepositoryEvidence(repoRoot, item.url, gitCommand, { candidateTag, sourceRef });
if (error) {
errors.push(`${item.location}: ${item.url}: ${error}`);
}
Expand All @@ -276,7 +306,10 @@ function sourceRefArgument(args) {

if (process.argv[1] && resolve(process.argv[1]) === scriptPath) {
try {
const result = checkEvidenceLinks({ sourceRef: sourceRefArgument(process.argv.slice(2)) });
const repoRoot = resolve(scriptDir, '../../..');
const sourceRef = sourceRefArgument(process.argv.slice(2));
const candidateTag = currentReleaseCandidateTag(repoRoot);
const result = checkEvidenceLinks({ repoRoot, sourceRef, candidateTag });
if (result.errors.length > 0) {
console.error('Evidence link check failed:');
for (const error of result.errors) {
Expand Down
52 changes: 51 additions & 1 deletion docs/site/scripts/check-evidence-links.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { test } from 'node:test';

import { checkEvidenceLinks, extractEvidenceUrlsFromYaml } from './check-evidence-links.mjs';
import {
candidateTagFromValidationOutput,
checkEvidenceLinks,
extractEvidenceUrlsFromYaml,
} from './check-evidence-links.mjs';

const here = dirname(fileURLToPath(import.meta.url));
const repositoryRoot = resolve(here, '../../..');
Expand Down Expand Up @@ -98,6 +102,52 @@ test('accepts semver tags, full commits, and root-relative current docs', (t) =>
});
});

test('reads the candidate tag only from validated current-manifest output', () => {
assert.equal(
candidateTagFromValidationOutput(
'validated /tmp/registry-stack-beta-30.yaml: beta-30 0.20.0\n',
),
'v0.20.0',
);
assert.equal(candidateTagFromValidationOutput('error: validation failed\n'), undefined);
});

test('checks the exact unpublished release tag against the selected source', (t) => {
const { root, commit } = createRepository(t);
const dataDir = writeEvidenceData(root, {
contractUrls: [
'https://github.com/registrystack/registry-stack/blob/v9.9.9/source/file.md',
],
});

assert.deepEqual(
checkEvidenceLinks({
repoRoot: root,
dataDir,
sourceRef: commit,
candidateTag: 'v9.9.9',
}),
{ checked: 1, errors: [] },
);
});

test('does not substitute the selected source for another missing tag', (t) => {
const { root, commit } = createRepository(t);
const dataDir = writeEvidenceData(root, {
contractUrls: [
'https://github.com/registrystack/registry-stack/blob/v9.9.8/source/file.md',
],
});

const result = checkEvidenceLinks({
repoRoot: root,
dataDir,
sourceRef: commit,
candidateTag: 'v9.9.9',
});
assert.match(result.errors[0], /missing Git commit or tag v9\.9\.8/);
});

test('rejects branches, short commits, missing refs, and missing paths', async (t) => {
const { root, commit } = createRepository(t);
const cases = [
Expand Down
Loading