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');