From 9a8aaa2d2d880720bda4728664101fdd2f64dcfe Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 12 Sep 2026 15:56:24 +0500 Subject: [PATCH] =?UTF-8?q?deploy-caddy:=20=D1=81=D1=82=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B6=20=D0=BE=D1=82=D1=81=D1=82=D0=B0=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B4=D0=B0,=20=D0=BE=D0=B1=D1=89?= =?UTF-8?q?=D0=B8=D0=B9=20=D0=BB=D0=BE=D0=BA=20=D0=B8=20=D0=BA=D0=B2=D0=BE?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF?= =?UTF-8?q?=D1=83=D1=82=D0=B5=D0=B9=20(#3448)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Оживший быстрый путь снимает гард свежести :latest. Пока caddy_only был мёртв, любой push шёл полным деплоем, и гард #2950 прикрывал прод по умолчанию. Теперь при 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; workflow-level concurrency там не исполняется, см. deploy.yml). Дифф A..B — один caddy-файл, быстрый путь включается, `compose pull` + `up -d` не делает никто: прод крутит старый образ, голова main зелёная, сигнала нет. Сторож в ssh-скрипте deploy-caddy: если прод отстаёт от origin/main не только по Caddyfile/caddy/**, быстрый путь запрещён, шаг падает и называет файлы. Стоит ДО `git reset --hard` намеренно — при отказе прод-HEAD остаётся честным для следующего прогона. У Trade-In для того же заведён отдельный маркер (/opt/gendesign/.tradein-deployed-sha, deploy-tradein.yml), у ПТИЦЫ маркера нет, и `git reset` делает прод-HEAD его эквивалентом. ЧЕГО СТОРОЖ НЕ ЛОВИТ: `deploy`, упавшую ПОСЛЕ `git reset --hard` (например на миграции). Тогда прод-HEAD уже равен новому коммиту, а контейнеры старые — это остаётся за настоящим маркером «что задеплоено». Тот же лок, что и у полного деплоя. deploy-caddy делает `git reset --hard` в /opt/gendesign, то есть правит прод-дерево — ровно то, что job `deploy` сериализует через flock /var/lock/gendesign-docker-deploy.lock. Пока путь был мёртв, сталкиваться было нечему; теперь это первая джоба, трогающая прод-дерево в обход сериализации. Квотирование путей. `git diff --name-only` и `git ls-files` при core.quotePath (умолчание true) отдают не-ASCII пути закавыченными с \NNN-экранированием — `^backend/` такую строку не матчит, и файл backend/<кириллица>.py дал бы backend=false. Старый paths-filter брал `--name-status -z`, где квотирования нет: это единственное место, где переход на свой diff менял поведение. В дереве такие пути уже живут (docs/Бизнес-план…). Добавлен `-c core.quotePath=false` в обе команды и в сторож выше. Co-Authored-By: Claude Opus 5 --- .forgejo/workflows/deploy.yml | 58 +++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) 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.