Skip to content
Merged
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
57 changes: 56 additions & 1 deletion .github/scripts/pin-bump.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,59 @@ function bumpPins(source, { pattern, from, to }) {
return { source: bumped, count };
}

module.exports = { bumpPins, escapeLiteral };
/**
* Move one `export const NAME = "…";` declaration to a new version, in the
* TypeScript source text of `packages/cli/src/rules/capabilities.ts`.
*
* WHY THIS IS PART OF THE BUMP. `capabilities.ts` publishes `AST_GREP_VERSION`
* and `VALE_VERSION` by hand, and `test/engine-version-consistency.test.ts`
* asserts they agree with the pins. That test is deliberate and the constant
* is deliberately NOT derived from package.json (its docblock says why), so a
* bot commit that moves the pins and not the constant fails Validate BY
* CONSTRUCTION. That is what happened on taskless/cli#368: the base of a stack
* was red before anyone had looked at it, and the child (#372) had to carry
* the constant along with the real work. The bot moves both now, so the
* consistency test is what it was meant to be — a check on humans, not a
* scheduled failure.
*
* WHY TEXT AGAIN. The same reason as `bumpPins`: the file is prettier-formatted
* and the diff a reviewer reads should be one string. The match is anchored on
* the whole declaration line, so a mention of the name in a docblock or a
* `{@link VALE_VERSION}` is not a candidate.
*
* WHY EXACTLY ONE. Zero means the declaration moved or was renamed and the
* workflow would otherwise push a commit the consistency test rejects — the
* failure this exists to remove. Two means the anchor is no longer specific,
* and rewriting both would be a guess. Either is a failed run.
*
* @param source the capabilities.ts text
* @param name the exported constant, e.g. `VALE_VERSION`
* @param to the version to write. For Vale this is the BASE version the
* binary reports (`3.22.0`), not the stamped npm version.
* @returns the rewritten source and the version the declaration held before
*/
function bumpVersionConstant(source, { name, to }) {
if (!/^[A-Z][A-Z0-9_]*$/.test(String(name))) {
throw new Error(
`the constant name must be an UPPER_SNAKE identifier, got ${JSON.stringify(name)}`
);
}
const matcher = new RegExp(
`^(export const ${name} = ")([^"\\n]*)(";)$`,
"gm"
);
const matches = [...source.matchAll(matcher)];
if (matches.length !== 1) {
throw new Error(
`expected exactly one \`export const ${name} = "…";\` declaration, found ${matches.length}`
);
}
const [match] = matches;
const [whole, head, from, tail] = match;
const at = match.index;
const bumped =
source.slice(0, at) + head + to + tail + source.slice(at + whole.length);
return { source: bumped, from };
}

module.exports = { bumpPins, bumpVersionConstant, escapeLiteral };
85 changes: 84 additions & 1 deletion .github/scripts/pin-bump.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
const test = require("node:test");
const assert = require("node:assert/strict");

const { bumpPins } = require("./pin-bump.cjs");
const { bumpPins, bumpVersionConstant } = require("./pin-bump.cjs");

/** The boundary-aware pattern sg-detect.cjs enumerates its pins with. */
const AST_GREP = /^@ast-grep\/cli(-|$)/;
Expand Down Expand Up @@ -114,3 +114,86 @@ test("a stateful /g pattern is refused rather than silently skipping pins", () =
/must not be \/g/
);
});

/**
* The constant half of the bump. `capabilities.ts` declares `AST_GREP_VERSION`
* and `VALE_VERSION` by hand and `engine-version-consistency.test.ts` holds
* them to the pins, so a bot commit that moves only the pins is red before
* anyone reads it (taskless/cli#368). These pin the rewrite that closes that.
*/
const CAPABILITIES = [
"/**",
" * Pinned against the binary by `test/ast-grep-vendor-contract.test.ts`.",
" */",
'export const AST_GREP_VERSION = "0.45.3";',
"",
"/**",
" * The Vale release, measured against {@link VALE_VERSION}'s binary.",
' * A literal stamp like "3.21.0-20260915061224" lives in package.json.',
" */",
'export const VALE_VERSION = "3.21.0";',
"",
'export const OTHER = "3.21.0";',
"",
].join("\n");

test("the named constant moves to the base version and reports what it held", () => {
const { source, from } = bumpVersionConstant(CAPABILITIES, {
name: "VALE_VERSION",
to: "3.22.0",
});
assert.equal(from, "3.21.0");
assert.match(source, /^export const VALE_VERSION = "3\.22\.0";$/m);
// Every other byte survives: the docblock's mention of the old version, the
// `{@link}`, and the unrelated constant at the same value.
assert.match(source, /"3\.21\.0-20260915061224"/);
assert.match(source, /\{@link VALE_VERSION\}/);
assert.match(source, /^export const OTHER = "3\.21\.0";$/m);
assert.match(source, /^export const AST_GREP_VERSION = "0\.45\.3";$/m);
assert.equal(source.split("\n").length, CAPABILITIES.split("\n").length);
});

test("the ast-grep constant is reached by the same anchor", () => {
const { source, from } = bumpVersionConstant(CAPABILITIES, {
name: "AST_GREP_VERSION",
to: "0.46.0",
});
assert.equal(from, "0.45.3");
assert.match(source, /^export const AST_GREP_VERSION = "0\.46\.0";$/m);
assert.match(source, /^export const VALE_VERSION = "3\.21\.0";$/m);
});

