mirror of
https://github.com/kucherenko/jscpd.git
synced 2026-09-19 08:11:03 +08:00
4c3f007d0b
`cargo publish` verifies by building the packaged crate against the *published* versions of its dependencies, not the local paths. basta uses `cpd_core::deadcode`, `cpd_reporter::deadcode` and a public `LineIndex` — none of which exist in the versions on crates.io — so every attempt failed with "failed to verify package tarball", and basta could not reach crates.io until a jscpd release happened to carry those three crates along. That is the coupling this workflow exists to avoid. It now walks basta's own dependency graph, publishes in topological order whatever is missing, and waits for the index between crates. jscpd is excluded by construction: it is a sibling, not a dependency, and releasing it is not this workflow's job. Bumps the three crates that actually changed: cpd-core 0.1.13 -> 0.1.14 (the `deadcode` module), cpd-tokenizer 0.1.15 -> 0.1.16 (`LineIndex` made public, `.pyi`), cpd-reporter 0.1.14 -> 0.1.15 (the dead-code reporters). cpd-finder is untouched and stays where it is. Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
278 lines
10 KiB
JavaScript
278 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const packageJson = JSON.parse(
|
|
fs.readFileSync(path.join(root, "package.json"), "utf8"),
|
|
);
|
|
|
|
const npmVersion = packageJson.version;
|
|
|
|
const subCrates = [
|
|
{ dir: "crates/cpd-core", version: "0.1.14" },
|
|
{ dir: "crates/cpd-tokenizer", version: "0.1.16" },
|
|
{ dir: "crates/cpd-finder", version: "0.1.16" },
|
|
{ dir: "crates/cpd-reporter", version: "0.1.15" },
|
|
{ dir: "crates/basta", version: "0.1.1" },
|
|
];
|
|
|
|
const mainCrate = { dir: "crates/cpd", version: npmVersion };
|
|
|
|
// basta ships on its own cadence: its crate version is the single source of
|
|
// truth for the npm wrapper and its platform packages alike, so a jscpd
|
|
// release never drags basta's version along and vice versa.
|
|
const bastaVersion = subCrates.find((c) => c.dir === "crates/basta").version;
|
|
|
|
const subCrateVersions = {};
|
|
for (const { dir, version } of subCrates) {
|
|
subCrateVersions[dir.split("/")[1]] = version;
|
|
}
|
|
|
|
function updateCargoToml(filePath, updates) {
|
|
let content = fs.readFileSync(filePath, "utf8");
|
|
let changed = false;
|
|
|
|
for (const [pattern, replacement] of updates) {
|
|
const newContent = content.replace(pattern, replacement);
|
|
if (newContent !== content) {
|
|
content = newContent;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
fs.writeFileSync(filePath, content);
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
// Update sub-crate versions and their dependency references
|
|
for (const { dir, version } of subCrates) {
|
|
const filePath = path.join(root, dir, "Cargo.toml");
|
|
const crateName = dir.split("/")[1];
|
|
const updates = [
|
|
[/^version = ".*"$/m, `version = "${version}"`],
|
|
];
|
|
|
|
// Update dependency versions for other sub-crates this crate depends on
|
|
for (const [depName, depVersion] of Object.entries(subCrateVersions)) {
|
|
if (crateName !== depName) {
|
|
updates.push([
|
|
new RegExp(`^(${depName} = \\{ version = )"([^"]+)"(, path = "([^"]+)"\\s*)\\}`, "m"),
|
|
`$1"${depVersion}"$3}`,
|
|
]);
|
|
}
|
|
}
|
|
|
|
const changed = updateCargoToml(filePath, updates);
|
|
console.log(`${changed ? "Updated" : "No change"} ${dir}/Cargo.toml version to ${version}`);
|
|
}
|
|
|
|
// Update main crate (jscpd) version and its dependency references
|
|
{
|
|
const filePath = path.join(root, mainCrate.dir, "Cargo.toml");
|
|
const updates = [
|
|
[/^version = ".*"$/m, `version = "${mainCrate.version}"`],
|
|
];
|
|
|
|
for (const [depName, depVersion] of Object.entries(subCrateVersions)) {
|
|
updates.push([
|
|
new RegExp(`^(${depName} = \\{ version = )"([^"]+)"(, path = "([^"]+)"\\s*)\\}`, "m"),
|
|
`$1"${depVersion}"$3}`,
|
|
]);
|
|
}
|
|
|
|
const changed = updateCargoToml(filePath, updates);
|
|
console.log(`${changed ? "Updated" : "No change"} ${mainCrate.dir}/Cargo.toml version to ${mainCrate.version}`);
|
|
}
|
|
|
|
console.log(`Version sync complete: npm=${npmVersion}, sub-crates=${JSON.stringify(subCrateVersions)}`);
|
|
|
|
// Sync cpd package optionalDependencies (platform binary packages)
|
|
{
|
|
const cpdPkgPath = path.join(root, "package.json");
|
|
const cpdPkg = JSON.parse(fs.readFileSync(cpdPkgPath, "utf8"));
|
|
let changed = false;
|
|
for (const [dep, version] of Object.entries(cpdPkg.optionalDependencies || {})) {
|
|
if (version !== npmVersion) {
|
|
cpdPkg.optionalDependencies[dep] = npmVersion;
|
|
changed = true;
|
|
}
|
|
}
|
|
if (changed) {
|
|
fs.writeFileSync(cpdPkgPath, `${JSON.stringify(cpdPkg, null, 2)}\n`);
|
|
console.log(`Updated package.json optionalDependencies to ${npmVersion}`);
|
|
} else {
|
|
console.log(`No change package.json optionalDependencies (${npmVersion})`);
|
|
}
|
|
}
|
|
|
|
// Sync jscpd wrapper package version and its platform pins.
|
|
//
|
|
// Version and optionalDependencies are updated independently on purpose. An
|
|
// earlier revision skipped the whole block when the version already matched,
|
|
// which meant anything that set `version` before this script ran (a manual
|
|
// edit, a partial release) left the platform pins stale — and they ship that
|
|
// way, so the wrapper resolves an older engine than it claims to be. That is
|
|
// how jscpd@5.1.0 went out pinned to the 5.0.16 binaries.
|
|
{
|
|
const jscpdPkgPath = path.join(root, "jscpd", "package.json");
|
|
const jscpdPkg = JSON.parse(fs.readFileSync(jscpdPkgPath, "utf8"));
|
|
let changed = false;
|
|
|
|
if (jscpdPkg.version !== npmVersion) {
|
|
jscpdPkg.version = npmVersion;
|
|
changed = true;
|
|
}
|
|
|
|
for (const [dep, version] of Object.entries(jscpdPkg.optionalDependencies || {})) {
|
|
if (version !== npmVersion) {
|
|
jscpdPkg.optionalDependencies[dep] = npmVersion;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
fs.writeFileSync(jscpdPkgPath, `${JSON.stringify(jscpdPkg, null, 2)}\n`);
|
|
console.log(`Updated jscpd/package.json to ${npmVersion}`);
|
|
} else {
|
|
console.log(`No change jscpd/package.json (${npmVersion})`);
|
|
}
|
|
}
|
|
|
|
// Sync the basta wrapper package version and its platform pins, against the
|
|
// basta crate version rather than the jscpd release version. Version and pins
|
|
// are set independently for the same reason the jscpd block does it: anything
|
|
// that sets one without the other ships a wrapper resolving the wrong engine.
|
|
{
|
|
const bastaPkgPath = path.join(root, "basta", "package.json");
|
|
const bastaPkg = JSON.parse(fs.readFileSync(bastaPkgPath, "utf8"));
|
|
let changed = false;
|
|
|
|
if (bastaPkg.version !== bastaVersion) {
|
|
bastaPkg.version = bastaVersion;
|
|
changed = true;
|
|
}
|
|
|
|
for (const [dep, version] of Object.entries(bastaPkg.optionalDependencies || {})) {
|
|
if (version !== bastaVersion) {
|
|
bastaPkg.optionalDependencies[dep] = bastaVersion;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
fs.writeFileSync(bastaPkgPath, `${JSON.stringify(bastaPkg, null, 2)}\n`);
|
|
console.log(`Updated basta/package.json to ${bastaVersion}`);
|
|
} else {
|
|
console.log(`No change basta/package.json (${bastaVersion})`);
|
|
}
|
|
}
|
|
|
|
// PyPI needs PEP 440 versions: 5.3.0-beta.1 -> 5.3.0b1 (alpha/beta/rc only).
|
|
// Mirrors `pep440()` in build-pypi-wheels.py; keep the two in step. Anything
|
|
// else throws rather than guessing, since a wrong mapping would pin the
|
|
// pre-commit hook to a version that never reaches PyPI.
|
|
function toPep440(version) {
|
|
const m = /^(\d+\.\d+\.\d+)(?:-(alpha|beta|rc)\.?(\d+))?$/.exec(version);
|
|
if (!m) throw new Error(`cannot map version ${version} to PEP 440`);
|
|
const [, base, kind, num] = m;
|
|
return kind ? `${base}${{ alpha: "a", beta: "b", rc: "rc" }[kind]}${num}` : base;
|
|
}
|
|
const pypiVersion = toPep440(npmVersion);
|
|
|
|
// Sync CITATION.cff: `version` follows the engine version, and `date-released`
|
|
// is set to today's UTC date whenever the version changes, so a release commit
|
|
// carries the right citation metadata without a manual edit.
|
|
{
|
|
const citationPath = path.join(root, "..", "CITATION.cff");
|
|
const citation = fs.readFileSync(citationPath, "utf8");
|
|
const current = /^version: (.*)$/m.exec(citation)?.[1];
|
|
if (current !== npmVersion) {
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
const updated = citation
|
|
.replace(/^version: .*$/m, `version: ${npmVersion}`)
|
|
.replace(/^date-released: .*$/m, `date-released: ${today}`);
|
|
fs.writeFileSync(citationPath, updated);
|
|
console.log(`Updated ../CITATION.cff to ${npmVersion} (released ${today})`);
|
|
} else {
|
|
console.log(`No change ../CITATION.cff (${npmVersion})`);
|
|
}
|
|
}
|
|
|
|
// Sync the repository-root pyproject.toml. It is a private project whose only
|
|
// job is to make `pre-commit` (language: python, see .pre-commit-hooks.yaml)
|
|
// install the `jscpd` binary from PyPI when a project points its hook at this
|
|
// repo, so its `version` and its exact `jscpd` pin follow the engine version.
|
|
{
|
|
const pyprojectPath = path.join(root, "..", "pyproject.toml");
|
|
const changed = updateCargoToml(pyprojectPath, [
|
|
[/^version = ".*"$/m, `version = "${pypiVersion}"`],
|
|
[/^dependencies = \["jscpd==[^"]*"\]$/m, `dependencies = ["jscpd==${pypiVersion}"]`],
|
|
]);
|
|
console.log(`${changed ? "Updated" : "No change"} ../pyproject.toml to ${pypiVersion}`);
|
|
}
|
|
|
|
// Fail loudly rather than shipping a wrapper that resolves the wrong engine.
|
|
{
|
|
const problems = [];
|
|
for (const rel of ["package.json", "jscpd/package.json"]) {
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(root, rel), "utf8"));
|
|
if (pkg.version !== npmVersion) {
|
|
problems.push(`${rel}: version is ${pkg.version}, expected ${npmVersion}`);
|
|
}
|
|
for (const [dep, version] of Object.entries(pkg.optionalDependencies || {})) {
|
|
if (version !== npmVersion) {
|
|
problems.push(`${rel}: ${dep} pinned to ${version}, expected ${npmVersion}`);
|
|
}
|
|
}
|
|
}
|
|
{
|
|
const citation = fs.readFileSync(path.join(root, "..", "CITATION.cff"), "utf8");
|
|
const version = /^version: (.*)$/m.exec(citation)?.[1];
|
|
if (version !== npmVersion) {
|
|
problems.push(`../CITATION.cff: version is ${version}, expected ${npmVersion}`);
|
|
}
|
|
}
|
|
{
|
|
const pyproject = fs.readFileSync(path.join(root, "..", "pyproject.toml"), "utf8");
|
|
const version = /^version = "([^"]*)"$/m.exec(pyproject)?.[1];
|
|
if (version !== pypiVersion) {
|
|
problems.push(`../pyproject.toml: version is ${version}, expected ${pypiVersion}`);
|
|
}
|
|
const pin = /^dependencies = \["jscpd==([^"]*)"\]$/m.exec(pyproject)?.[1];
|
|
if (pin !== pypiVersion) {
|
|
problems.push(`../pyproject.toml: jscpd pinned to ${pin}, expected ${pypiVersion}`);
|
|
}
|
|
}
|
|
{
|
|
const pkg = JSON.parse(
|
|
fs.readFileSync(path.join(root, "basta", "package.json"), "utf8"),
|
|
);
|
|
if (pkg.version !== bastaVersion) {
|
|
problems.push(`basta/package.json: version is ${pkg.version}, expected ${bastaVersion}`);
|
|
}
|
|
for (const [dep, version] of Object.entries(pkg.optionalDependencies || {})) {
|
|
if (version !== bastaVersion) {
|
|
problems.push(`basta/package.json: ${dep} pinned to ${version}, expected ${bastaVersion}`);
|
|
}
|
|
}
|
|
const targets = JSON.parse(
|
|
fs.readFileSync(path.join(root, "npm", "prebuilt-targets.json"), "utf8"),
|
|
);
|
|
for (const key of Object.keys(targets)) {
|
|
if (!(`basta-${key}` in (pkg.optionalDependencies || {}))) {
|
|
problems.push(`basta/package.json: no optional dependency for platform ${key}`);
|
|
}
|
|
}
|
|
}
|
|
if (problems.length > 0) {
|
|
console.error("Version sync verification FAILED:");
|
|
for (const p of problems) console.error(` - ${p}`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`Verified npm package versions and platform pins are all ${npmVersion}`);
|
|
console.log(`Verified basta wrapper and platform pins are all ${bastaVersion}`);
|
|
} |