diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index c751dbca..3e8cd771 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -181,12 +181,12 @@ jobs: fi if [ -n "$BASE" ]; then - FILES=$(git diff --no-renames --name-only "$BASE" HEAD) + FILES=$(git -c core.quotePath=false diff --no-renames --name-only "$BASE" HEAD) N=$(printf '%s\n' "$FILES" | grep -c . || true) echo "База: $BASE → $(git rev-parse HEAD); изменённых файлов: $N" printf '%s\n' "$FILES" | sed 's/^/ /' else - FILES=$(git ls-files) + FILES=$(git -c core.quotePath=false ls-files) N=$(printf '%s\n' "$FILES" | grep -c . || true) echo "База не определена (event=$EVENT, before='${BEFORE:-}') — считаем изменённым весь репозиторий ($N файлов), деплой полный" fi @@ -1343,8 +1343,62 @@ jobs: fingerprint: ${{ secrets.DEPLOY_SSH_FINGERPRINT }} script: | set -euo pipefail + # #3448: ТОТ ЖЕ ЛОК, что берёт полный деплой (см. job `deploy` выше). + # Эта джоба делает `git reset --hard` в /opt/gendesign, то есть правит + # прод-дерево — ровно то, что полный деплой сериализует локом. Пока + # быстрый путь был мёртв, столкнуться было нечему; теперь есть. + exec 9>/var/lock/gendesign-docker-deploy.lock + if flock -n 9; then + echo "→ докер-лок свободен, взят сразу" + else + echo "→ докер-лок занят соседним деплоем, жду (до 900с)…" + lock_wait_started=$(date +%s) + if ! flock -w 900 9; then + echo "ERROR: не дождался лока докер-деплоя за 900с." + echo " Кто держит: ssh на хост, затем fuser -v /var/lock/gendesign-docker-deploy.lock" + exit 1 + fi + echo "→ докер-лок получен через $(( $(date +%s) - lock_wait_started ))с ожидания" + fi cd /opt/gendesign git fetch origin main + + # ── #3448: быстрый путь законен, только если прод отстаёт РОВНО на + # конфиг прокси ──────────────────────────────────────────────────── + # + # Джоба `changes` считает дифф between-push (before→HEAD) и не знает, + # что доехало до прода. Пока caddy_only был мёртв, любой push шёл + # полным деплоем и гард свежести :latest (#2950, job `deploy`) + # прикрывал прод по умолчанию. Оживший быстрый путь этот гард + # обходит: при caddy_only=true джоба `deploy` пропускается целиком. + # + # Сценарий отказа: push A правит бэкенд, билды ~6 мин, `deploy` в + # очереди; через 2 мин push B правит только caddy/. Forgejo на + # 10.0.3 отменяет ещё не стартовавший `deploy` предыдущего прогона + # ДАЖЕ при cancel-in-progress: false (наблюдение 21.08.2026 10:35:13, + # см. шапку scripts/check-latest-image-revision.sh). Дифф A..B — один + # caddy-файл, быстрый путь включается, `compose pull` + `up -d` не + # делает никто: прод крутит старый образ при зелёной голове main. + # + # Единственный источник правды о том, что реально на проде, — HEAD + # прод-дерева (у Trade-In для этого заведён отдельный маркер + # /opt/gendesign/.tradein-deployed-sha, см. deploy-tradein.yml:150; + # у ПТИЦЫ маркера нет, но git reset ниже делает HEAD эквивалентом). + # Проверка стоит ДО reset намеренно: при отказе прод-HEAD остаётся + # честным для следующего прогона. + # + # ЧЕГО ЭТА ПРОВЕРКА НЕ ЛОВИТ: `deploy` прогона A, упавшую ПОСЛЕ + # `git reset --hard` (например на миграции). Тогда прод-HEAD уже + # равен A, а контейнеры старые, и caddy-only push пройдёт быстрым + # путём. Это остаётся за настоящим маркером «что задеплоено». + PROD_HEAD=$(git rev-parse HEAD) + OUTSIDE=$(git -c core.quotePath=false diff --name-only "$PROD_HEAD" origin/main | grep -vE '^(Caddyfile$|caddy/)' || true) + if [ -n "$OUTSIDE" ]; then + echo "::error::прод отстаёт не только по конфигу прокси — быстрый путь запрещён:" + printf '%s\n' "$OUTSIDE" | sed 's/^/ /' + echo "Запусти полный деплой через workflow_dispatch." + exit 1 + fi git reset --hard origin/main # Конфиг примонтирован read-only с хоста, пересборка не нужна — # контейнер читает тот же файл, что только что обновил git.