Files
redwoodjs__local-ci/scripts/gen-compatibility-md.mjs
T
Peter Pistorius e2fe5761ba feat(compat): JSON source of truth + fill gaps + fix defaults.run.working-directory (#291)
* 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>
2026-04-20 08:47:20 +02:00

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(" &nbsp; ");
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}`);
}