mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
fe23965191
## Summary This PR covers the release automation follow-up from #19890. It replaces the earlier Rust-based prototype with a small Node script, `.github/scripts/update-rule-versions.js`, that scans `declare_oxc_lint!` declarations under `crates/oxc_linter/src/rules/**` and: - rewrites stable `version = "next"` entries to the actual oxlint release version during release prep - leaves `nursery` rules on `version = "next"` - reuses the same script during release CI and fails if rerunning it would still rewrite tracked rule files, so no stable `version = "next"` remains at release time - prints a simple change report - supports `--dry-run` for local verification The implementation also now hardens a few failure modes that came up during review: - comment-only lines inside `declare_oxc_lint!` blocks are ignored when parsing metadata - trailing `// ...` and single-line `/* ... */` comments on metadata lines are handled correctly - multi-line block comments on metadata lines and macro terminators are handled correctly - doc examples inside the macro block that mention `version = "next"` are not misclassified as invalid matches - symlinked entries under the lint rules tree are rejected instead of being silently skipped - the script still fails if `version = "next"` is found outside a `declare_oxc_lint!` block Workflow changes: - `.github/workflows/prepare_release_apps.yml` sets up Node and runs the rewrite script after `cargo release-oxc update` - `.github/workflows/release_apps.yml` sets up Node, reruns the script with the released oxlint version, and then checks `git diff --exit-code -- crates/oxc_linter/src/rules` ## Testing ```bash node --test .github/scripts/update-rule-versions.test.js node .github/scripts/update-rule-versions.js --root . --release-version 1.61.0 --dry-run cargo check -p oxc_linter --locked ``` ## Notes This PR intentionally does not include the docs/website wiring from #19890. That should stay in a follow-up PR since it touches a different data flow and also involves `oxc-project/website#937`. ## AI Usage Disclosure I used AI assistance to help inspect the release workflows, compare the implementation direction against the maintainer feedback, and draft the initial automation changes. I manually reviewed the final implementation, added the regression tests for the workflow guard and parser edge cases above, ran the checks listed here, and remain responsible for the final code and submission. --------- Co-authored-by: Cam McHenry <camchenry@users.noreply.github.com> Co-authored-by: Cameron <cameron.clark@hey.com>
90 lines
2.9 KiB
TypeScript
90 lines
2.9 KiB
TypeScript
#!/usr/bin/env node
|
|
/* eslint-disable no-console */
|
|
|
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { parseArgs } from "node:util";
|
|
|
|
const DEFAULT_RULES_ROOT = path.join("crates", "oxc_linter", "src", "rules");
|
|
|
|
function analyzeRuleFile(source: string, releaseVersion: string): string {
|
|
return source.replace(/declare_oxc_lint!\(([\s\S]*?\n)\s*\);/g, (match, body: string) => {
|
|
// Strip leading doc comments, then extract comma-delimited fields: name, plugin, category
|
|
const stripped = body.replace(/^(?:\s*\/\/\/.*\n)*/, "");
|
|
const fields = [...stripped.matchAll(/(\w+)\s*,/g)];
|
|
if (fields.length < 3) return match;
|
|
|
|
const category = fields[2][1];
|
|
if (category === "nursery") return match;
|
|
if (!/version\s*=\s*"next"/.test(body)) return match;
|
|
|
|
return match.replace(/(version\s*=\s*)"next"/, `$1"${releaseVersion}"`);
|
|
});
|
|
}
|
|
|
|
const { values } = parseArgs({
|
|
args: process.argv.slice(2),
|
|
options: {
|
|
"release-version": { type: "string", short: "r" },
|
|
root: { type: "string", short: "C", default: process.cwd() },
|
|
write: { type: "boolean", short: "w", default: false },
|
|
help: { type: "boolean", short: "h" },
|
|
},
|
|
strict: true,
|
|
});
|
|
|
|
const releaseVersion = values["release-version"];
|
|
const { root, write } = values;
|
|
|
|
if (values.help) {
|
|
console.log(`Usage:
|
|
node .github/scripts/update-rule-versions.mts --release-version <x.y.z> [--root <path>] [--write]
|
|
|
|
Options:
|
|
--release-version, -r Version to replace \`version = "next"\` with
|
|
--root, -C Repository root (defaults to current working directory)
|
|
--write, -w Write changes to files (default: dry-run)
|
|
--help, -h Show this help
|
|
`);
|
|
process.exit(0);
|
|
}
|
|
|
|
if (!releaseVersion) {
|
|
throw new Error("missing required `--release-version <x.y.z>`");
|
|
}
|
|
if (!/^\d+\.\d+\.\d+$/.test(releaseVersion)) {
|
|
throw new Error(`release version must be x.y.z, got \`${releaseVersion}\``);
|
|
}
|
|
|
|
const dryRun = !write;
|
|
const repoRoot = path.resolve(root);
|
|
const rulesRoot = path.join(repoRoot, DEFAULT_RULES_ROOT);
|
|
if (!existsSync(rulesRoot)) {
|
|
throw new Error(`rules root does not exist: ${rulesRoot}`);
|
|
}
|
|
|
|
let updatedCount = 0;
|
|
|
|
const ruleFiles = readdirSync(rulesRoot, { recursive: true })
|
|
.filter((f) => typeof f === "string" && f.endsWith(".rs"))
|
|
.map((f) => path.join(rulesRoot, String(f)))
|
|
.sort();
|
|
|
|
for (const filePath of ruleFiles) {
|
|
const source = readFileSync(filePath, "utf8");
|
|
const updatedSource = analyzeRuleFile(source, releaseVersion);
|
|
if (updatedSource === source) continue;
|
|
|
|
const relativeFile = path.relative(repoRoot, filePath).replaceAll(path.sep, "/");
|
|
console.log(
|
|
`${dryRun ? "Would update" : "Updated"} ${relativeFile}: version = "next" -> version = "${releaseVersion}"`,
|
|
);
|
|
updatedCount++;
|
|
|
|
if (!dryRun) {
|
|
writeFileSync(filePath, updatedSource);
|
|
}
|
|
}
|
|
|
|
console.log(`${updatedCount} rule(s) ${dryRun ? "would be updated" : "updated"}.`);
|