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
Почему: деплой катит 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
70 lines
4.5 KiB
Bash
Executable file
70 lines
4.5 KiB
Bash
Executable file
#!/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
|