Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*.json text eol=lf
*.md text eol=lf
*.mjs text eol=lf
*.mts text eol=lf
*.py text eol=lf
*.sh text eol=lf
*.ts text eol=lf
Expand Down
165 changes: 165 additions & 0 deletions .github/scripts/check_plugin_source_compatibility.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
// Check that tracked plugin source stays portable across repository imports.

import { spawnSync } from "node:child_process";
import { lstatSync, readFileSync } from "node:fs";
import { basename, extname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";

const MAX_SOURCE_FILE_BYTES = 150_000;
const MAX_DEPENDENCY_LOCK_BYTES = 2_000_000;
const DEPENDENCY_LOCK_NAMES = new Set([
"Cargo.lock",
"package-lock.json",
"pnpm-lock.yaml",
"requirements.txt",
"uv.lock",
"yarn.lock",
]);
const LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s+/u;
const HTML_BLOCK = /^\s*<\/?[A-Za-z][^>]*>\s*$/u;
const NATURAL_LINE_ENDINGS = new Set("\\.?!:;。!?:;)]}'\"`>");
const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });

function trackedFiles(pluginRoot: string): string[] {
const result = spawnSync(
"git",
["-C", pluginRoot, "ls-files", "-z", "--", "."],
{
maxBuffer: Infinity,
},
);
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(result.stderr.toString().trim() || "git ls-files failed");
}
return utf8.decode(result.stdout).split("\0").filter(Boolean);
}

function isMarkdownStructure(line: string): boolean {
const stripped = line.trim();
return (
!stripped ||
["#", ">", "|", "<!--", "::", "```", "~~~"].some((prefix) =>
stripped.startsWith(prefix),
) ||
["---", "***", "___"].includes(stripped) ||
HTML_BLOCK.test(stripped) ||
line.startsWith(" ") ||
line.startsWith("\t")
);
}

function lineEndsNaturally(line: string): boolean {
const stripped = line.trimEnd();
return (
line.endsWith(" ") ||
NATURAL_LINE_ENDINGS.has(stripped.slice(-1)) ||
/https?:\/\/\S+$/u.test(stripped)
);
}

function hardWrappedLines(content: string): number[] {
const lines = content.split(
/\r\n|[\n\r\v\f\u001c-\u001e\u0085\u2028\u2029]/u,
);
const offenders: number[] = [];
let inFence = false;
let inFrontmatter = content.startsWith("---\n");
for (let index = 0; index < lines.length - 1; index++) {
const line = lines[index]!;
const stripped = line.trim();
if (stripped.startsWith("```") || stripped.startsWith("~~~")) {
inFence = !inFence;
continue;
}
if (index > 0 && inFrontmatter && stripped === "---") {
inFrontmatter = false;
continue;
}
if (inFence || inFrontmatter) continue;
const followingLine = lines[index + 1]!;
if (isMarkdownStructure(line) || isMarkdownStructure(followingLine))
continue;
if (LIST_ITEM.test(followingLine) || lineEndsNaturally(line)) continue;
if (!/[A-Za-z0-9`]$/u.test(stripped)) continue;
if (!/^[A-Za-z0-9`(]/u.test(followingLine.trim())) continue;
offenders.push(index + 1);
}
return offenders;
}

function sourceCompatibilityErrors(pluginRoot: string): string[] {
const errors: string[] = [];
for (const relativePath of trackedFiles(pluginRoot)) {
const path = join(pluginRoot, relativePath);
const stat = lstatSync(path);
if (!stat.isFile()) continue;
const maximum = DEPENDENCY_LOCK_NAMES.has(basename(relativePath))
? MAX_DEPENDENCY_LOCK_BYTES
: MAX_SOURCE_FILE_BYTES;
// Git emits forward slashes even on Windows.
if (stat.size > maximum) {
errors.push(
`${relativePath}: file is ${stat.size} bytes; maximum is ${maximum} bytes`,
);
}
if (extname(relativePath).toLowerCase() !== ".md") continue;
const content = utf8.decode(readFileSync(path)).replace(/\r\n?/gu, "\n");
for (const lineNumber of hardWrappedLines(content)) {
errors.push(
`${relativePath}:${lineNumber}: prose is hard-wrapped mid-sentence; use a natural Markdown line`,
);
}
}
return errors.sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
}

function main(): number {
let values: { "plugin-root": string; help?: boolean };
try {
({ values } = parseArgs({
options: {
"plugin-root": {
type: "string",
default: fileURLToPath(
new URL("../../plugins/codex-security", import.meta.url),
),
},
help: { type: "boolean", short: "h" },
},
}));
} catch (error) {
console.error((error as Error).message);
return 2;
}
if (values.help) {
console.log(
"Check tracked plugin source for deterministic import compatibility.\n",
);
console.log(
"Usage: node --experimental-strip-types --disable-warning=ExperimentalWarning check_plugin_source_compatibility.mts [--plugin-root PATH]",
);
console.log(
"\n--plugin-root PATH plugin source root (default: plugins/codex-security in this repository)",
);
return 0;
}
try {
const errors = sourceCompatibilityErrors(resolve(values["plugin-root"]));
if (errors.length) {
console.error(errors.join("\n"));
return 1;
}
} catch (error) {
console.error(
`source compatibility check failed: ${(error as Error).message}`,
);
return 2;
}
console.log("Plugin source compatibility checks passed.");
return 0;
}

process.exitCode = main();
152 changes: 0 additions & 152 deletions .github/scripts/check_plugin_source_compatibility.py

This file was deleted.

Loading
Loading