From 245b7f984765785045d45115f0f9df8da94a506d Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 16 Sep 2026 21:55:59 +0300 Subject: [PATCH] =?UTF-8?q?ci:=20=D1=81=D1=82=D1=80=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D0=B9=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=20=D1=81=D0=B2=D0=B5?= =?UTF-8?q?=D0=B6=D0=B5=D1=81=D1=82=D0=B8=20=D1=81=D0=BA=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D1=88=D0=BE=D1=82=D0=BE=D0=B2=20=D0=B2=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=BD=D0=B0=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BD=D0=B4=D0=B8=D0=B4=D0=B0=D1=82=D0=B5=20(#586)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверка #479 обязана быть строгой на кандидате беты и на релизном гейте. Фактически она не была строгой ни разу: preflight сравнивал со строкой `heavy=true` ВЕСЬ вывод `classify-changes.mjs --heavy`, а вывод двухстрочный — `heavy=…` и `mutants_requested=…`. В `$(…)` строки схлопываются через пробел, сравнение не совпадало никогда, режим оставался `warn`. Видно построчно в логе прогона 35091507839 на кандидате `4c44ef60`: скриншоты документации: режим warn (heavy=true WARN screenshot source fingerprint is stale; ... ok документация То есть проверка увидела устаревший индекс и пропустила кандидата. Обе беты после `699ab471` уехали с ним; на чистом checkout того же SHA `node scripts/check-docs.mjs --strict` падает с ERROR. Правило, которое из этого следует: формат `$GITHUB_OUTPUT` — построчный `ключ=значение`, читать его надо по ключу либо не читать вовсе. Где нужен один ответ, CLI отдаёт один ответ: `--screenshots-mode` печатает `warn` или `strict`, и в shell не остаётся ни разбора, ни развилки. Свидетели в `test/classify-changes.test.mjs`: режим по каждому событию, форма вызова в workflow (сравнение со строкой `heavy=true` не должно вернуться) и прямая проверка того, что вывод `--heavy` многострочный — то есть целиком сравнивать его нельзя. Мутант `screenshot-freshness-never-strict` возвращает прежнее «никогда не strict» и обязан краснеть. Issue: #586 User-Visible: no --- .github/workflows/validate.yml | 10 ++++++---- scripts/classify-changes.mjs | 29 ++++++++++++++++++++++++++++- scripts/mutation-registry.mjs | 15 +++++++++++++++ test/classify-changes.test.mjs | 30 ++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 89acf3f9..6241e9b9 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -82,10 +82,12 @@ jobs: HEAD_MESSAGE: ${{ github.event.head_commit.message }} FULL_INPUT: ${{ inputs.full }} run: | - heavy=$(node scripts/classify-changes.mjs --heavy) - mode=warn - [ "$heavy" = "heavy=true" ] && mode=strict - echo "скриншоты документации: режим $mode ($heavy)" + # #586: CLI отдаёт ОДИН ответ. Прежде здесь сравнивался со строкой + # `heavy=true` весь вывод `--heavy`, а он двухстрочный: в `$(…)` + # строки схлопываются через пробел, сравнение не совпадало никогда, + # и строгий режим не включился ни на одном кандидате. + mode=$(node scripts/classify-changes.mjs --screenshots-mode) + echo "скриншоты документации: режим $mode" node scripts/check-docs.mjs --external --screenshots=$mode # Конвейер читает `process.yml` из ветки по умолчанию, поэтому файл обязан diff --git a/scripts/classify-changes.mjs b/scripts/classify-changes.mjs index e4c19c4e..e5bb8759 100755 --- a/scripts/classify-changes.mjs +++ b/scripts/classify-changes.mjs @@ -109,6 +109,25 @@ export function mutantsRequested({ eventName, headMessage, fullInput, mutantsInp return hasReleaseTrailer(headMessage); } +/** + * Режим гейта свежести скриншотов документации (#479, починен в #586). + * + * Значение одно, и считается оно здесь, а не в shell. Прежде preflight сравнивал + * со строкой `heavy=true` ВЕСЬ вывод `--heavy`, а вывод из двух строк + * (`heavy=…` и `mutants_requested=…`) в `$(…)` схлопывается в одну через + * пробел. Сравнение не совпадало никогда: строгий режим не включился ни на + * одном кандидате, проверка деградировала в предупреждение, а предупреждения + * в сводке не видно. Так обе беты после `699ab471` уехали с устаревшим + * индексом кадров. + * + * Правило разбора, которое из этого следует: формат `$GITHUB_OUTPUT` — + * построчный `ключ=значение`, и читать его надо по ключу либо не читать вовсе. + * Где нужен один ответ — CLI отдаёт один ответ. + */ +export function screenshotsGateMode(inputs) { + return heavyGatesRequested(inputs) ? 'strict' : 'warn'; +} + /** Трейлер `Release: vX.Y.Z` в конце сообщения коммита — признак кандидата. */ export function hasReleaseTrailer(message) { return /^Release:\s*v?\d+\.\d+\.\d+\S*\s*$/m.test(String(message || '')); @@ -125,7 +144,15 @@ export function formatOutputs(outputs) { // `file:///C:/C:/...`, CLI считал себя импортированным и молчал. const invokedDirectly = isMainModule(import.meta.url); if (invokedDirectly) { - if (process.argv.includes('--heavy')) { + if (process.argv.includes('--screenshots-mode')) { + // #586: один вопрос — один ответ. Разбирать многострочный вывод в shell + // больше негде и нечем. + process.stdout.write(`${screenshotsGateMode({ + eventName: process.env.EVENT_NAME, + headMessage: process.env.HEAD_MESSAGE, + fullInput: process.env.FULL_INPUT, + })}\n`); + } else if (process.argv.includes('--heavy')) { // Отдельный вызов: у `heavy` другие входы (событие, сообщение head-коммита), // и на dev он нужен даже там, где классификация путей выключена. const heavy = heavyGatesRequested({ diff --git a/scripts/mutation-registry.mjs b/scripts/mutation-registry.mjs index 22809fbd..080d9a58 100644 --- a/scripts/mutation-registry.mjs +++ b/scripts/mutation-registry.mjs @@ -9917,6 +9917,21 @@ const MUTANT_DEFINITIONS = [ replace: " if (settings?.fill_mode === 'never-a-fill-mode') return spaceFill;", }], }, + { + id: 'screenshot-freshness-never-strict', + guard: 'node --test --test-name-pattern="#586" test/classify-changes.test.mjs', + because: '#586: preflight сравнивал со строкой `heavy=true` ВЕСЬ двухстрочный вывод CLI, ' + + 'поэтому строгий режим свежести скриншотов не включился ни на одном кандидате, и обе ' + + 'беты после 699ab471 уехали с устаревшим индексом кадров. Проверка, которая умеет ' + + 'только предупреждать, не гейт', + patches: [{ + file: 'scripts/classify-changes.mjs', + // рантайм-ложь, а не мёртвая ветка (#568): модуль обязан импортироваться + find: ' return heavyGatesRequested(inputs) ? \'strict\' : \'warn\';', + replace: ' return heavyGatesRequested(inputs) && String(inputs?.eventName) === \'mutant-never-an-event\'\n' + + ' ? \'strict\' : \'warn\';', + }], + }, { id: 'stable-candidate-compares-against-itself', guard: 'node --test --test-name-pattern="AC2" test/performance-baseline.test.mjs', diff --git a/test/classify-changes.test.mjs b/test/classify-changes.test.mjs index ff104752..0b824d64 100644 --- a/test/classify-changes.test.mjs +++ b/test/classify-changes.test.mjs @@ -1,8 +1,10 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; import { CHECK_OF_OUTPUT, CLASSIFIERS, OUTPUTS, PERF_PROFILES, classifyAll, classifyChanges, formatOutputs, mutantsRequested, + screenshotsGateMode, } from '../scripts/classify-changes.mjs'; import { manifest } from '../scripts/check-inputs.mjs'; import { fileURLToPath } from 'node:url'; @@ -185,6 +187,34 @@ test('CLI --heavy читает событие и сообщение из окр assert.equal(run({ EVENT_NAME: 'workflow_dispatch', FULL_INPUT: 'false', MUTANTS_INPUT: 'true', HEAD_MESSAGE: '' }), 'heavy=false\nmutants_requested=true'); }); +test('#586: режим гейта скриншотов приходит одним значением и на кандидате строгий', () => { + assert.equal(screenshotsGateMode({ eventName: 'push', headMessage: 'fix: x\n\nIssue: #1\nUser-Visible: no' }), 'warn'); + assert.equal(screenshotsGateMode({ eventName: 'push', headMessage: 'x\n\nRelease: v1.2.3' }), 'strict', 'кандидат беты'); + assert.equal(screenshotsGateMode({ eventName: 'push', headMessage: 'x\n\nRelease: v1.76.0' }), 'strict', 'кандидат стабильного'); + assert.equal(screenshotsGateMode({ eventName: 'workflow_dispatch', fullInput: 'true' }), 'strict'); + assert.equal(screenshotsGateMode({ eventName: 'workflow_dispatch', fullInput: 'false' }), 'warn'); + assert.equal(screenshotsGateMode({ eventName: 'pull_request' }), 'strict'); + assert.equal(screenshotsGateMode({ eventName: 'schedule' }), 'strict'); + + // Регрессия, ради которой заведён #586: у `--heavy` вывод ДВУХСТРОЧНЫЙ, и + // сравнение всего вывода со строкой `heavy=true` не совпадает никогда. + const run = (args, env) => execFileSync(process.execPath, ['scripts/classify-changes.mjs', ...args], { + encoding: 'utf8', env: { ...process.env, ...env }, + }).trim(); + const candidate = { EVENT_NAME: 'push', HEAD_MESSAGE: 'x\n\nRelease: v1.2.3' }; + assert.notEqual(run(['--heavy'], candidate), 'heavy=true', 'вывод --heavy многострочный — сравнивать его целиком нельзя'); + assert.equal(run(['--screenshots-mode'], candidate), 'strict'); + assert.equal(run(['--screenshots-mode'], { EVENT_NAME: 'push', HEAD_MESSAGE: 'fix: x' }), 'warn'); +}); + +test('#586: preflight спрашивает режим одним значением, а не разбирает вывод --heavy', () => { + const workflow = readFileSync(new URL('../.github/workflows/validate.yml', import.meta.url), 'utf8'); + assert.match(workflow, /classify-changes\.mjs --screenshots-mode/); + assert.ok(!/=\s*"heavy=true"/.test(workflow), + 'сравнение со строкой «heavy=true» вернулось — строгий режим снова не включится'); + assert.match(workflow, /check-docs\.mjs --external --screenshots=\$mode/); +}); + test('#510 AC1: мутанты по диффу запрашиваются кандидатом, PR, ночью и по кнопке — не обычным пушем', () => { const t = (env) => mutantsRequested(env); assert.equal(t({ eventName: 'push', headMessage: 'fix: x\n\nIssue: #1\nUser-Visible: no' }), false, 'обычный push');