diff --git a/.github/workflows/schema-sync.yml b/.github/workflows/schema-sync.yml index e0af002..70497e5 100644 --- a/.github/workflows/schema-sync.yml +++ b/.github/workflows/schema-sync.yml @@ -76,3 +76,13 @@ jobs: --label schema-sync \ --label "automated pr" fi + + # Keep a coverage comment on the PR, updated on each sync + npm run --silent schema:coverage > coverage.md + if [ ! -s coverage.md ]; then + printf '## Docs coverage\n\nPolicy docs and schema entries match.\n' > coverage.md + fi + gh pr comment automation/schema-sync \ + --body-file coverage.md \ + --edit-last \ + --create-if-none diff --git a/package.json b/package.json index d747f4f..582168a 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "preview": "astro preview", "astro": "astro", "schema:sync": "node scripts/sync-schema.mjs", + "schema:coverage": "node scripts/check-schema-coverage.mjs", "release-dates:sync": "node scripts/sync-release-dates.mjs" }, "dependencies": { diff --git a/scripts/check-schema-coverage.mjs b/scripts/check-schema-coverage.mjs new file mode 100644 index 0000000..d85d7c6 --- /dev/null +++ b/scripts/check-schema-coverage.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env node +// Compare policies in schema/policies-schema.json against policy docs pages +// and print as markdown. Used by .github/workflows/schema-sync.yml +import { readFile, readdir } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve, join, parse } from "node:path"; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const SCHEMA_PATH = join(REPO_ROOT, "schema", "policies-schema.json"); +const DOCS_DIR = join(REPO_ROOT, "src", "content", "docs", "reference", "policies"); +const DOCS_PATH = "src/content/docs/reference/policies"; + +// Docs pages that intentionally have no schema entry. +const NOT_IN_SCHEMA = new Set([ + // macOS plist-only switch, read before policies.json is parsed. + "EnterprisePoliciesEnabled", +]); + +const schema = JSON.parse(await readFile(SCHEMA_PATH, "utf8")); +const schemaPolicies = new Set(Object.keys(schema.properties ?? {})); + +const docPages = new Set( + (await readdir(DOCS_DIR)).filter((f) => /\.mdx?$/.test(f)).map((f) => parse(f).name), +); + +const missingDocs = [...schemaPolicies].filter((p) => !docPages.has(p)).sort(); +const missingSchema = [...docPages] + .filter((p) => !schemaPolicies.has(p) && !NOT_IN_SCHEMA.has(p)) + .sort(); + +// Print nothing when docs and schema match +if (missingDocs.length === 0 && missingSchema.length === 0) process.exit(0); + +const lines = ["## Docs coverage", ""]; + +if (missingDocs.length > 0) { + lines.push(`**Policies without docs** (add a page under \`${DOCS_PATH}/\`):`, ""); + lines.push(...missingDocs.map((p) => `- \`${p}\``), ""); +} +if (missingSchema.length > 0) { + lines.push("**Docs without a schema entry** (removed or renamed upstream?):", ""); + lines.push(...missingSchema.map((p) => `- \`${p}\``), ""); +} + +console.log(lines.join("\n").trimEnd());