From b573590a964ca91be79c5dab51dbef0881a6a12f Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 28 Aug 2026 10:19:37 +0300 Subject: [PATCH] =?UTF-8?q?ci:=20a=20force-push=20runs=20everything=20?= =?UTF-8?q?=E2=80=94=20the=20classifier=20stops=20guessing=20a=20rewritten?= =?UTF-8?q?=20range=20(#347)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit github.event.before dies with a force-push, and the merge-base fallback then guessed a diff range: on issue/333 it reported two review-doc files while the real diff touched custom_components/** — frontend and backend jobs silently skipped and the run stayed success, the exact #171/#207 class of silent pass that nearly hid a genuine backend regression from code review. The classifier now distinguishes the two fallback cases instead of merging them: a ZERO before is a genuinely new branch and keeps the merge-base range; a NON-ZERO before that no longer exists is a rewritten history, and the range is not provable — frontend/backend/integration all go true, with a loud note in the step summary. A force-push is rare and almost always follows a rebase, where the full run is what an honest signal costs. The three branches of the decision are pinned by a workflow-contract unit next to the existing performance-workflow contracts. Issue: #347 User-Visible: no --- .github/workflows/validate.yml | 22 ++++++++++++++++++++-- test/performance-workflow.test.mjs | 23 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) 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 — без повторной проверки существования'); +});