gendesign/scripts/check-latest-image-revision.sh
bot-backend 54889a956b
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m7s
CI / backend-tests (pull_request) Successful in 17m41s
fix(ci): деплой падает громко, если :latest отстаёт от головы по компоненту (#2950)
Почему: деплой катит IMAGE_TAG=latest. Forgejo отменяет ещё не стартовавший
deploy предыдущего run'а той же группы при новом push (cancel-in-progress:
false на pending-job не действует): 21.08 10:35:13 deploy run'а ccf84b4a
«Has been cancelled» через 4 с после старта run'а 95db3f44 (ops-only, билды
пропущены), и тот накатил :latest как есть. Прод получил новый код только
потому, что билды ccf84b4a легли в 10:34:08/14 — за 70 с до pull'а; иначе
голова была бы зелёной, а прод — на старом образе. Host-lock (#2955)
закрывает гонку прунов, но не эту.

Что: build-push-action ставит org.opencontainers.image.revision=github.sha
(12 шагов: попытка+ретрай × 6 образов). В deploy-job обоих workflow перед
SSH — scripts/check-latest-image-revision.sh: читает ревизию :latest из
registry (imagetools inspect), требует, чтобы она содержала последний
коммит по путям компонента (те же фильтры, что запускают сборку), ждёт билд
предшественника до 15 мин, иначе ::error и выход 1. Ревизия новее — ок
(workflow_dispatch собирает голову).

Тест test_2950_latest_image_revision_gate.py: подменный docker на PATH +
настоящий временный git; коды выхода по значению (свежий 0, новее 0, отстаёт
1 с ожиданием, чужой компонент 1, без метки 1, registry упал 1, usage 2);
мутация «всегда 0» → 5/7 красных.

Refs #2950
2026-08-21 16:50:34 +05:00

70 lines
4.5 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Гард свежести образа :latest перед деплоем (#2950).
#
# ЗАЧЕМ. Деплой катит IMAGE_TAG=latest. Forgejo отменяет ещё не стартовавший
# `deploy` предыдущего run'а той же concurrency-группы, когда приходит новый push
# (cancel-in-progress: false объявлен, но на pending-job не действует — 21.08.2026
# 10:35:13 «Has been cancelled» через 4 с после старта следующего run'а). Следующий
# run — например ops-only, с пропущенными билдами — катит :latest как есть. 21.08
# прод получил новый код только потому, что билды предшественника успели за 70 с
# до pull'а; иначе голова main была бы зелёной, а прод — на старом образе.
#
# ЧТО ПРОВЕРЯЕТ. Образ :latest в registry несёт метку org.opencontainers.image.revision
# (ставится build-push-action в deploy.yml / deploy-tradein.yml). Гард требует, чтобы
# эта ревизия СОДЕРЖАЛА последний коммит, трогавший пути компонента (git log по
# checkout'у run'а): то есть :latest не старше того, что обязано было собраться.
# Ревизия новее — нормально (workflow_dispatch собирает всё на голове).
# Пока условие не выполнено — ждёт (билд предшественника ещё пушит) до таймаута,
# затем падает громко: лучше красный деплой, чем тихий откат.
#
# Использование: check-latest-image-revision.sh <image-repo> <timeout_s> -- <path>...
# CHECK_LATEST_SLEEP_S — пауза между опросами (по умолчанию 20; тесты ставят меньше).
# Коды: 0 — свежий; 1 — отстаёт/метки нет/registry не ответил до таймаута; 2 — usage.
set -euo pipefail
IMAGE="${1:-}"; TIMEOUT="${2:-}"
if [ -z "$IMAGE" ] || [ -z "$TIMEOUT" ] || [ "${3:-}" != "--" ] || [ $# -lt 4 ]; then
echo "usage: $0 <image-repo> <timeout_s> -- <path>..." >&2; exit 2
fi
shift 3
SLEEP="${CHECK_LATEST_SLEEP_S:-20}"
LAST="$(git log -1 --format=%H -- "$@" || true)"
if [ -z "$LAST" ]; then
echo "::error::$IMAGE — нет ни одного коммита по путям: $*" >&2; exit 2
fi
read_revision() {
# Печатает ревизию из метки образа или пустую строку. .Image для multi-arch —
# карта по платформам, для single-arch — сам конфиг; разбираем оба вида.
docker buildx imagetools inspect "$IMAGE:latest" --format '{{json .Image}}' 2>/dev/null \
| python3 -c '
import json, sys
try:
d = json.load(sys.stdin)
except Exception:
sys.exit(0)
cands = [d] if isinstance(d, dict) and "config" in d else [v for v in d.values() if isinstance(v, dict)]
for c in cands:
rev = ((c.get("config") or {}).get("Labels") or {}).get("org.opencontainers.image.revision")
if rev:
print(rev); break
' || true
}
deadline=$(( $(date +%s) + TIMEOUT ))
while :; do
REV="$(read_revision)"
if [ -n "$REV" ] && git cat-file -e "${REV}^{commit}" 2>/dev/null \
&& git merge-base --is-ancestor "$LAST" "$REV"; then
echo "$IMAGE:latest — ревизия ${REV:0:10} содержит последний коммит по компоненту ${LAST:0:10}"
exit 0
fi
now=$(date +%s)
if [ "$now" -ge "$deadline" ]; then
echo "::error::$IMAGE:latest несёт ревизию '${REV:-<метки нет / registry не ответил>}', а последний коммит, трогавший [$*] — ${LAST:0:10}. :latest отстаёт (билд предшественника не доехал или отменён) — деплой остановлен, иначе прод получил бы старый образ при зелёной голове (#2950). Если билд того коммита упал — чини его или запусти workflow_dispatch полного деплоя." >&2
exit 1
fi
echo "$IMAGE:latest — ревизия '${REV:-<метки нет>}' ещё не содержит ${LAST:0:10}; жду билд предшественника ($(( deadline - now )) с)"
sleep "$SLEEP"
done