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
3 changes: 2 additions & 1 deletion docs/publishing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
15 changes: 14 additions & 1 deletion scripts/store.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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/);
Expand Down
4 changes: 2 additions & 2 deletions scripts/track-releases.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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$/;
Expand Down Expand Up @@ -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" });
Expand Down
32 changes: 32 additions & 0 deletions scripts/validate-index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <old-index.json>]
//
// --verify-archives downloads every archive (or, with --base, only archives
Expand Down Expand Up @@ -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"];
Expand Down Expand Up @@ -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) {
Expand Down
Loading