test("a constant already at the target is rewritten to itself", () => {
const { source, from } = bumpVersionConstant(CAPABILITIES, {
name: "VALE_VERSION",
to: "3.21.0",
});
assert.equal(from, "3.21.0");
assert.equal(source, CAPABILITIES);
});

test("a missing declaration fails the run rather than writing nothing", () => {
assert.throws(
() =>
bumpVersionConstant(CAPABILITIES, {
name: "RUFF_VERSION",
to: "1.0.0",
}),
/expected exactly one `export const RUFF_VERSION = "…";` declaration, found 0/
);
});

test("a declaration that appears twice is ambiguous and refused", () => {
const doubled = `${CAPABILITIES}export const VALE_VERSION = "3.20.0";\n`;
assert.throws(
() => bumpVersionConstant(doubled, { name: "VALE_VERSION", to: "3.22.0" }),
/found 2/
);
});

test("a name that is not an identifier cannot become a pattern", () => {
assert.throws(
() => bumpVersionConstant(CAPABILITIES, { name: ".*", to: "3.22.0" }),
/UPPER_SNAKE identifier/
);
});
53 changes: 46 additions & 7 deletions .github/scripts/sg-detect.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@
* nothing. This is what update-badges.cjs calls.
*
* --write rewrite every `@ast-grep/cli*` pin in packages/cli/package.json to
* the upstream version. `ast-grep-upgrade.yml` then regenerates the
* lockfile and opens a pull request.
* the upstream version, and `AST_GREP_VERSION` in
* packages/cli/src/rules/capabilities.ts with it.
* `ast-grep-upgrade.yml` then regenerates the lockfile and the rule
* schema, checks the result, and opens a pull request.
*
* THIS REVERSES AN EARLIER DECISION, deliberately. This script used
* to refuse to write on the grounds that a lockfile-touching bump
Expand Down Expand Up @@ -84,7 +86,7 @@
const { appendFileSync, readFileSync, writeFileSync } = require("node:fs");
const { join } = require("node:path");

const { bumpPins } = require("./pin-bump.cjs");
const { bumpPins, bumpVersionConstant } = require("./pin-bump.cjs");
const {
fetchReleaseByTag,
formatReleaseNotes,
Expand All @@ -101,6 +103,25 @@ const PACKAGE_JSON_PATH = join(
"package.json"
);

/**
* Where `AST_GREP_VERSION` is declared by hand. `--write` moves it with the
* pins, because `test/engine-version-consistency.test.ts` holds the two
* together and a bump that moved only the pins failed Validate before anyone
* had looked at it (taskless/cli#368, the Vale instance of the same shape).
*/
const CAPABILITIES_PATH = join(
__dirname,
"..",
"..",
"packages",
"cli",
"src",
"rules",
"capabilities.ts"
);

const VERSION_CONSTANT = "AST_GREP_VERSION";

/** `@ast-grep/cli` itself and its per-platform siblings. */
const PIN_PATTERN = /^@ast-grep\/cli(-|$)/;

Expand Down Expand Up @@ -242,6 +263,7 @@ async function main({
releaseFor = fetchReleaseByTag,
packageJsonPath = PACKAGE_JSON_PATH,
packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")),
capabilitiesPath = CAPABILITIES_PATH,
} = {}) {
const json = argv.includes("--json");
const write = argv.includes("--write");
Expand Down Expand Up @@ -270,10 +292,13 @@ async function main({
console.log(JSON.stringify(comparison));
}

// Only the ahead path has anything to write. The pins are rewritten before
// the notes are fetched so that a network failure on the (optional) changelog
// cannot leave a half-done bump: by the time anything can throw below, the
// file on disk is either fully bumped or untouched.
// Only the ahead path has anything to write. BOTH rewrites are computed
// before EITHER file is written, and the writes come before the notes are
// fetched, so that neither a refused rewrite nor a network failure on the
// (optional) changelog can leave a half-done bump: by the time anything can
// throw below, the two files on disk are either both bumped or both
// untouched. A `--write` run by hand that fails therefore leaves a clean
// tree, not a package.json that moved without its constant.
if (write && ahead) {
const pins = collectPins(packageJson);
const source = readFileSync(packageJsonPath, "utf8");
Expand All @@ -292,8 +317,22 @@ async function main({
`expected to rewrite ${pins.size} @ast-grep/cli* pins, rewrote ${count}`
);
}

// Upstream's own packages, so the constant is the pin verbatim. Rewritten
// in the same run as the pins so the bot commit is self-consistent: a
// missing declaration throws here, before anything is written, rather
// than failing Validate later.
const { source: constants, from } = bumpVersionConstant(
readFileSync(capabilitiesPath, "utf8"),
{ name: VERSION_CONSTANT, to: upstream }
);

writeFileSync(packageJsonPath, bumped);
writeFileSync(capabilitiesPath, constants);
log(`Rewrote ${count} pins in ${packageJsonPath} to ${upstream}.`);
log(
`Rewrote ${VERSION_CONSTANT} in ${capabilitiesPath}: ${from} -> ${upstream}.`
);
}

// Only the ahead path has a bump to describe. A second request, unlike Vale's
Expand Down
Loading
Loading