mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
ci(mutants): skip the nightly registry on a proven tree; install only the shard's environment
#620. Mutants were ~70 % of CI machine time. 1. mutation-gate.yml: a green full run (aggregator verified all six shards) leaves a cache marker keyed by material tree + workflow SHA. A scheduled night with the same tree and workflow, marker not older than 7 days, skips the shards and writes "reused from run N" to the run summary. Red runs leave no marker, so the next night runs again and still files the issue (#472). Manual dispatch always runs the full registry. Decision is a pure function in scripts/mutation-nightly-reuse.mjs. 2. changed_mutants: the shard plan (already computed before setup, #518) now names the environment of its guards (plan-browser=/plan-python=, from scripts/mutation-environment.mjs). Python + backend deps only for shards with pytest guards, Chromium only for shards whose guards reach Playwright; pip wheels cached. Every shard still runs and reports, so the six mutant jobs of the review proof are unchanged. Item 3 of the issue (smoke guards -> node --test) is out of scope here. Issue: #620 User-Visible: no
This commit is contained in:
@@ -19,6 +19,13 @@ name: Мутационный гейт
|
||||
# tsc), а реестр режется на четыре чересполосных шарда — полный прогон
|
||||
# укладывается в десятки минут вместо часов. Локальный дифф-режим:
|
||||
# node scripts/mutation-gate.mjs --changed origin/dev..HEAD
|
||||
#
|
||||
# #620: ночь по расписанию на дереве, уже доказанном зелёным полным прогоном
|
||||
# (тот же tree материала, тот же SHA workflow, маркер не старше недели), шарды не
|
||||
# гоняет — в сводке «reused from run N». Маркер пишет только зелёный агрегатор,
|
||||
# поэтому красный не переносится: следующая ночь гонит реестр заново и снова
|
||||
# заводит issue (#472). Ручной dispatch гонит полный реестр всегда. Решение —
|
||||
# чистая функция scripts/mutation-nightly-reuse.mjs.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -54,6 +61,8 @@ jobs:
|
||||
sha: ${{ steps.identity.outputs.sha }}
|
||||
tree: ${{ steps.identity.outputs.tree }}
|
||||
ref: ${{ steps.identity.outputs.ref }}
|
||||
reuse: ${{ steps.reuse.outputs.reuse }}
|
||||
reused_run: ${{ steps.reuse.outputs.reused_run }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
@@ -65,10 +74,48 @@ jobs:
|
||||
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
echo "tree=$(git rev-parse 'HEAD^{tree}')" >> "$GITHUB_OUTPUT"
|
||||
echo "ref=${{ github.event_name == 'workflow_dispatch' && inputs.ref || 'dev' }}" >> "$GITHUB_OUTPUT"
|
||||
# #620: самый свежий маркер зелёного прогона этого tree и этого workflow.
|
||||
# Ключ уникален на прогон (кэш неизменяем), восстанавливается по префиксу —
|
||||
# иначе перепроверка после недели не смогла бы освежить маркер.
|
||||
- name: Маркер зелёного прогона этого дерева
|
||||
if: github.event_name == 'schedule'
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
with:
|
||||
path: artifacts/mutation-green
|
||||
key: mutation-green-v1-${{ steps.identity.outputs.tree }}-${{ github.workflow_sha }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
mutation-green-v1-${{ steps.identity.outputs.tree }}-${{ github.workflow_sha }}-
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 22
|
||||
# Любой сбой решения — полный прогон: пропуск должен быть доказан, а не
|
||||
# выведен из ошибки. Материал без скрипта (dispatch старого ref) — тоже.
|
||||
- name: Нужен ли прогон
|
||||
id: reuse
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
TREE: ${{ steps.identity.outputs.tree }}
|
||||
WORKFLOW_SHA: ${{ github.workflow_sha }}
|
||||
RUN_URL_BASE: ${{ github.server_url }}/${{ github.repository }}/actions/runs
|
||||
run: |
|
||||
decision="$RUNNER_TEMP/mutation-reuse.txt"
|
||||
if [ -f scripts/mutation-nightly-reuse.mjs ] && node scripts/mutation-nightly-reuse.mjs --decide \
|
||||
--event="$EVENT_NAME" --tree="$TREE" --workflow-sha="$WORKFLOW_SHA" \
|
||||
--marker=artifacts/mutation-green/marker.json \
|
||||
--summary="$GITHUB_STEP_SUMMARY" --run-url-base="$RUN_URL_BASE" > "$decision"; then
|
||||
cat "$decision" >> "$GITHUB_OUTPUT"
|
||||
if grep -qx 'reuse=true' "$decision"; then
|
||||
echo "::notice::reused from run $(sed -n 's/^reused_run=//p' "$decision")"
|
||||
fi
|
||||
else
|
||||
echo "::warning::решение о повторном использовании не получено — полный прогон"
|
||||
echo "reuse=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
mutants:
|
||||
name: "Мутанты: каждый обязан красить тесты (шард ${{ matrix.shard }} из 6)"
|
||||
needs: material
|
||||
if: needs.material.outputs.reuse != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -164,7 +211,7 @@ jobs:
|
||||
evidence:
|
||||
name: "Доказать единый material всех шардов"
|
||||
needs: [material, mutants]
|
||||
if: always()
|
||||
if: always() && needs.material.outputs.reuse != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
@@ -189,6 +236,36 @@ jobs:
|
||||
--workflow-sha=${{ github.workflow_sha }} \
|
||||
--run-id=${{ github.run_id }} --run-attempt=${{ github.run_attempt }}
|
||||
|
||||
# #620: маркер пишется ТОЛЬКО после зелёного агрегатора — все шесть шардов
|
||||
# доказаны на одном material. Красный или неполный прогон маркера не оставляет,
|
||||
# и следующая ночь гонит реестр заново.
|
||||
green_marker:
|
||||
name: "Записать маркер зелёного прогона"
|
||||
needs: [material, mutants, evidence]
|
||||
if: needs.material.outputs.reuse != 'true' && needs.mutants.result == 'success' && needs.evidence.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ needs.material.outputs.sha }}
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Маркер
|
||||
run: |
|
||||
node scripts/mutation-nightly-reuse.mjs \
|
||||
--write-marker=artifacts/mutation-green/marker.json \
|
||||
--tree=${{ needs.material.outputs.tree }} \
|
||||
--sha=${{ needs.material.outputs.sha }} \
|
||||
--workflow-sha=${{ github.workflow_sha }} \
|
||||
--run-id=${{ github.run_id }} --run-attempt=${{ github.run_attempt }} \
|
||||
--event=${{ github.event_name }}
|
||||
- name: Сохранить маркер
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
with:
|
||||
path: artifacts/mutation-green
|
||||
key: mutation-green-v1-${{ needs.material.outputs.tree }}-${{ github.workflow_sha }}-${{ github.run_id }}
|
||||
|
||||
# Адресат у отказа (#472). Только по расписанию: ручной dispatch остаётся
|
||||
# для отладки самого гейта, его результат смотрят в прогоне — issue на
|
||||
# каждый такой отказ был бы шумом, который снова перестанут читать.
|
||||
@@ -198,7 +275,10 @@ jobs:
|
||||
report:
|
||||
name: "Отказ расписания: issue и Telegram"
|
||||
needs: [material, mutants, evidence]
|
||||
if: always() && github.event_name == 'schedule' && (needs.mutants.result != 'success' || needs.evidence.result != 'success')
|
||||
# #620: ночь, принявшая доказательство прошлого зелёного прогона, отказом не
|
||||
# является — её шарды пропущены намеренно. Любой другой пропуск шардов
|
||||
# (упал material) по-прежнему заводит issue.
|
||||
if: always() && github.event_name == 'schedule' && needs.material.outputs.reuse != 'true' && (needs.mutants.result != 'success' || needs.evidence.result != 'success')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -652,29 +652,47 @@ jobs:
|
||||
fi
|
||||
echo "диапазон: $base..$HEAD_SHA"
|
||||
echo "base=$base" >> "$GITHUB_OUTPUT"
|
||||
count=$(node scripts/mutation-gate.mjs --changed="$base..$HEAD_SHA" --shard="$SHARD/6" \
|
||||
--ledger="artifacts/mutation-ledger/shard-$SHARD.json" --plan-only \
|
||||
| tee /dev/stderr | sed -n 's/^plan=//p' | tail -1)
|
||||
plan="$RUNNER_TEMP/mutation-plan-$SHARD.txt"
|
||||
node scripts/mutation-gate.mjs --changed="$base..$HEAD_SHA" --shard="$SHARD/6" \
|
||||
--ledger="artifacts/mutation-ledger/shard-$SHARD.json" --plan-only > "$plan"
|
||||
cat "$plan"
|
||||
count=$(sed -n 's/^plan=//p' "$plan" | tail -1)
|
||||
echo "к прогону в этом шарде: ${count:-0}"
|
||||
echo "count=${count:-0}" >> "$GITHUB_OUTPUT"
|
||||
# #620: план называет и окружение своих гардов. Непрочитанная строка
|
||||
# при непустом плане значит «ставить»: лишняя установка стоит минуты,
|
||||
# недоустановка — красный чистый прогон и лишний круг задачи.
|
||||
if [ "${count:-0}" != "0" ]; then
|
||||
browser=$(sed -n 's/^plan-browser=//p' "$plan" | tail -1)
|
||||
python=$(sed -n 's/^plan-python=//p' "$plan" | tail -1)
|
||||
echo "окружение шарда: браузер ${browser:-true}, Python ${python:-true}"
|
||||
echo "browser=${browser:-true}" >> "$GITHUB_OUTPUT"
|
||||
echo "python=${python:-true}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- if: steps.plan.outputs.count != '0'
|
||||
run: npm ci
|
||||
- if: steps.plan.outputs.count != '0'
|
||||
# Python и зависимости бэкенда — только шарду с pytest-гардами (#620);
|
||||
# колёса pip кэшируются по requirements.txt.
|
||||
- if: steps.plan.outputs.count != '0' && steps.plan.outputs.python == 'true'
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
||||
with:
|
||||
python-version: '3.14'
|
||||
cache: pip
|
||||
cache-dependency-path: tests_backend/requirements.txt
|
||||
- name: Backend test dependencies
|
||||
if: steps.plan.outputs.count != '0'
|
||||
if: steps.plan.outputs.count != '0' && steps.plan.outputs.python == 'true'
|
||||
run: pip install -r tests_backend/requirements.txt
|
||||
# Chromium — только шарду с браузерными гардами (#620): смоки, golden и
|
||||
# юниты, чей граф исполнения импортирует Playwright.
|
||||
- name: Кэш браузеров Playwright
|
||||
id: pw
|
||||
if: steps.plan.outputs.count != '0'
|
||||
if: steps.plan.outputs.count != '0' && steps.plan.outputs.browser == 'true'
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||||
- name: Установить Chromium
|
||||
if: steps.plan.outputs.count != '0' && steps.pw.outputs.cache-hit != 'true'
|
||||
if: steps.plan.outputs.count != '0' && steps.plan.outputs.browser == 'true' && steps.pw.outputs.cache-hit != 'true'
|
||||
run: npx playwright install --with-deps chromium
|
||||
- name: Затронутые мутанты ловятся
|
||||
if: steps.plan.outputs.count != '0'
|
||||
|
||||
Reference in New Issue
Block a user