fix(ci): прод-деплои в одну группу concurrency — прун одного убивал pull другого (#2950) (#2952)
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy Trade-In / changes (push) Successful in 10s
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-worker (push) Successful in 51s
Deploy / build-backend (push) Successful in 52s
Deploy / build-frontend (push) Successful in 51s
Deploy Trade-In / build-browser (push) Successful in 32s
Deploy / deploy (push) Successful in 1m21s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Successful in 3m30s
Deploy Trade-In / test (push) Successful in 4m15s
Deploy Trade-In / build-backend (push) Successful in 31s
Deploy Trade-In / deploy (push) Successful in 2m6s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy Trade-In / changes (push) Successful in 10s
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-worker (push) Successful in 51s
Deploy / build-backend (push) Successful in 52s
Deploy / build-frontend (push) Successful in 51s
Deploy Trade-In / build-browser (push) Successful in 32s
Deploy / deploy (push) Successful in 1m21s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Successful in 3m30s
Deploy Trade-In / test (push) Successful in 4m15s
Deploy Trade-In / build-backend (push) Successful in 31s
Deploy Trade-In / deploy (push) Successful in 2m6s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
This commit is contained in:
parent
c93d6cbde1
commit
2a01dea103
5 changed files with 102 additions and 1 deletions
|
|
@ -158,6 +158,15 @@ jobs:
|
||||||
# переведён в expired, test_get_role_known_users стал красным и
|
# переведён в expired, test_get_role_known_users стал красным и
|
||||||
# доехал до main незамеченным (починен в PR #2587).
|
# доехал до main незамеченным (починен в PR #2587).
|
||||||
- 'auth/**'
|
- 'auth/**'
|
||||||
|
# Тот же класс, что и с auth/** выше (#2950). В backend/tests/ops/
|
||||||
|
# лежат гейты на сами workflow-файлы — например «оба прод-деплоя
|
||||||
|
# обязаны быть в одной группе concurrency». Правка, разводящая
|
||||||
|
# группы обратно, не трогает 'backend/**' → без этих строк
|
||||||
|
# backend-tests пропускался бы, гейт не исполнялся, и регрессия
|
||||||
|
# уезжала в main зелёной. Гейт, который не запускается на той самой
|
||||||
|
# правке, от которой стережёт, — украшение.
|
||||||
|
- '.forgejo/workflows/deploy.yml'
|
||||||
|
- '.forgejo/workflows/deploy-tradein.yml'
|
||||||
- '.forgejo/workflows/ci.yml'
|
- '.forgejo/workflows/ci.yml'
|
||||||
frontend:
|
frontend:
|
||||||
- 'frontend/**'
|
- 'frontend/**'
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,23 @@ on:
|
||||||
- ".forgejo/workflows/deploy-tradein.yml"
|
- ".forgejo/workflows/deploy-tradein.yml"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# #2950: ОБЩАЯ группа с deploy-tradein.yml — не опечатка и не копипаста.
|
||||||
|
# Оба деплоя ходят по SSH в ОДИН докер-демон (стеки gendesign-* и tradein-*
|
||||||
|
# плюс сам forgejo-runner живут на одной VM), и `docker image prune -af` одного
|
||||||
|
# сносит leases ещё не доехавшего `compose pull` другого:
|
||||||
|
# unable to lease content: lease does not exist: not found
|
||||||
|
# 20.08 так и вышло: run 8083 упал за 5с — прун соседнего деплоя отработал через
|
||||||
|
# 0.4с после обрыва пула. Прод остался на старом коде, при том что голова main
|
||||||
|
# показывала success (зелёным был чужой, Trade-In'овый деплой той же головы).
|
||||||
|
# Разные группы + cancel-in-progress: false не спасают: false сериализует раны
|
||||||
|
# ВНУТРИ группы, а гонка была МЕЖДУ группами.
|
||||||
|
# Цена: деплои ждут друг друга целиком, вместе с билдами (~6 мин). Осознанно:
|
||||||
|
# host-lock (flock) сериализовал бы только докер-секцию, но у него своя отказная
|
||||||
|
# мода — дочерний процесс наследует fd лока и при аварийной смерти job'а лок
|
||||||
|
# залипает (проверено на хосте: после kill -9 лок остался занят). Сериализацию
|
||||||
|
# гарантирует планировщик Forgejo, залипать там нечему.
|
||||||
concurrency:
|
concurrency:
|
||||||
group: deploy-tradein-prod
|
group: deploy-prod
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,21 @@ on:
|
||||||
- "ops/docker-prune.sh"
|
- "ops/docker-prune.sh"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# #2950: ОБЩАЯ группа с deploy-tradein.yml — не опечатка и не копипаста.
|
||||||
|
# Оба деплоя ходят по SSH в ОДИН докер-демон (стеки gendesign-* и tradein-*
|
||||||
|
# плюс сам forgejo-runner живут на одной VM), и `docker image prune -af` одного
|
||||||
|
# сносит leases ещё не доехавшего `compose pull` другого:
|
||||||
|
# unable to lease content: lease does not exist: not found
|
||||||
|
# 20.08 так и вышло: run 8083 упал за 5с — прун соседнего деплоя отработал через
|
||||||
|
# 0.4с после обрыва пула. Прод остался на старом коде, при том что голова main
|
||||||
|
# показывала success (зелёным был чужой, Trade-In'овый деплой той же головы).
|
||||||
|
# Разные группы + cancel-in-progress: false не спасают: false сериализует раны
|
||||||
|
# ВНУТРИ группы, а гонка была МЕЖДУ группами.
|
||||||
|
# Цена: деплои ждут друг друга целиком, вместе с билдами (~6 мин). Осознанно:
|
||||||
|
# host-lock (flock) сериализовал бы только докер-секцию, но у него своя отказная
|
||||||
|
# мода — дочерний процесс наследует fd лока и при аварийной смерти job'а лок
|
||||||
|
# залипает (проверено на хосте: после kill -9 лок остался занят). Сериализацию
|
||||||
|
# гарантирует планировщик Forgejo, залипать там нечему.
|
||||||
concurrency:
|
concurrency:
|
||||||
group: deploy-prod
|
group: deploy-prod
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
|
||||||
0
backend/tests/ops/__init__.py
Normal file
0
backend/tests/ops/__init__.py
Normal file
62
backend/tests/ops/test_2950_deploy_concurrency_group.py
Normal file
62
backend/tests/ops/test_2950_deploy_concurrency_group.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
"""Гейт: оба прод-деплоя обязаны лежать в ОДНОЙ группе concurrency.
|
||||||
|
|
||||||
|
Почему это тест, а не комментарий в yml (#2950). Деплой ПТИЦЫ и деплой Trade-In
|
||||||
|
ходят по SSH в один и тот же докер-демон — стеки gendesign-* и tradein-* плюс сам
|
||||||
|
forgejo-runner живут на одной VM. Каждый в конце делает `docker image prune -af`.
|
||||||
|
Пока группы разные, Forgejo запускает их параллельно, и прун одного сносит leases
|
||||||
|
ещё не доехавшего `compose pull` другого:
|
||||||
|
|
||||||
|
unable to lease content: lease does not exist: not found
|
||||||
|
|
||||||
|
20.08 так и вышло: run 8083 упал за 5 секунд, прод остался на старом коде. Разводка
|
||||||
|
групп обратно ничего не сломает в CI и не даст ни одного сигнала — отказ проявится
|
||||||
|
только в следующем совпадении окон двух мержей. Поэтому инвариант зафиксирован здесь.
|
||||||
|
|
||||||
|
`cancel-in-progress: false` частью инварианта тоже является: прод-деплои обязаны
|
||||||
|
достаиваться в очереди, а не отменять друг друга — отменённый деплой оставляет прод
|
||||||
|
на старом коде ровно так же, как упавший.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
# backend/tests/ops/<этот файл> → корень репозитория
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
WORKFLOWS = REPO_ROOT / ".forgejo" / "workflows"
|
||||||
|
|
||||||
|
PROD_DEPLOYS = ("deploy.yml", "deploy-tradein.yml")
|
||||||
|
|
||||||
|
|
||||||
|
def _concurrency(name: str) -> dict:
|
||||||
|
path = WORKFLOWS / name
|
||||||
|
assert path.is_file(), f"нет {path} — переехал workflow, гейт ослеп"
|
||||||
|
return yaml.safe_load(path.read_text()).get("concurrency") or {}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name", PROD_DEPLOYS)
|
||||||
|
def test_prod_deploy_declares_concurrency(name: str) -> None:
|
||||||
|
"""У каждого прод-деплоя concurrency вообще объявлена."""
|
||||||
|
assert _concurrency(name).get(
|
||||||
|
"group"
|
||||||
|
), f"{name}: пропала секция concurrency — деплои снова могут пойти параллельно"
|
||||||
|
|
||||||
|
|
||||||
|
def test_both_prod_deploys_share_one_group() -> None:
|
||||||
|
"""Группа у обоих одна и та же — иначе прун одного убьёт pull другого."""
|
||||||
|
groups = {name: _concurrency(name).get("group") for name in PROD_DEPLOYS}
|
||||||
|
assert len(set(groups.values())) == 1, (
|
||||||
|
f"прод-деплои разведены по разным группам concurrency: {groups}. "
|
||||||
|
"Они ходят в ОДИН докер-демон; `docker image prune -af` одного обрывает "
|
||||||
|
"`compose pull` другого (#2950)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name", PROD_DEPLOYS)
|
||||||
|
def test_prod_deploy_queues_instead_of_cancelling(name: str) -> None:
|
||||||
|
"""Раны достаиваются в очереди: отменённый деплой = прод на старом коде."""
|
||||||
|
assert _concurrency(name).get("cancel-in-progress") is False, (
|
||||||
|
f"{name}: cancel-in-progress должен быть false — иначе более новый мерж "
|
||||||
|
"отменит выкатку предыдущего и та молча не доедет"
|
||||||
|
)
|
||||||
Loading…
Add table
Reference in a new issue