mirror of
https://github.com/redwoodjs/local-ci.git
synced 2026-09-19 07:54:29 +08:00
e2fe5761ba
* feat(compat): make compatibility.json the source of truth Flip the compatibility matrix so packages/cli/compatibility.json is authoritative. scripts/gen-compatibility-md.mjs renders the .md from it, pnpm check runs a drift check, and the website imports the JSON directly and renders inline markdown in keys/notes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: add compat smoke coverage for secrets, working-dir defaults, runner image Fill three audit gaps identified against packages/cli/compatibility.json: - smoke-secrets-env-file: secrets resolved from .env.agent-ci and from the shell environment fallback - smoke-defaults-workdir: workflow- and job-level defaults.run.working-directory plus a step-level override - smoke-runner-image: .github/agent-ci.Dockerfile is picked up and the resulting image is used at job start Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(workflow): propagate defaults.run.working-directory to steps Parse defaults.run.working-directory at workflow and job level and merge it into each run-step's inputs with step > job > workflow precedence. Step-level working-directory already worked; the defaults blocks were silently ignored. Closes #290. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(compat): add proof field to compatibility.json rows Every `supported`/`partial` row now lists the smoke workflows that exercise it end-to-end. `pnpm compat:gen`/`compat:check` fails if a listed path does not resolve, catching renames. Rows with an empty `proof: []` are the audit gaps tracked in #292. Refs #292. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(dtu): attach step env per-step so it does not leak to siblings Step-level env was being aggregated into the job-wide EnvironmentVariables map (last-wins), so a later step's `env: FOO=x` would override an earlier step's effective FOO at runtime. Emit step.Env as the mapped step's own `environment` instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(compat): fill audit gaps #292 — env, expressions, local action Three smokes covering eight of the ten gaps listed in #292: - smoke-env-scoping: workflow + job + step `env:` precedence - smoke-expressions: hashFiles, format, join, runner.arch, type literals, startsWith/endsWith - smoke-local-action: `steps[*].uses: ./.github/actions/...` (verified via marker file rather than steps.<id>.outputs.*) Proof fields updated for every row these smokes cover. hashFiles downgraded to ⚠️ partial — agent-ci emits SHA-1, GitHub Actions emits SHA-256 (tracked by #294). Two gaps remain open as filed issues: - #293 defaults.run.shell / steps[*].shell not applied - #295 ${{ steps.<id>.outputs.<name> }} leaks literal text in run scripts Refs #292. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
3.2 KiB
JavaScript
105 lines
3.2 KiB
JavaScript
#!/usr/bin/env node
|
|
// Generate packages/cli/compatibility.md from compatibility.json.
|
|
// The JSON is the source of truth; run `pnpm compat:gen` after editing it.
|
|
// `--check` exits non-zero if the on-disk .md differs from what the JSON produces.
|
|
|
|
import { access, readFile, writeFile } from "node:fs/promises";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = resolve(__dirname, "..");
|
|
const cliDir = resolve(repoRoot, "packages/cli");
|
|
const jsonPath = resolve(cliDir, "compatibility.json");
|
|
const mdPath = resolve(cliDir, "compatibility.md");
|
|
|
|
const data = JSON.parse(await readFile(jsonPath, "utf8"));
|
|
|
|
const missingProof = [];
|
|
for (const section of data.sections) {
|
|
for (const row of section.rows) {
|
|
if (!row.proof) {
|
|
continue;
|
|
}
|
|
for (const path of row.proof) {
|
|
try {
|
|
await access(resolve(repoRoot, path));
|
|
} catch {
|
|
missingProof.push(`${section.id}/${row.key}: ${path}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (missingProof.length > 0) {
|
|
console.error(`compatibility.json references proof paths that do not exist:`);
|
|
for (const entry of missingProof) {
|
|
console.error(` ${entry}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
const isWideGrapheme = (g) => {
|
|
for (const ch of g) {
|
|
const cp = ch.codePointAt(0);
|
|
if ((cp >= 0x2600 && cp <= 0x27bf) || cp >= 0x1f000) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
const visualWidth = (s) => {
|
|
let w = 0;
|
|
for (const { segment } of segmenter.segment(s)) {
|
|
w += isWideGrapheme(segment) ? 2 : 1;
|
|
}
|
|
return w;
|
|
};
|
|
|
|
const escapeCell = (s) => s.replace(/\|/g, "\\|");
|
|
const pad = (s, width) => s + " ".repeat(Math.max(0, width - visualWidth(s)));
|
|
|
|
function renderTable(rows) {
|
|
const header = ["Key", "Status", "Notes"];
|
|
const cells = rows.map((row) => [
|
|
escapeCell(row.key),
|
|
data.legend[row.status].icon,
|
|
escapeCell(row.notes || ""),
|
|
]);
|
|
const widths = header.map((h, i) =>
|
|
Math.max(visualWidth(h), ...cells.map((row) => visualWidth(row[i]))),
|
|
);
|
|
const line = (row) => `| ${row.map((c, i) => pad(c, widths[i])).join(" | ")} |`;
|
|
const sep = `| ${widths.map((w) => "-".repeat(Math.max(3, w))).join(" | ")} |`;
|
|
return [line(header), sep, ...cells.map(line)].join("\n");
|
|
}
|
|
|
|
const legendLine = Object.values(data.legend)
|
|
.map((l) => `${l.icon} = ${l.label}`)
|
|
.join(" ");
|
|
|
|
const parts = ["# YAML Compatibility", "", data.intro, "", legendLine, ""];
|
|
|
|
for (const section of data.sections) {
|
|
parts.push(`## ${section.title}`);
|
|
parts.push("");
|
|
parts.push(renderTable(section.rows));
|
|
parts.push("");
|
|
}
|
|
|
|
const output = parts.join("\n").replace(/\n+$/, "") + "\n";
|
|
|
|
if (process.argv.includes("--check")) {
|
|
const existing = await readFile(mdPath, "utf8").catch(() => "");
|
|
if (existing !== output) {
|
|
console.error(
|
|
`compatibility.md is out of sync with compatibility.json.\nRun \`pnpm compat:gen\` and commit the result.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
console.log("compatibility.md is in sync with compatibility.json");
|
|
} else {
|
|
await writeFile(mdPath, output);
|
|
console.log(`wrote ${mdPath}`);
|
|
}
|