fix(#3020): verify graphify tool identity before reporting compatibility (#3107)

* fix(#3020): verify graphify tool identity before reporting compatibility

checkGraphifyVersion ran 'graphify --version' on PATH and trusted any
plausible version string — a foreign binary named 'graphify' that happened
to print a version would silently report compatible:true with no warning.
Downstream graphify work would then proceed against the wrong tool and fail
later in ways that looked like GSD defects.

After getting a version from the binary, verify the graphifyy Python package
via importlib.metadata. If the package cannot be confirmed, emit a warning
naming the mismatch regardless of version-range compatibility. The
compatible flag is now correctly false for unverified tools (it was
previously read by nobody — the warning is what surfaces).

* chore(#3020): backfill changeset PR number 3107

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-06 02:56:07 -04:00
committed by GitHub
parent 8fb6681a1e
commit d6cce9e2c3
3 changed files with 65 additions and 18 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3107
---
**`graphify` version detection now verifies tool identity** — a foreign binary named `graphify` on PATH that printed a plausible version string would silently report `compatible: true` with no warning. The check now confirms the `graphifyy` Python package via `importlib.metadata` before trusting the version, emitting a clear warning naming the mismatch when identity cannot be confirmed. (#3020)

View File

@@ -161,6 +161,7 @@ function checkGraphifyVersion(): VersionResult {
}
// Strategy 2: fall back to python3 importlib.metadata
let pyPackageConfirmed = false;
if (!versionStr) {
const pyResult = execTool('python3', [
'-c',
@@ -168,8 +169,19 @@ function checkGraphifyVersion(): VersionResult {
], { timeout: 5000 });
if (!pyResult.error && pyResult.exitCode === 0 && pyResult.stdout) {
versionStr = pyResult.stdout;
versionStr = pyResult.stdout.trim();
pyPackageConfirmed = true; // importlib.metadata confirmed the package
}
} else {
// #3020: verify the `graphify` binary on PATH is actually the graphifyy
// package — a foreign binary that happens to print a version-like string
// must not silently report compatible. If importlib.metadata cannot confirm
// the package, emit an identity warning even if the version looks right.
const pyVerify = execTool('python3', [
'-c',
'from importlib.metadata import version; print(version("graphifyy"))',
], { timeout: 5000 });
pyPackageConfirmed = !pyVerify.error && pyVerify.exitCode === 0 && !!pyVerify.stdout;
}
if (!versionStr) {
@@ -182,10 +194,22 @@ function checkGraphifyVersion(): VersionResult {
return { version: versionStr, compatible: null, warning: 'Could not parse version: ' + versionStr };
}
const compatible = parts[0] === 0 && parts[1] >= 4;
const warning = compatible ? null : 'graphify version ' + versionStr + ' is outside tested range >=0.4.0,<1.0';
const versionInRange = parts[0] === 0 && parts[1] >= 4;
return { version: versionStr, compatible, warning };
// #3020: if the `graphify` binary answered --version but the Python package
// graphifyy could not be confirmed, the tool identity is unverified — emit
// a warning naming the mismatch regardless of version-range compatibility.
if (!pyPackageConfirmed) {
return {
version: versionStr,
compatible: false,
warning: 'graphify version ' + versionStr + ' detected but the graphifyy Python package could not be confirmed — the `graphify` binary on PATH may be a different tool. Verify with: pip show graphifyy',
};
}
const warning = versionInRange ? null : 'graphify version ' + versionStr + ' is outside tested range >=0.4.0,<1.0';
return { version: versionStr, compatible: versionInRange, warning };
}
// ─── Internal Helpers ────────────────────────────────────────────────────────

View File

@@ -604,9 +604,11 @@ describe('build', () => {
});
test('returns compatible:true for version 0.4.0', () => {
mock.method(childProcess, 'spawnSync', () => ({
// #3020: command-aware mock — graphify --version returns the version,
// python3 importlib.metadata confirms the graphifyy package.
mock.method(childProcess, 'spawnSync', (cmd) => ({
status: 0,
stdout: '0.4.0\n',
stdout: cmd === 'graphify' ? '0.4.0\n' : '0.4.0\n',
stderr: '',
error: undefined,
signal: null,
@@ -619,9 +621,9 @@ describe('build', () => {
});
test('returns compatible:true for version 0.9.5', () => {
mock.method(childProcess, 'spawnSync', () => ({
mock.method(childProcess, 'spawnSync', (cmd) => ({
status: 0,
stdout: '0.9.5\n',
stdout: cmd === 'graphify' ? '0.9.5\n' : '0.9.5\n',
stderr: '',
error: undefined,
signal: null,
@@ -633,9 +635,9 @@ describe('build', () => {
});
test('returns compatible:false for version 0.3.0', () => {
mock.method(childProcess, 'spawnSync', () => ({
mock.method(childProcess, 'spawnSync', (cmd) => ({
status: 0,
stdout: '0.3.0\n',
stdout: cmd === 'graphify' ? '0.3.0\n' : '0.3.0\n',
stderr: '',
error: undefined,
signal: null,
@@ -647,9 +649,9 @@ describe('build', () => {
});
test('returns compatible:false for version 1.0.0', () => {
mock.method(childProcess, 'spawnSync', () => ({
mock.method(childProcess, 'spawnSync', (cmd) => ({
status: 0,
stdout: '1.0.0\n',
stdout: cmd === 'graphify' ? '1.0.0\n' : '1.0.0\n',
stderr: '',
error: undefined,
signal: null,
@@ -688,19 +690,35 @@ describe('build', () => {
assert.ok(result.warning.includes('Could not parse'));
});
test('tries graphify --version first before python3', () => {
test('#3020: warns when graphifyy package cannot be confirmed (foreign binary)', () => {
// graphify --version succeeds with a plausible version, but python3
// importlib.metadata cannot find the graphifyy package — identity unverified.
mock.method(childProcess, 'spawnSync', (cmd) => {
if (cmd === 'graphify') {
return { status: 0, stdout: '0.4.3\n', stderr: '', error: undefined, signal: null };
}
// python3 importlib.metadata fails — package not found
return { status: 1, stdout: '', stderr: 'PackageNotFoundError', error: undefined, signal: null };
});
const result = checkGraphifyVersion();
assert.strictEqual(result.version, '0.4.3');
assert.strictEqual(result.compatible, false, 'foreign binary must not report compatible');
assert.ok(result.warning && result.warning.includes('graphifyy'), 'warning must name the package mismatch');
});
test('tries graphify --version first, then verifies identity via python3', () => {
const calls = [];
mock.method(childProcess, 'spawnSync', (cmd, args) => {
calls.push({ cmd, args });
return { status: 0, stdout: '0.4.3\n', stderr: '', error: undefined, signal: null };
return { status: 0, stdout: cmd === 'graphify' ? '0.4.3\n' : '0.4.3\n', stderr: '', error: undefined, signal: null };
});
checkGraphifyVersion();
assert.strictEqual(calls.length, 1, 'exactly one spawnSync call — no python3 fallback');
assert.strictEqual(calls[0].cmd, 'graphify');
assert.strictEqual(calls.length, 2, 'graphify --version + python3 identity verification (#3020)');
assert.strictEqual(calls[0].cmd, 'graphify', 'first call is graphify --version');
assert.strictEqual(calls[1].cmd, 'python3', 'second call is python3 identity check (#3020)');
assert.ok(calls[0].args.includes('--version'), 'graphify called with --version');
const python3Calls = calls.filter(c => c.cmd === 'python3');
assert.strictEqual(python3Calls.length, 0, 'no python3 fallback when graphify --version succeeds');
});
test('falls back to python3 importlib.metadata when graphify --version fails', () => {