fix(fleet): correct operator documentation validation
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -49,23 +49,157 @@ async function markdownFiles(root: string): Promise<string[]> {
|
||||
}
|
||||
|
||||
function localMarkdownTargets(source: string): string[] {
|
||||
return [...source.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)]
|
||||
.map((match): string => match[1] ?? '')
|
||||
const link =
|
||||
/\[[^\]]*\]\(\s*(?:<([^>]+)>|((?:\\.|[^()\s]|\([^()]*\))+))(?:\s+(?:"[^"]*"|'[^']*'|\([^)]*\)))?\s*\)/g;
|
||||
return [...source.matchAll(link)]
|
||||
.map((match): string => match[1] ?? match[2] ?? '')
|
||||
.filter(
|
||||
(target): boolean =>
|
||||
target !== '' &&
|
||||
!target.startsWith('#') &&
|
||||
!target.startsWith('http://') &&
|
||||
!target.startsWith('https://') &&
|
||||
!target.startsWith('mailto:'),
|
||||
)
|
||||
.map((target): string => decodeURIComponent(target.split('#', 1)[0] ?? ''));
|
||||
);
|
||||
}
|
||||
|
||||
function markdownHeadingAnchors(source: string): Set<string> {
|
||||
const anchors = new Set<string>();
|
||||
let fence: { readonly marker: string; readonly length: number } | undefined;
|
||||
|
||||
for (const line of source.split('\n')) {
|
||||
const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
|
||||
if (fence === undefined && fenceMatch !== null) {
|
||||
const run = fenceMatch[1] ?? '';
|
||||
fence = { marker: run[0] ?? '', length: run.length };
|
||||
continue;
|
||||
}
|
||||
if (fence !== undefined) {
|
||||
const closingRun = line.match(/^\s{0,3}(`{3,}|~{3,})\s*$/)?.[1];
|
||||
if (
|
||||
closingRun !== undefined &&
|
||||
closingRun[0] === fence.marker &&
|
||||
closingRun.length >= fence.length
|
||||
) {
|
||||
fence = undefined;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const heading = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/)?.[1];
|
||||
if (heading === undefined) continue;
|
||||
const base = heading
|
||||
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/[`*_~]/g, '')
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}\s-]/gu, '')
|
||||
.replace(/\s+/g, '-');
|
||||
let anchor = base;
|
||||
let duplicate = 0;
|
||||
while (anchors.has(anchor)) {
|
||||
duplicate += 1;
|
||||
anchor = `${base}-${duplicate}`;
|
||||
}
|
||||
anchors.add(anchor);
|
||||
}
|
||||
return anchors;
|
||||
}
|
||||
|
||||
function markdownLinkViolations(
|
||||
sourcePath: string,
|
||||
source: string,
|
||||
documents: Readonly<Record<string, string>>,
|
||||
): string[] {
|
||||
const violations: string[] = [];
|
||||
for (const target of localMarkdownTargets(source)) {
|
||||
const [encodedPath = '', encodedFragment] = target.split('#', 2);
|
||||
const targetPath = decodeURIComponent(encodedPath);
|
||||
const normalizedTarget = resolve('/', dirname(sourcePath), targetPath).slice(1);
|
||||
const targetSource = documents[normalizedTarget];
|
||||
if (targetSource === undefined) {
|
||||
violations.push(`${sourcePath} -> ${target}: missing file`);
|
||||
continue;
|
||||
}
|
||||
if (encodedFragment !== undefined) {
|
||||
const fragment = decodeURIComponent(encodedFragment);
|
||||
if (fragment === '' || !markdownHeadingAnchors(targetSource).has(fragment)) {
|
||||
violations.push(`${sourcePath} -> ${target}: missing heading`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
function exampleSafetyViolationKinds(source: string): string[] {
|
||||
const kinds = new Set<string>();
|
||||
const sensitiveKey = /(?:secret|token|password|credential|MOSAIC_AGENT_COMMAND)/i;
|
||||
const credentialFormat =
|
||||
/(?:\bAKIA[0-9A-Z]{16}\b|\bAIza[0-9A-Za-z_-]{35}\b|\bgh[pousr]_[A-Za-z0-9]{20,}\b|\bgithub_pat_[A-Za-z0-9_]{20,}\b|\bglpat-[A-Za-z0-9_-]{20,}\b|\bnpm_[A-Za-z0-9]{20,}\b|\bsk_live_[A-Za-z0-9]{16,}\b|\bxox[baprs]-[A-Za-z0-9-]{10,}\b|\bBearer\s+[A-Za-z0-9._~+/=-]{16,}\b|\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b|-----BEGIN [A-Z ]*PRIVATE KEY-----|\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s/:]+:[^\s/@]+@)/;
|
||||
const privilegedCommand =
|
||||
/(?:^|[\n;&|])\s*(?:[$#>]\s*)?(?:env\s+(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+\s+)*|command\s+)*(?:(?:sudo|doas|pkexec)\s+|su\s+(?:-|--command\b|-c\b)|(?:systemctl|service|mount|umount|reboot|shutdown|poweroff|halt|chown|chmod|useradd|usermod|groupadd|visudo)\s+)/m;
|
||||
|
||||
if (sensitiveKey.test(source)) kinds.add('sensitive-key');
|
||||
if (credentialFormat.test(source)) kinds.add('credential-format');
|
||||
if (privilegedCommand.test(source)) kinds.add('privileged-command');
|
||||
if (/\b(?:Tess|Ultron)\b/.test(source)) kinds.add('identity');
|
||||
return [...kinds].sort();
|
||||
}
|
||||
|
||||
function fencedCodeBlocks(source: string): string[] {
|
||||
return [...source.matchAll(/```[^\n]*\n(.*?)```/gs)].map((match): string => match[1] ?? '');
|
||||
}
|
||||
|
||||
const UNSAFE_EXAMPLE_FIXTURES = [
|
||||
{
|
||||
expected: 'privileged-command',
|
||||
source: ['cd /srv && su', 'do systemctl restart example'].join(''),
|
||||
},
|
||||
{ expected: 'credential-format', source: ['ghp_', 'a'.repeat(36)].join('') },
|
||||
{
|
||||
expected: 'credential-format',
|
||||
source: ['eyJ', 'a'.repeat(12), '.', 'b'.repeat(12), '.', 'c'.repeat(12)].join(''),
|
||||
},
|
||||
] as const;
|
||||
|
||||
describe('documentation validation regressions', (): void => {
|
||||
it('rejects a local Markdown link whose heading fragment does not exist', (): void => {
|
||||
expect(
|
||||
markdownLinkViolations('docs/fleet/source.md', '[broken](target.md#missing)', {
|
||||
'docs/fleet/target.md': '# Present',
|
||||
}),
|
||||
).toEqual(['docs/fleet/source.md -> target.md#missing: missing heading']);
|
||||
});
|
||||
|
||||
it('accepts balanced-parenthesis link destinations and visible-text heading anchors', (): void => {
|
||||
expect(localMarkdownTargets('[guide](guide-(legacy).md#setup "Guide")')).toEqual([
|
||||
'guide-(legacy).md#setup',
|
||||
]);
|
||||
expect(markdownHeadingAnchors('# [Fleet API](cli.md) behavior')).toContain(
|
||||
'fleet-api-behavior',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps longer fenced blocks closed only by an equal-or-longer fence', (): void => {
|
||||
expect(markdownHeadingAnchors('````markdown\n```\n# Not a heading\n````\n# Present')).toEqual(
|
||||
new Set(['present']),
|
||||
);
|
||||
});
|
||||
|
||||
it('assigns a free suffix when a prior heading already occupies the next duplicate anchor', (): void => {
|
||||
expect(markdownHeadingAnchors('# Foo\n# Foo-1\n# Foo')).toEqual(
|
||||
new Set(['foo', 'foo-1', 'foo-2']),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(UNSAFE_EXAMPLE_FIXTURES)(
|
||||
'classifies unsafe example fixture as $expected',
|
||||
({ expected, source }): void => {
|
||||
expect(exampleSafetyViolationKinds(source)).toContain(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('fleet operator documentation', (): void => {
|
||||
it('ships every accepted information-architecture page', async (): Promise<void> => {
|
||||
await expect(
|
||||
@@ -73,20 +207,34 @@ describe('fleet operator documentation', (): void => {
|
||||
).resolves.toHaveLength(REQUIRED_FLEET_PAGES.length);
|
||||
});
|
||||
|
||||
it('resolves every local Markdown link in the fleet book and sitemap', async (): Promise<void> => {
|
||||
it('resolves every local Markdown link and heading fragment in the fleet book and sitemap', async (): Promise<void> => {
|
||||
const files = [...(await markdownFiles(fleetDocs)), join(repositoryRoot, 'docs', 'SITEMAP.md')];
|
||||
const missing: string[] = [];
|
||||
const documents: Record<string, string> = {};
|
||||
for (const file of files) {
|
||||
const source = await readFile(file, 'utf8');
|
||||
const relative = file.slice(repositoryRoot.length + 1);
|
||||
documents[relative] = await readFile(file, 'utf8');
|
||||
}
|
||||
|
||||
const violations: string[] = [];
|
||||
for (const [sourcePath, source] of Object.entries(documents)) {
|
||||
for (const target of localMarkdownTargets(source)) {
|
||||
try {
|
||||
await readFile(resolve(dirname(file), target), 'utf8');
|
||||
} catch {
|
||||
missing.push(`${file.slice(repositoryRoot.length + 1)} -> ${target}`);
|
||||
const encodedPath = target.split('#', 1)[0] ?? '';
|
||||
const targetPath = resolve(
|
||||
dirname(join(repositoryRoot, sourcePath)),
|
||||
decodeURIComponent(encodedPath),
|
||||
);
|
||||
const relativeTarget = targetPath.slice(repositoryRoot.length + 1);
|
||||
if (documents[relativeTarget] === undefined) {
|
||||
try {
|
||||
documents[relativeTarget] = await readFile(targetPath, 'utf8');
|
||||
} catch {
|
||||
// The deterministic validator below records the missing target without exposing content.
|
||||
}
|
||||
}
|
||||
}
|
||||
violations.push(...markdownLinkViolations(sourcePath, source, documents));
|
||||
}
|
||||
expect(missing).toEqual([]);
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it('validates the canonical documentation example through the production compiler and resolver', async (): Promise<void> => {
|
||||
@@ -105,26 +253,20 @@ describe('fleet operator documentation', (): void => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps every fenced fleet example free of sensitive values, arbitrary commands, and product-hardcoded identities', async (): Promise<void> => {
|
||||
it('keeps every fenced fleet example free of sensitive values, privileged commands, arbitrary command overrides, and product-hardcoded identities', async (): Promise<void> => {
|
||||
const violations: string[] = [];
|
||||
for (const file of await markdownFiles(fleetDocs)) {
|
||||
const source = await readFile(file, 'utf8');
|
||||
for (const [index, block] of fencedCodeBlocks(source).entries()) {
|
||||
if (/(?:secret|token|password|credential|MOSAIC_AGENT_COMMAND)/i.test(block)) {
|
||||
violations.push(`${file.slice(repositoryRoot.length + 1)}#block-${index + 1}: sensitive`);
|
||||
}
|
||||
if (/\b(?:Tess|Ultron)\b/.test(block)) {
|
||||
violations.push(`${file.slice(repositoryRoot.length + 1)}#block-${index + 1}: identity`);
|
||||
for (const kind of exampleSafetyViolationKinds(block)) {
|
||||
violations.push(`${file.slice(repositoryRoot.length + 1)}#block-${index + 1}: ${kind}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rosterSource = await readFile(join(fleetDocs, 'examples', 'roster-v2.yaml'), 'utf8');
|
||||
if (/(?:secret|token|password|credential|MOSAIC_AGENT_COMMAND)/i.test(rosterSource)) {
|
||||
violations.push('docs/fleet/examples/roster-v2.yaml: sensitive');
|
||||
}
|
||||
if (/\b(?:Tess|Ultron)\b/.test(rosterSource)) {
|
||||
violations.push('docs/fleet/examples/roster-v2.yaml: identity');
|
||||
for (const kind of exampleSafetyViolationKinds(rosterSource)) {
|
||||
violations.push(`docs/fleet/examples/roster-v2.yaml: ${kind}`);
|
||||
}
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user