mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Windows portability (the three red tests on the owner's machine at green CI):
- scripts/spawn-portable.mjs: isMainModule via pathToFileURL (the
`file://${argv[1]}` form gives file:///C:/C:/... and the CLI stays silent);
portableCommand — a shell only for npm/npx/.cmd, node and git run directly
(spawn via shell dropped the quotes of `node -e "…"`). Applied to
classify-changes, mutation-gate-report, review-doc-guard, check-inputs,
merge-candidate, gate-small, rebase-on-dev.
- the rebase-on-dev test pins core.autocrlf=false / core.eol=lf through
GIT_CONFIG_* for its temp repository instead of touching the user's git
config; test/windows-portability.test.mjs forbids both anti-patterns.
Pins: scripts/toolchain-pins.mjs reads Node/Python from validate.yml, the HA
stack from tests_backend/requirements.txt, Playwright/Chromium from the
lockfile — no second dictionary; `npm run toolchain:check` compares the
machine; .nvmrc/.python-version are derived and tested equal;
scripts/wsl-setup.sh provisions WSL/Linux with those pins.
scripts/task-packet.mjs: one derived view of an issue (status, rights, owner
decisions, branch vs dev, Validate on the tip, previous verdict with recorded
tree, AC → evidence, unwitnessed). scripts/wait-verdict.mjs: polls labels,
pipeline comments and optionally Validate, prints only on state change, exit
0/3/4, writes nothing.
gate:small --smokes: after build the browser phase runs bundle-sync and then
the directly matched and registered smokes, two at a time; broad matches stay
with the reviewer. package.json changed, so the bundle is rebuilt here.
Issue: #496
User-Visible: no
123 lines
6.8 KiB
JavaScript
123 lines
6.8 KiB
JavaScript
#!/usr/bin/env node
|
||
// Пины toolchain — один источник, тот же, что у CI (#496).
|
||
//
|
||
// Локальный контур расходился с CI молча: Node 24 против 22, Python 3.13 в
|
||
// документах против 3.14 в workflow, Home Assistant «какой поставился». Здесь
|
||
// пины НЕ объявляются — они ЧИТАЮТСЯ из файлов, которыми живёт CI:
|
||
// Node — `node-version` в .github/workflows/validate.yml
|
||
// Python — `python-version` там же
|
||
// HA-стек — tests_backend/requirements.txt (homeassistant==, плагин pytest)
|
||
// Playwright — package-lock.json, Chromium — browsers.json самого Playwright
|
||
// Второго словаря версий не появляется; `.nvmrc` и `.python-version` обязаны
|
||
// совпадать с этим чтением (тест), чтобы nvm/uv/pyenv подхватывали их сами.
|
||
//
|
||
// node scripts/toolchain-pins.mjs # таблица пинов
|
||
// node scripts/toolchain-pins.mjs --json # для скриптов установки
|
||
// node scripts/toolchain-pins.mjs --check # локальное окружение против пинов
|
||
|
||
import { spawnSync } from 'node:child_process';
|
||
import { existsSync, readFileSync } from 'node:fs';
|
||
import { dirname, resolve } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { isMainModule } from './spawn-portable.mjs';
|
||
|
||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||
const read = (rel) => readFileSync(resolve(ROOT, rel), 'utf8');
|
||
|
||
/** Единственное значение среди совпадений; разнобой — ошибка, а не «первое». */
|
||
function single(values, what) {
|
||
const unique = [...new Set(values)];
|
||
if (unique.length !== 1) throw new Error(`${what}: ожидался один пин, найдено ${unique.length} (${unique.join(', ') || 'ни одного'})`);
|
||
return unique[0];
|
||
}
|
||
|
||
export function pinsFromSources({
|
||
validateYml = read('.github/workflows/validate.yml'),
|
||
requirements = read('tests_backend/requirements.txt'),
|
||
packageLock = JSON.parse(read('package-lock.json')),
|
||
browsers = JSON.parse(read('node_modules/playwright-core/browsers.json')),
|
||
} = {}) {
|
||
const node = single([...validateYml.matchAll(/node-version:\s*['"]?(\d+)['"]?/g)].map((m) => m[1]), 'node-version');
|
||
const python = single([...validateYml.matchAll(/python-version:\s*['"]?([\d.]+)['"]?/g)].map((m) => m[1]), 'python-version');
|
||
const req = (name) => single([...requirements.matchAll(new RegExp(`^${name.replace(/-/g, '\\-')}==([^\\s#]+)`, 'gm'))].map((m) => m[1]), name);
|
||
const chromium = browsers.browsers.find((b) => b.name === 'chromium');
|
||
return {
|
||
node,
|
||
python,
|
||
homeassistant: req('homeassistant'),
|
||
pytestHomeAssistant: req('pytest-homeassistant-custom-component'),
|
||
playwright: packageLock.packages['node_modules/playwright'].version,
|
||
chromium: chromium ? { revision: chromium.revision, version: chromium.browserVersion } : null,
|
||
};
|
||
}
|
||
|
||
function run(cmd, args) {
|
||
const r = spawnSync(cmd, args, { encoding: 'utf8' });
|
||
if (r.error || r.status !== 0) return null;
|
||
return `${r.stdout || ''}${r.stderr || ''}`.trim();
|
||
}
|
||
|
||
/** Что установлено локально; `null` — не найдено. */
|
||
export function localToolchain({ exec = run } = {}) {
|
||
const node = process.versions.node;
|
||
const pyOut = exec('python', ['--version']) || exec('python3', ['--version']) || exec('py', ['-3', '--version']);
|
||
const python = pyOut ? (pyOut.match(/(\d+\.\d+(?:\.\d+)?)/) || [])[1] || null : null;
|
||
const pipShow = (name) => {
|
||
const out = exec('python', ['-m', 'pip', 'show', name]) || exec('python3', ['-m', 'pip', 'show', name]);
|
||
return out ? (out.match(/^Version:\s*(\S+)/m) || [])[1] || null : null;
|
||
};
|
||
let playwright = null;
|
||
try { playwright = JSON.parse(read('node_modules/playwright/package.json')).version; } catch { /* нет */ }
|
||
return {
|
||
node, python,
|
||
homeassistant: pipShow('homeassistant'),
|
||
pytestHomeAssistant: pipShow('pytest-homeassistant-custom-component'),
|
||
playwright,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Сравнение: `{ ok, lines, failures }`. Node — по мажору, Python — по minor,
|
||
* HA-стек — точно, но только если установлен (без него — предупреждение:
|
||
* харнесс HA на Windows и так невозможен, fcntl).
|
||
*/
|
||
export function compareToolchain(pins, local) {
|
||
const lines = []; const failures = [];
|
||
const row = (name, want, have, ok, note = '') => {
|
||
lines.push(`${ok ? 'ok ' : 'FAIL'} ${name.padEnd(14)} пин ${String(want).padEnd(12)} локально ${String(have ?? '—').padEnd(12)}${note}`);
|
||
if (!ok) failures.push(name);
|
||
};
|
||
row('node', pins.node, local.node, String(local.node).split('.')[0] === String(pins.node),
|
||
' (сравнение по мажору; .nvmrc)');
|
||
const pyMinor = (v) => (v ? v.split('.').slice(0, 2).join('.') : null);
|
||
row('python', pins.python, local.python, pyMinor(local.python) === pyMinor(pins.python),
|
||
' (сравнение по minor; .python-version)');
|
||
for (const [key, name] of [['homeassistant', 'homeassistant'], ['pytestHomeAssistant', 'pytest-ha-plugin']]) {
|
||
if (local[key] == null) {
|
||
lines.push(`warn ${name.padEnd(14)} пин ${String(pins[key]).padEnd(12)} локально — (не установлен: полный HA-харнесс — Linux/WSL)`);
|
||
continue;
|
||
}
|
||
row(name, pins[key], local[key], local[key] === pins[key]);
|
||
}
|
||
row('playwright', pins.playwright, local.playwright, local.playwright === pins.playwright, ' (npm ci ставит из lockfile)');
|
||
return { ok: failures.length === 0, lines, failures };
|
||
}
|
||
|
||
if (isMainModule(import.meta.url)) {
|
||
const argv = process.argv.slice(2);
|
||
const pins = pinsFromSources();
|
||
if (argv.includes('--json')) {
|
||
process.stdout.write(`${JSON.stringify(pins, null, 2)}\n`);
|
||
} else if (argv.includes('--check')) {
|
||
const result = compareToolchain(pins, localToolchain());
|
||
for (const line of result.lines) console.log(line);
|
||
console.log(result.ok
|
||
? 'toolchain совпадает с CI'
|
||
: `toolchain расходится с CI: ${result.failures.join(', ')} — установка: docs/DEVELOPMENT.md, WSL: scripts/wsl-setup.sh`);
|
||
process.exitCode = result.ok ? 0 : 1;
|
||
} else {
|
||
console.log(`Node ${pins.node} · Python ${pins.python} · homeassistant ${pins.homeassistant} · pytest-homeassistant-custom-component ${pins.pytestHomeAssistant} · Playwright ${pins.playwright} · Chromium ${pins.chromium?.version} (rev ${pins.chromium?.revision})`);
|
||
console.log('источники: .github/workflows/validate.yml, tests_backend/requirements.txt, package-lock.json, playwright-core/browsers.json');
|
||
}
|
||
}
|