ci: auto-refresh vendored js-yaml/re2js on Dependabot PRs (#4573) (#4576)

lint-vendored-deps.cjs gates gsd-core/bin/lib/vendor/{js-yaml.cjs,re2js.cjs}
for byte-freshness against node_modules and requires package.json's pin to
literally match the installed version. Dependabot regularly opens
lockfile-only PRs bumping these packages within the existing semver range,
which it can never satisfy (it has no awareness of the vendor copy or the
pin), so every such PR sits permanently red until a human manually runs the
refresh and pushes a fixup commit. PR #4565 was the latest instance.

Adds `--fix` to lint-vendored-deps.cjs (fixRow): mechanically re-copies the
upstream build artifact over the vendored .cjs/.d.cts twins and bumps the
manifest pin, preserving its range-operator style. It never touches a
hand-authored twin's declared type surface (js-yaml.d.cts) — a remaining
finding there means a real upstream API break, and --fix leaves it failing
rather than mask it.

Adds .github/workflows/dependabot-vendor-refresh.yml: on a same-repo
dependabot[bot] PR touching package.json/package-lock.json (same
defense-in-depth identity check as dependabot-auto-merge.yml), runs
`--fix` and, only on a clean result, commits and pushes the refresh back
to the PR branch via GSD_BOT_PR_TOKEN (falling back to GITHUB_TOKEN, same
pattern as auto-backmerge.yml) so the push re-triggers `synchronize` and
the real lint-vendored-deps check in test.yml genuinely re-passes. A
non-clean --fix result (real incompatibility) makes no commit, leaving
the actual failure visible for a human — this fixes the check's own
complaint, it does not bypass or weaken the check.

Closes #4573

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-09-09 11:46:12 -04:00
committed by GitHub
parent 30468f16fe
commit 6bd5c22f68
3 changed files with 322 additions and 16 deletions

View File

@@ -28,6 +28,8 @@ const {
compareFiles,
checkRow,
stripRangeOperator,
pinOperatorPrefix,
fixRow,
declaredValueExports,
checkHandAuthoredTwin,
resolvePath,
@@ -268,3 +270,88 @@ describe('G3: the hand-authored js-yaml type twin is excluded from byte-compare,
assert.doesNotMatch(content, /export function loadAll\(/, 'loadAll must stay undeclared per the narrowed surface');
});
});
describe('#4573: pinOperatorPrefix / fixRow — mechanical --fix for Dependabot-range vendor drift', () => {
test('pinOperatorPrefix extracts the leading range-operator token, or "" for an exact pin', () => {
assert.equal(pinOperatorPrefix('^4.3.1'), '^');
assert.equal(pinOperatorPrefix('~4.3.1'), '~');
assert.equal(pinOperatorPrefix('4.3.1'), '');
assert.equal(pinOperatorPrefix('>=4.3.1'), '>=');
});
test('regression guard: stripRangeOperator is unchanged for the same inputs after the PIN_OPERATOR_RE refactor', () => {
assert.equal(stripRangeOperator('^4.3.1'), '4.3.1');
assert.equal(stripRangeOperator('~4.3.1'), '4.3.1');
assert.equal(stripRangeOperator('4.3.1'), '4.3.1');
assert.equal(stripRangeOperator('>=4.3.1'), '4.3.1');
});
test('fixRow resolves mechanical .cjs drift: mutated vendored js-yaml.cjs is byte-restored to match node_modules', (t) => {
const row = jsYamlRow();
const vendoredAbs = path.join(REPO_ROOT, row.vendoredCjs);
const upstreamAbs = path.join(REPO_ROOT, row.upstreamCjs);
const original = fs.readFileSync(vendoredAbs, 'utf8');
fs.writeFileSync(vendoredAbs, `${original}\n// mutated for test\n`);
t.after(() => {
// Safety net: fixRow copies FROM upstream, so the vendored file should
// already be back in its original clean state — but re-copy from
// upstream regardless in case an assertion above threw before fixRow
// completed, so this test never leaves the tree dirty.
fs.copyFileSync(upstreamAbs, vendoredAbs);
});
const findings = fixRow(row);
assert.deepEqual(findings, [], `expected fixRow to leave zero findings, got: ${JSON.stringify(findings)}`);
assert.ok(
fs.readFileSync(vendoredAbs).equals(fs.readFileSync(upstreamAbs)),
'expected the vendored .cjs to byte-equal node_modules/js-yaml/dist/js-yaml.js after fixRow',
);
});
test('fixRow does NOT mask a genuine hand-authored-twin incompatibility: a fake declared export still surfaces after --fix', (t) => {
const row = jsYamlRow();
const srcTwinAbs = path.join(REPO_ROOT, row.srcTwin);
const original = fs.readFileSync(srcTwinAbs, 'utf8');
fs.writeFileSync(
srcTwinAbs,
`${original}\nexport function thisFixRowExportDoesNotExistAtRuntime(): void;\n`,
);
t.after(() => {
fs.writeFileSync(srcTwinAbs, original);
});
const findings = fixRow(row);
assert.ok(
findings.some((f) => f.includes('thisFixRowExportDoesNotExistAtRuntime')),
`expected the hand-authored-twin finding to survive fixRow, got: ${JSON.stringify(findings)}`,
);
});
test('fixRow preserves the pin\'s original range-operator style when rewriting package.json', (t) => {
const row = jsYamlRow();
const pkgPath = path.join(REPO_ROOT, 'package.json');
const installedPkgPath = path.join(REPO_ROOT, 'node_modules', 'js-yaml', 'package.json');
const installedVersion = JSON.parse(fs.readFileSync(installedPkgPath, 'utf8')).version;
const originalContent = fs.readFileSync(pkgPath, 'utf8');
t.after(() => {
fs.writeFileSync(pkgPath, originalContent);
});
const pkg = JSON.parse(originalContent);
const stalePin = installedVersion === '4.0.0' ? '~4.0.1' : '~4.0.0';
pkg.devDependencies['js-yaml'] = stalePin;
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
const findings = fixRow(row);
assert.deepEqual(findings, [], `expected fixRow to leave zero findings, got: ${JSON.stringify(findings)}`);
const after = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
const pinnedAfter = after.devDependencies['js-yaml'];
assert.equal(
pinnedAfter,
`~${installedVersion}`,
`expected fixRow to preserve the "~" range-operator style from the stale pin and correct the version to the installed one, got pin "${pinnedAfter}"`,
);
});
});