From 293309355be909092fe8e5769f3f33059769ec19 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 12 Sep 2026 12:53:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=BD=D0=BE=D0=BD=D1=81=20=D1=83=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=D0=B8=D1=82=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D0=B8=20=D0=B0=D1=81?= =?UTF-8?q?=D1=81=D0=B5=D1=82=D0=BE=D0=B2,=20=D0=B0=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D1=80=D1=8F=D0=B4=D0=BE=D0=BC=20=D1=81=20=D0=BD=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Три воркфлоу висели на одном событии `release: published` и бежали параллельно. Анонс выигрывал эту гонку всегда: проверять ему нечего. 12.09 стабильную v1.75.0 объявили в канале в ту же минуту, когда гейт отказал — Full Performance был красный (#537), E2E после него не выполнялся вовсе, ассеты не выкладывались. Подписчики получили сообщение о релизе, страница которого осталась без `houseplan-card.js`. Триггер события снят: у анонса остаются кнопка проверки связи и вызов из воркфлоу. `release.yml` зовёт его после джобы выкладки (`needs: build`), то есть красный гейт или несостоявшаяся выкладка сообщения не рождают. Путь беты не тронут — `publish-prerelease.yml` звал анонс сам и раньше. Мёртвая ветка чтения события из шага убрана вместе с триггером: тело берётся из заметок ветки тега, как в вызове из беты. Issue: #538 User-Visible: no --- .github/workflows/announce.yml | 39 ++++++++++++++-------------------- .github/workflows/release.yml | 17 +++++++++++++++ scripts/mutation-gate.mjs | 24 +++++++++++++++++++++ test/release-workflow.test.mjs | 27 +++++++++++++++++++++++ 4 files changed, 84 insertions(+), 23 deletions(-) diff --git a/.github/workflows/announce.yml b/.github/workflows/announce.yml index 08327464..29ff1de0 100644 --- a/.github/workflows/announce.yml +++ b/.github/workflows/announce.yml @@ -3,9 +3,14 @@ name: Анонс релиза # Stable releases are announced; prereleases are deliberately silent. # workflow_dispatch exists purely as a connectivity test button and therefore # remains allowed to send a test message. +# #538: НЕТ триггера `release: published`. Три воркфлоу висели на одном событии +# и бежали параллельно, а анонсу нечего было проверять — он выигрывал гонку +# всегда. 12.09 v1.75.0 объявили в канале в ту же минуту, когда гейт ассетов +# отказал: релиз остался без `houseplan-card.js`, E2E не выполнялся вовсе, а +# подписчики получили сообщение. Анонс вызывается только после того, как работа +# сделана: `release.yml` зовёт его после выкладки ассетов, +# `publish-prerelease.yml` — после публикации беты. on: - release: - types: [published] workflow_dispatch: {} workflow_call: inputs: @@ -37,7 +42,7 @@ permissions: jobs: telegram: name: Оповещение в Telegram (только стабильные) - if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'release' && github.event.release.prerelease == false) || (github.event_name == 'workflow_call' && inputs.prerelease == false) }} + if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_call' && inputs.prerelease == false) }} runs-on: ubuntu-latest steps: - name: Check out release notes for a reusable call @@ -54,32 +59,20 @@ jobs: INPUT_NAME: ${{ inputs.release_name }} INPUT_URL: ${{ inputs.url }} INPUT_PRE: ${{ inputs.prerelease }} - RELEASE_TAG: ${{ github.event.release.tag_name }} - RELEASE_NAME: ${{ github.event.release.name }} - RELEASE_URL: ${{ github.event.release.html_url }} - RELEASE_PRE: ${{ github.event.release.prerelease }} - # The body goes through env, never through shell interpolation — - # release notes are arbitrary text. - RELEASE_BODY: ${{ github.event.release.body }} EVENT: ${{ github.event_name }} run: | set -euo pipefail if [ "$EVENT" = "workflow_dispatch" ] && [ "$CALLED" != "true" ]; then TEXT="✅ Тест: оповещения о релизах houseplan-card подключены." else - if [ "$CALLED" = "true" ]; then - TAG=$INPUT_TAG - NAME=$INPUT_NAME - URL=$INPUT_URL - PRE=$INPUT_PRE - BODY=$(cat docs/RELEASE-NOTES.md) - else - TAG=$RELEASE_TAG - NAME=$RELEASE_NAME - URL=$RELEASE_URL - PRE=$RELEASE_PRE - BODY=$RELEASE_BODY - fi + # Единственный путь к сообщению о релизе — вызов из воркфлоу, который + # уже закончил работу (#538). Тело берётся из файла заметок ветки + # тега, а не из события: события здесь больше нет. + TAG=$INPUT_TAG + NAME=$INPUT_NAME + URL=$INPUT_URL + PRE=$INPUT_PRE + BODY=$(cat docs/RELEASE-NOTES.md) if [ "$PRE" = "true" ]; then echo "Prerelease Telegram announcement is disabled" exit 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c31f6d2a..6aa9b067 100755 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,6 +96,23 @@ jobs: uses: softprops/action-gh-release@v3 with: files: dist/houseplan-card.js + announce: + # #538: анонс — последнее звено, а не параллельное. Пока он висел на самом + # событии `release: published`, он обгонял гейт: 12.09 v1.75.0 объявили в + # канале в ту же минуту, когда проверка отказала выкладывать ассеты. + # `needs: build` означает, что молчание — это тоже ответ: красный гейт или + # несостоявшаяся выкладка сообщения не рождают. + name: Оповещение о релизе после выкладки + needs: build + uses: ./.github/workflows/announce.yml + with: + reusable: true + tag: ${{ github.event.release.tag_name }} + release_name: ${{ github.event.release.name }} + url: ${{ github.event.release.html_url }} + prerelease: ${{ github.event.release.prerelease }} + ref: ${{ github.event.release.tag_name }} + secrets: inherit hacs-discovery: name: HACS-видимость пре-релиза (порядок бет) # HACS 2.0.x takes the first prerelease in GitHub's response instead of diff --git a/scripts/mutation-gate.mjs b/scripts/mutation-gate.mjs index d97bcacb..813b812c 100644 --- a/scripts/mutation-gate.mjs +++ b/scripts/mutation-gate.mjs @@ -7735,6 +7735,30 @@ const MUTANT_DEFINITIONS = [ replace: ' settled=true; break', }], }, + { + id: 'announce-listens-to-the-release-event-again', + guard: 'node --test test/release-workflow.test.mjs', + because: '#538: пока анонс висел на самом событии `release: published`, он бежал ' + + 'параллельно гейту и обгонял его — проверять ему нечего. 12.09 v1.75.0 объявили в ' + + 'канале в ту же минуту, когда проверка отказала выкладывать ассеты, и снаружи это ' + + 'выглядело обычным релизом', + patches: [{ + file: '.github/workflows/announce.yml', + find: 'on:\n workflow_dispatch: {}', + replace: 'on:\n release:\n types: [published]\n workflow_dispatch: {}', + }], + }, + { + id: 'announce-job-stops-waiting-for-the-assets', + guard: 'node --test test/release-workflow.test.mjs', + because: '#538: зависимость анонса от выкладки — это и есть правило «молчание тоже ' + + 'ответ». Без неё анонс уходит при красном гейте, то есть ровно то, что случилось', + patches: [{ + file: '.github/workflows/release.yml', + find: ' name: Оповещение о релизе после выкладки\n needs: build', + replace: ' name: Оповещение о релизе после выкладки\n if: always()', + }], + }, { id: 'render-invalidation-unknown-key-ignored', guard: 'node --test test/render-invalidation.test.mjs', diff --git a/test/release-workflow.test.mjs b/test/release-workflow.test.mjs index 78de1b86..06fbcdf6 100644 --- a/test/release-workflow.test.mjs +++ b/test/release-workflow.test.mjs @@ -23,3 +23,30 @@ test('#514: the gate dispatches with a token that can reach houseplan-e2e, with const step = workflow.slice(e2e, at(' build:\n')); assert.match(step, /GH_TOKEN: \$\{\{ secrets\.E2E_DISPATCH_TOKEN \|\| secrets\.HP_PROCESS_TOKEN \}\}/); }); + +// #538: анонс — последнее звено выпуска, а не параллельное ему. Пока он висел +// на самом событии `release: published`, гонку он выигрывал всегда: проверять +// ему нечего. 12.09 v1.75.0 объявили в канале в ту же минуту, когда гейт +// отказал выкладывать ассеты, и снаружи это выглядело обычным релизом. +const announce = readFileSync(new URL('../.github/workflows/announce.yml', import.meta.url), 'utf8'); + +test('#538 AC1: событие релиза не может запустить анонс', () => { + const triggers = announce.slice(announce.indexOf('\non:'), announce.indexOf('\npermissions:')); + assert.ok(!/^\s*release:/m.test(triggers), 'в триггерах анонса нет `release:`'); + assert.match(triggers, /^\s*workflow_dispatch:/m, 'кнопка проверки связи остаётся'); + assert.match(triggers, /^\s*workflow_call:/m, 'вызов из воркфлоу остаётся'); + assert.ok(!/github\.event\.release\./.test(announce), + 'мёртвая ветка события не оставлена в шагах'); +}); + +test('#538 AC2: release.yml зовёт анонс после выкладки ассетов', () => { + const job = at(' announce:\n'); + const build = at(' build:\n'); + assert.ok(build < job, 'анонс описан после сборки, а не до неё'); + const block = workflow.slice(job, workflow.indexOf('\n hacs-discovery:')); + assert.match(block, /needs: build/, 'анонс зависит от выкладки ассетов'); + assert.match(block, /uses: \.\/\.github\/workflows\/announce\.yml/); + assert.match(block, /prerelease: \$\{\{ github\.event\.release\.prerelease \}\}/, + 'беты остаются тихими по тому же признаку, что и раньше'); + assert.match(block, /secrets: inherit/); +});