diff --git a/docs/publishing.md b/docs/publishing.md index e81f4da..c8aa2fc 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -96,9 +96,10 @@ shasum -a 256 zig-support-2.0.0-darwin-arm64.zip 字段说明见 [schema/plugin-index.schema.json](../schema/plugin-index.schema.json)。`homepage`、 `localizedDescriptions`、`tags`、`icon` 可选;`icon` 只能是同一仓库的 raw 文件。 -2. 本地校验: +2. 统一格式并本地校验(CI 要求索引使用统一格式:两格缩进,纯值列表写在一行): ```bash + node scripts/validate-index.mjs --write-format node scripts/validate-index.mjs --verify-archives ``` diff --git a/scripts/store.test.mjs b/scripts/store.test.mjs index c0f8a6c..0798127 100644 --- a/scripts/store.test.mjs +++ b/scripts/store.test.mjs @@ -5,7 +5,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; import { addVersion, platformsForAsset } from "./track-releases.mjs"; -import { inspectArchive, validateEntry, validateIndex } from "./validate-index.mjs"; +import { formatIndex, inspectArchive, validateEntry, validateIndex } from "./validate-index.mjs"; // Fixtures are copied from Lithe-IDEA shared/contracts/plugins/v1/fixtures/marketplace // so this validator reports the same code as Lithe's Rust Core for each case. @@ -48,6 +48,19 @@ test("the published index is valid", () => { assert.deepEqual(validateIndex(index), []); }); +test("the published index is canonically formatted", () => { + const text = readFileSync(new URL("../lithe-plugin-index.json", import.meta.url), "utf8"); + assert.equal(text, formatIndex(JSON.parse(text))); +}); + +test("formatting keeps plain arrays inline and round-trips", () => { + const text = formatIndex(fixtureIndex); + assert.deepEqual(JSON.parse(text), fixtureIndex); + assert.match(text, /"platforms": \["darwin-arm64"\]/); + assert.match(text, /"tags": \["Language", "zig"\]/); + assert.equal(formatIndex({ schemaVersion: 1, plugins: [] }), '{\n "schemaVersion": 1,\n "plugins": []\n}\n'); +}); + test("duplicate ids and unsorted plugins fail the store policy", () => { const second = { ...validEntry(), id: "io.example.another" }; assert.match(validateIndex({ schemaVersion: 1, plugins: [validEntry(), validEntry()] }).join("\n"), /plugin\.index\.duplicate/); diff --git a/scripts/track-releases.mjs b/scripts/track-releases.mjs index ef6a8f8..086b4cf 100644 --- a/scripts/track-releases.mjs +++ b/scripts/track-releases.mjs @@ -18,7 +18,7 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; -import { checkManifest, readPackageManifests, validateIndex } from "./validate-index.mjs"; +import { checkManifest, formatIndex, readPackageManifests, validateIndex } from "./validate-index.mjs"; export const KNOWN_ARCHITECTURES = { darwin: ["arm64", "x64"], windows: ["x64", "arm64"], linux: ["x64", "arm64"] }; const ASSET_PLATFORM = /-(darwin|windows|linux)(?:-(arm64|x64))?\.zip$/; @@ -165,7 +165,7 @@ async function main(argv) { for (const problem of problems) console.error(`✗ ${problem}`); return 1; } - if (!dryRun && added.length > 0) writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`); + if (!dryRun && added.length > 0) writeFileSync(indexPath, formatIndex(index)); if (process.env.GITHUB_OUTPUT) { const summary = added.join(", "); writeFileSync(process.env.GITHUB_OUTPUT, `added=${added.length}\nsummary=${summary}\n`, { flag: "a" }); diff --git a/scripts/validate-index.mjs b/scripts/validate-index.mjs index 8ac3ccc..f6bf708 100644 --- a/scripts/validate-index.mjs +++ b/scripts/validate-index.mjs @@ -8,6 +8,7 @@ // // Usage: // node scripts/validate-index.mjs [index.json] +// node scripts/validate-index.mjs [index.json] --write-format // node scripts/validate-index.mjs [index.json] --verify-archives [--base ] // // --verify-archives downloads every archive (or, with --base, only archives @@ -163,6 +164,32 @@ export function validateEntry(entry) { } } +/** + * Canonical index text: two-space indentation, arrays of plain values kept on + * one line. The tracker writes this form and CI requires it, so bot and + * hand-written pull requests produce the same minimal diffs. + */ +export function formatIndex(index) { + const format = (value, indent) => { + if (Array.isArray(value)) { + if (value.length === 0) return "[]"; + if (value.every((item) => item === null || typeof item !== "object")) { + return `[${value.map((item) => JSON.stringify(item)).join(", ")}]`; + } + const inner = `${indent} `; + return `[\n${value.map((item) => inner + format(item, inner)).join(",\n")}\n${indent}]`; + } + if (value !== null && typeof value === "object") { + const keys = Object.keys(value); + if (keys.length === 0) return "{}"; + const inner = `${indent} `; + return `{\n${keys.map((key) => `${inner}${JSON.stringify(key)}: ${format(value[key], inner)}`).join(",\n")}\n${indent}}`; + } + return JSON.stringify(value); + }; + return `${format(index, "")}\n`; +} + /** Returns a list of problems; an empty list means the index is publishable. */ export function validateIndex(index) { if (!isObject(index)) return ["index root must be an object"]; @@ -298,6 +325,11 @@ async function main(argv) { return 1; } const problems = validateIndex(index); + if (args.includes("--write-format")) { + writeFileSync(indexPath, formatIndex(index)); + } else if (readFileSync(indexPath, "utf8") !== formatIndex(index)) { + problems.push(`store policy: ${indexPath} is not canonically formatted; run node scripts/validate-index.mjs --write-format`); + } if (problems.length === 0 && verify) { let baseIndex; if (basePath) {