diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 72adefc4..a7b16679 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -165,10 +165,28 @@ jobs: zero=$(printf '%040d' 0) base="$BEFORE_SHA" if [ "$EVENT_NAME" = "pull_request" ]; then base="$BASE_SHA"; fi + # #347: непустой before, которого больше не существует, — это + # force-push (ребейз/аменд переписал историю ветки). Диапазон в + # этом случае НЕ угадывается: merge-base после переписанной истории + # уже выдавал «изменились только два doc-файла» при реальном диффе + # в custom_components/**, и тяжёлые job тихо скипались, а общий + # статус рана оставался success — ровно класс тихого пропуска + # #171/#207. Force-push редок, полный прогон дешевле ложного + # «зелёного». + if [ -n "$base" ] && [ "$base" != "$zero" ] \ + && ! git cat-file -e "$base" 2>/dev/null; then + echo "force-push: before=$base переписан — полный прогон без классификации" + { + echo "### Классификация изменённых файлов" + echo "\`before\` (=$base) переписан force-push'ем: диапазон недоказуем," + echo "все тяжёлые job запускаются без фильтра путей (#347)." + } >> "$GITHUB_STEP_SUMMARY" + printf 'frontend=true\nbackend=true\nintegration=true\n' >> "$GITHUB_OUTPUT" + exit 0 + fi # Новая ветка: before нулевой, диапазон считается от merge-base с dev, # иначе классифицировалась бы вся история. - if [ -z "$base" ] || [ "$base" = "$zero" ] \ - || ! git cat-file -e "$base" 2>/dev/null; then + if [ -z "$base" ] || [ "$base" = "$zero" ]; then git fetch -q origin dev base=$(git merge-base origin/dev "$HEAD_SHA" || echo "$HEAD_SHA~1") fi diff --git a/test/performance-workflow.test.mjs b/test/performance-workflow.test.mjs index 065acd10..97b48703 100644 --- a/test/performance-workflow.test.mjs +++ b/test/performance-workflow.test.mjs @@ -46,3 +46,26 @@ test('full performance is isolated to stable, scheduled and manual entry points' assert.ok(release.includes('if: ${{ !github.event.release.prerelease }}')); assert.ok(release.includes('--workflow=performance.yml --label="Полные бенчмарки производительности"')); }); + +test('#347: a rewritten before forces the full run instead of guessing the range', () => { + // Force-push kills github.event.before; the merge-base fallback then + // guessed a range that hid a real custom_components/** diff behind two doc + // files, and the heavy jobs silently skipped while the run stayed green — + // the #171/#207 class of silent pass. The contract: a non-zero before that + // no longer exists switches classification to an unconditional full run + // with a loud step-summary note, and the merge-base fallback remains ONLY + // for the genuinely new branch (zero before). + const workflow = readWorkflow('validate.yml'); + const classify = workflow.slice( + workflow.indexOf('Классификация изменённых файлов'), + workflow.indexOf('reuse:'), + ); + assert.ok( + /force-push[\s\S]*?frontend=true[\s\S]*?backend=true[\s\S]*?integration=true/.test(classify), + 'мёртвый before обязан включать полный прогон, не merge-base-угадывание'); + assert.ok(classify.includes('GITHUB_STEP_SUMMARY'), + 'пропуск классификации обязан быть громким в summary'); + const fallback = classify.slice(classify.indexOf('Новая ветка')); + assert.ok(!fallback.includes('cat-file'), + 'merge-base-фолбэк остаётся только для нулевого before — без повторной проверки существования'); +});