ci: закрепить образ раннера, таймауты job и некруглые cron (#658)

`ubuntu-latest` с 19.10.2026 переезжает на Ubuntu 26, а golden, скриншоты
документации и перф-бюджеты сняты на текущем образе: все 43 job на раннере
теперь явно на `ubuntu-24.04`, один образ на все workflow. 26 job получили
`timeout-minutes` по наблюдённой длительности с запасом; гейт релиза — 180,
больше суммы собственных ожиданий (60 + 60 + 45). Расписания ушли с круглых
минут (ночь 02:17, мутанты 00:43, метрики 05:23, полный перф 04:11), ночь
пишет в summary сдвиг старта и предупреждает, если он больше часа.

test/workflow-hygiene.test.mjs держит все три правила по тексту workflow
(разбор `parseJobSettings` в scripts/workflow-jobs.mjs) и исполняет шаг
сдвига старта настоящим bash; порядок осознанного подъёма образа —
docs/DEVELOPMENT.md.

Issue: #658
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-09-27 13:57:21 +03:00
parent 6fc789e4c8
commit bdac4b4fdc
22 changed files with 302 additions and 54 deletions
+9 -5
View File
@@ -52,7 +52,8 @@ jobs:
# commit/tree, а не самостоятельно читают dev в разное время.
material:
name: "Зафиксировать неизменяемый материал"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
sha: ${{ steps.identity.outputs.sha }}
tree: ${{ steps.identity.outputs.tree }}
@@ -112,7 +113,7 @@ jobs:
name: "Мутанты: каждый обязан красить тесты (шард ${{ matrix.shard }} из 6)"
needs: material
if: needs.material.outputs.reuse != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
@@ -208,7 +209,8 @@ jobs:
name: "Доказать единый material всех шардов"
needs: [material, mutants]
if: always() && needs.material.outputs.reuse != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
@@ -239,7 +241,8 @@ jobs:
name: "Записать маркер зелёного прогона"
needs: [material, mutants, evidence]
if: needs.material.outputs.reuse != 'true' && needs.mutants.result == 'success' && needs.evidence.result == 'success'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
@@ -275,7 +278,8 @@ jobs:
# является — её шарды пропущены намеренно. Любой другой пропуск шардов
# (упал 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
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
actions: read
+21 -1
View File
@@ -32,9 +32,29 @@ permissions:
jobs:
dispatch:
name: "Запустить Validate на dev с полным набором и дождаться результата"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 90
steps:
# #658: при плане 02:30 UTC ночь фактически стартовала в 07:42–08:05 и
# кончалась в рабочее время владельца. Сдвиг старта больше часа — видимое
# предупреждение, а не молча съеденная ночь. `github.event.schedule` —
# строка cron вызывающего `nightly.yml`; NOW_EPOCH подставляет только тест.
- name: "Сдвиг старта ночи против расписания"
if: github.event_name == 'schedule'
env:
SCHEDULE: ${{ github.event.schedule }}
run: |
set -euo pipefail
read -r minute hour _ <<< "$SCHEDULE"
now=${NOW_EPOCH:-$(date -u +%s)}
planned=$(date -u -d "@$now" +%F)
planned=$(date -u -d "$planned $hour:$minute" +%s)
[ "$planned" -le "$now" ] || planned=$((planned - 86400))
lag=$(( (now - planned) / 60 ))
echo "- Старт по расписанию \`$SCHEDULE\` (UTC): сдвиг $lag мин" >> "$GITHUB_STEP_SUMMARY"
if [ "$lag" -gt 60 ]; then
echo "::warning::ночной прогон стартовал на $lag мин позже расписания ($SCHEDULE UTC) — очередь расписаний GitHub (#658)"
fi
- env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
+1 -1
View File
@@ -25,7 +25,7 @@ permissions:
jobs:
metrics:
name: "Снимок недели: issue, раунды, прогоны"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
# Код — из dev, как у reconcile: расписание читается из main, а исполняется
+1 -1
View File
@@ -19,7 +19,7 @@ permissions:
jobs:
reconcile:
name: "Один снимок S4/S7 без polling модели"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
concurrency:
group: process-reconcile
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
resume:
name: "Разбудить раунд, ждавший этот Validate"
if: github.event.workflow_run.event == 'workflow_dispatch' && startsWith(github.event.workflow_run.head_branch, 'issue/')
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
concurrency:
group: process-resume-${{ github.event.workflow_run.head_branch }}
+6 -5
View File
@@ -48,7 +48,8 @@ jobs:
# Только статусные метки этапов ревью запускают конвейер (#499). Остальные
# события помечаются skipped и не занимают место в группе concurrency.
if: github.event.label.name == 'S4-spec-review' || github.event.label.name == 'S7-code-review'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
concurrency:
group: process-issue-${{ github.event.issue.number }}
cancel-in-progress: false
@@ -282,7 +283,7 @@ jobs:
issues: write
needs: guard
if: needs.guard.outputs.stage != ''
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
concurrency:
group: process-issue-${{ github.event.issue.number }}
cancel-in-progress: false
@@ -852,7 +853,7 @@ jobs:
issues: write
needs: [guard, prepare]
if: needs.prepare.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
concurrency:
group: process-issue-${{ github.event.issue.number }}
cancel-in-progress: false
@@ -934,7 +935,7 @@ jobs:
- name: Установить Chromium
if: steps.pw.outputs.cache-hit != 'true'
# Без --with-deps: системные библиотеки Chromium предустановлены в
# образе ubuntu-latest, а apt при промахе кэша съедал минуты из бюджета
# образе раннера, а apt при промахе кэша съедал минуты из бюджета
# ревью и подолгу перебирал недоступное azure-зеркало (#175). Если
# библиотека когда-нибудь пропадёт из образа, Chromium не запустится с
# внятной ошибкой — тогда флаг вернуть.
@@ -1196,7 +1197,7 @@ jobs:
issues: write
needs: [guard, prepare, model_review]
if: always() && needs.guard.outputs.stage != ''
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
concurrency:
group: process-issue-${{ github.event.issue.number }}
cancel-in-progress: false
+2 -1
View File
@@ -43,7 +43,8 @@ jobs:
telegram:
name: Оповещение в Telegram (только стабильные)
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_call' && inputs.prerelease == false) }}
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Check out release notes for a reusable call
if: ${{ inputs.reusable == true }}
+2 -1
View File
@@ -31,7 +31,8 @@ permissions:
jobs:
capture:
name: Съёмка и сверка скриншот-индекса
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
+4 -3
View File
@@ -16,9 +16,10 @@ on:
required: false
default: dev
schedule:
# Каждую ночь, 01:00 UTC (04:00 MSK) — после суток правок и до ночного
# полного Validate (nightly.yml, 02:30 UTC), чтобы не делить раннеры (#513).
- cron: '0 1 * * *'
# Каждую ночь, 00:43 UTC (03:43 MSK) — после суток правок и до ночного
# полного Validate (nightly.yml, 02:17 UTC), чтобы не делить раннеры (#513).
# Минута не круглая: такие старты GitHub сдвигает меньше (#658).
- cron: '43 0 * * *'
permissions:
contents: read
+3 -1
View File
@@ -9,8 +9,10 @@ name: Ночной полный прогон dev
# держит копии равными.
on:
# 02:17 UTC, не на круглой минуте: старты «ровно в час/полчаса» GitHub
# откладывает на часы (факт — 07:42–08:05 при плане 02:30, #658).
schedule:
- cron: '30 2 * * *'
- cron: '17 2 * * *'
workflow_dispatch: {}
permissions:
+3 -2
View File
@@ -10,7 +10,8 @@ on:
- ".github/workflows/**"
- "docs/**"
schedule:
- cron: "0 4 * * 1"
# Минута не круглая (#658): доходит до main вместе со стабильным релизом.
- cron: "11 4 * * 1"
workflow_dispatch:
inputs:
comparison_ref:
@@ -31,7 +32,7 @@ jobs:
# Every profile keeps base and candidate sequential on one hosted runner.
# Independent profile pairs run in parallel: cross-profile timing is never
# compared, while serialising all profile pairs cannot fit the job timeout.
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
+3 -2
View File
@@ -10,8 +10,9 @@ name: Метрики процесса
on:
schedule:
# Понедельник 05:00 UTC — после ночных прогонов, до рабочего дня.
- cron: '0 5 * * 1'
# Понедельник 05:23 UTC — после ночных прогонов, до рабочего дня; минута
# не круглая, чтобы не стоять в очереди «ровных» расписаний (#658).
- cron: '23 5 * * 1'
workflow_dispatch:
inputs:
days:
+7 -3
View File
@@ -21,7 +21,9 @@ concurrency:
jobs:
gate:
name: "Гейт: зелёная Проверка и релизный контракт"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
# Больше ожидания зелёного Validate в release-gate.mjs (до 60 мин) (#658).
timeout-minutes: 75
outputs:
sha: ${{ steps.candidate.outputs.sha }}
tag: ${{ steps.candidate.outputs.tag }}
@@ -122,7 +124,8 @@ jobs:
publish:
name: Публикация тега и релиза
needs: gate
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 20
outputs:
url: ${{ steps.verify.outputs.url }}
newly_published: ${{ steps.release.outputs.newly_published }}
@@ -289,7 +292,8 @@ jobs:
close-merged:
name: Закрытие вошедших issue
needs: [gate, publish]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
actions: read
+3 -3
View File
@@ -46,7 +46,7 @@ concurrency:
jobs:
prepare:
name: "Ревью релиза: вход линии"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
@@ -110,7 +110,7 @@ jobs:
name: "Ревью релиза: работа модели"
needs: prepare
if: needs.prepare.outputs.proceed == 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 60
# Недоверенная стадия. Прав на запись нет никаких: ни в репозиторий, ни в
# issue. Документ публикует `publish`; находки в issue превращает владелец.
@@ -258,7 +258,7 @@ jobs:
publish:
name: "Ревью релиза: документ в dev"
needs: [prepare, model_review]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
+13 -6
View File
@@ -48,7 +48,8 @@ jobs:
name: "Кандидат: точный SHA, режим и черновик"
# Публикация беты руками — не наш случай: у бет свой staged-путь.
if: ${{ github.event_name == 'workflow_dispatch' || !github.event.release.prerelease }}
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
sha: ${{ steps.resolve.outputs.sha }}
tag: ${{ steps.resolve.outputs.tag }}
@@ -142,7 +143,7 @@ jobs:
needs: candidate
if: ${{ needs.candidate.outputs.prerelease != 'true' }}
continue-on-error: true
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
actions: write
@@ -164,7 +165,10 @@ jobs:
gate:
name: "Гейт: контракт, Validate, Full Performance и E2E на точном SHA"
needs: candidate
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
# Больше суммы собственных ожиданий job: Validate и Full Performance ждутся
# до 60 мин каждый (release-gate.mjs), E2E — до 45 (e2e-gate.mjs): 165 < 180 (#658).
timeout-minutes: 180
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
@@ -234,7 +238,8 @@ jobs:
stage:
name: "Сборка: ассеты, SHA256SUMS и загрузка в черновик"
needs: [candidate, gate]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
@@ -374,7 +379,8 @@ jobs:
publish:
name: "Публикация и сверка публичных байтов"
needs: [candidate, gate, stage]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 15
outputs:
url: ${{ steps.verify.outputs.url }}
name: ${{ steps.verify.outputs.name }}
@@ -474,7 +480,8 @@ jobs:
# make that distribution failure impossible to miss in the release run.
if: ${{ needs.candidate.outputs.prerelease == 'true' }}
needs: [candidate, publish]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Verify the published tag is the prerelease HACS will discover
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
+26 -15
View File
@@ -50,7 +50,8 @@ jobs:
# действовать — на уровне шагов, с той же гранулярностью в логе.
preflight:
name: "Предполёт: документация, провенанс, процесс"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 15
# `actions: read` — чтение списка прогонов Validate (#388), `issues: read` —
# проверка 8 процессного гейта. Права перечислены явно, потому что job
# обращается к API сверх содержимого репозитория.
@@ -247,7 +248,8 @@ jobs:
# там делает код-ревью, которое гоняет гейты само (#127).
changes:
name: Классификация изменённых файлов
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
# Чтение списка прогонов Validate — единственное, что этой job нужно сверх
# содержимого репозитория (#387).
permissions:
@@ -406,7 +408,8 @@ jobs:
# performance_smoke включает набор, иначе glow-only результат засчитался бы
# прогону, которому нужен изометрический профиль.
needs: changes
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
smoke: ${{ steps.probe.outputs.smoke }}
golden: ${{ steps.probe.outputs.golden }}
@@ -555,7 +558,8 @@ jobs:
name: "HACS: валидация репозитория"
needs: changes
if: needs.changes.outputs.integration == 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: HACS validation
@@ -567,7 +571,8 @@ jobs:
name: "Hassfest: манифест интеграции"
needs: changes
if: needs.changes.outputs.integration == 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Hassfest validation
@@ -598,7 +603,7 @@ jobs:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
@@ -727,7 +732,8 @@ jobs:
name: "Фронтенд: типы, юниты, мутанты, синхрон бандла"
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 40
steps:
# Полная история без блобов (#342): гейт «новый код не добавляет any»
# diff-aware, ему нужен диапазон, а содержимое старых ревизий — нет.
@@ -837,7 +843,7 @@ jobs:
name: "Бандл головы dev для стенда"
needs: frontend
if: github.event_name == 'push' && github.ref == 'refs/heads/dev' && needs.frontend.result == 'success'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
continue-on-error: true
permissions:
@@ -867,7 +873,7 @@ jobs:
# Gated on `frontend` so a typecheck failure does not burn browser minutes.
needs: [changes, frontend, reuse]
if: needs.changes.outputs.heavy == 'true' && needs.reuse.outputs.smoke != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 20
# Смоки шардируются: последовательный прогон занимал ~7.5 минут и был
# критическим путём всего Validate. Три шарда режут его примерно вдвое;
@@ -974,7 +980,8 @@ jobs:
name: "Смоки: все шарды зелёные"
needs: [changes, smoke, reuse]
if: needs.changes.outputs.heavy == 'true' && needs.reuse.outputs.smoke != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Записать маркер успеха
run: |
@@ -998,7 +1005,8 @@ jobs:
# inexpensive and catches a different class of regressions than timings.
needs: [changes, frontend, reuse]
if: needs.changes.outputs.heavy == 'true' && needs.reuse.outputs.golden != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
@@ -1100,7 +1108,7 @@ jobs:
# prereleases. The expensive same-runner comparison lives in performance.yml.
needs: [changes, frontend, reuse]
if: needs.changes.outputs.heavy == 'true' && needs.reuse.outputs.performance_smoke != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
# 15 минут не хватало, когда установка браузера шла через apt: замер
# начинался на исходе окна (#206). Запас на холодный кэш — при попадании
# job укладывается в те же минуты, что и раньше. Диффозависимые профили
@@ -1196,7 +1204,8 @@ jobs:
name: "Геометрия: TS/Python parity исполнена"
needs: [changes, reuse]
if: needs.changes.outputs.geometry_parity == 'true' && needs.reuse.outputs.geometry_parity != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
@@ -1230,7 +1239,8 @@ jobs:
name: "Бэкенд: pytest в Home Assistant"
needs: [changes, reuse]
if: needs.changes.outputs.backend == 'true' && needs.reuse.outputs.backend != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
# Browser fixtures are generated by their real ESM factories and then
@@ -1316,7 +1326,8 @@ jobs:
name: "Доказательство выполненных проверок"
if: always()
needs: [preflight, changes, reuse, hacs, hassfest, changed_mutants, frontend, smoke, smoke_done, golden, performance_smoke, geometry_parity, backend]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with: