diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 02a103e..b434703 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,151 +1,13 @@ name: deploy-preland -# После коммита в main сохраняется legacy-выпуск на Beget. Новые домены ZoomPixel -# доставляются отдельными SFTP job: staging — только на stage, main — только на production. -# Цель — домен со своим хостингом (список — matrix ниже): реквизиты — секреты репозитория _FTP_HOST, -# _FTP_USER, _FTP_PASS (порт 21). Цели независимы: сбой одного хостинга остальные не останавливает. -# Заливаются файлы репозитория (git ls-files), кроме служебных (.gitea/, README.md, AGENTS.md, CLAUDE.md, .gitignore) и тех, что живут -# только на хостинге: requisites.html (реквизиты заполняют на месте — пустой файл из git их бы затёр). -# Рабочий config.json настраивается владельцем на каждом хостинге и является -# источником истины. Обычный deploy его не перезаписывает; задача может менять -# только прямо согласованные ключи после чтения текущего host-файла. -# На хостинге ничего не удаляется: удалённое из репозитория убирать вручную. - +# Решение владельца 19.10.2026, preland#4: только stage. +# Production выкаты временно отключены; Барбариска больше не используется. on: push: branches: - - main - staging jobs: - deploy: - name: deploy ${{ matrix.name }} - if: github.ref == 'refs/heads/main' - # Legacy — тоже production-выпуск: выполняем на выделенном runner, а не - # на общем Alpine-пуле, чтобы не зависеть от внешних apk mirrors. - runs-on: zoomzilla-production - - strategy: - fail-fast: false - matrix: - include: - # name — имя цели, url — адрес преленда (для проверки), prefix — префикс секретов _FTP_*, - # dir — каталог сайта на FTP ("" — корень FTP, как на beget). Новый домен — новая запись и три секрета; - # requisites.html создают на хостинге цели. Рабочий config.json остаётся - # на хостинге и обычным deploy не заменяется. - - name: barbariskatest - url: https://preland.anton.barbariskatest.ru - prefix: PRELAND - dir: "" - - defaults: - run: - shell: sh - - env: - PRELAND_URL: ${{ matrix.url }} - REMOTE_DIR: ${{ matrix.dir }} - # Последний успешно опубликованный legacy-снимок. На нестабильный FTP - # отправляем только изменения приложения после него, а не весь набор - # уже присутствующих медиафайлов. - LEGACY_DEPLOY_BASE: dfbb086e2b131a084f3de5b0f92c9cd69616c01a - - steps: - - name: Install tools - run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates git curl lftp php-cli - - # На production runner образ задания не содержит Node.js, поэтому - # actions/checkout не запускается. Клонируем тот же main напрямую: - # это не меняет содержимое host config.json. - - name: Checkout production branch - run: git clone --branch main http://git.dira89kv.beget.tech:3000/anton/preland.git . - - - name: Preserve host config and add focus if absent - env: - FTP_HOST: ${{ secrets[format('{0}_FTP_HOST', matrix.prefix)] }} - FTP_USER: ${{ secrets[format('{0}_FTP_USER', matrix.prefix)] }} - FTP_PASS: ${{ secrets[format('{0}_FTP_PASS', matrix.prefix)] }} - run: | - set -eu - [ -n "$FTP_HOST" ] && [ -n "$FTP_USER" ] && [ -n "$FTP_PASS" ] || { echo "нет секретов ${{ matrix.prefix }}_FTP_*"; exit 1; } - dir=$(printf '%s' "$REMOTE_DIR" | sed 's#^/*##; s#/*$##') - umask 077 - netrc=$(mktemp); config=$(mktemp) - trap 'rm -f "$netrc" "$config"' EXIT - printf 'machine %s login %s password %s\n' "$FTP_HOST" "$FTP_USER" "$FTP_PASS" > "$netrc" - curl -sS --ssl-reqd --netrc-file "$netrc" --fail --retry 3 \ - "ftp://$FTP_HOST/${dir:+$dir/}config.json" -o "$config" - changed=0 - php bin/ensure-focus-config.php "$config" || changed=$? - case "$changed" in - 0) ;; - 10) curl -sS --ssl-reqd --netrc-file "$netrc" --ftp-create-dirs --retry 3 --fail \ - -T "$config" "ftp://$FTP_HOST/${dir:+$dir/}config.json" ;; - *) exit "$changed" ;; - esac - - - name: Upload over FTP (TLS) - env: - FTP_HOST: ${{ secrets[format('{0}_FTP_HOST', matrix.prefix)] }} - FTP_USER: ${{ secrets[format('{0}_FTP_USER', matrix.prefix)] }} - FTP_PASS: ${{ secrets[format('{0}_FTP_PASS', matrix.prefix)] }} - run: | - set -eu - [ -n "$FTP_HOST" ] && [ -n "$FTP_USER" ] && [ -n "$FTP_PASS" ] || { echo "нет секретов ${{ matrix.prefix }}_FTP_*"; exit 1; } - dir=$(printf '%s' "$REMOTE_DIR" | sed 's#^/*##; s#/*$##') - case "$dir" in *[!A-Za-z0-9._/-]*) echo "недопустимый dir: $dir"; exit 1 ;; esac - umask 077 - script=$(mktemp) - trap 'rm -f "$script"' EXIT - git diff --name-only "$LEGACY_DEPLOY_BASE"..HEAD > changed-files.txt - grep -vE '^(\.gitea/|README\.md$|AGENTS\.md$|CLAUDE\.md$|\.gitignore$|requisites\.html$|config\.json$|bin/|tests/)' \ - changed-files.txt > files.txt || true - # имя уходит в адрес ftp:// как есть — пробелы и прочее экранирование не поддерживаем - if grep -q '[^A-Za-z0-9._/-]' files.txt; then echo "имена с недопустимыми символами:"; grep '[^A-Za-z0-9._/-]' files.txt; exit 1; fi - { - echo 'set cmd:fail-exit yes' - echo 'set ftp:ssl-force true' - echo 'set ftp:ssl-protect-data true' - echo 'set ssl:verify-certificate yes' - echo 'set net:max-retries 3' - echo 'set net:timeout 30' - } > "$script" - # Сервер legacy обрывает бинарные загрузки curl с 426. lftp ведёт - # каждую передачу отдельной FTPS-командой, не ослабляя TLS. - while read -r f; do - echo "загрузка: $f" - remote_dir=${f%/*} - [ "$remote_dir" = "$f" ] && remote_dir='.' - if [ "$remote_dir" != '.' ]; then - # Legacy-сервер возвращает 550 и для уже существующего каталога. - # Ошибку mkdir гасим, но put по-прежнему завершит выпуск ошибкой, - # если каталог действительно нельзя использовать. - printf 'mkdir -p %s%s || echo\n' "${dir:+$dir/}" "$remote_dir" >> "$script" - fi - printf 'put -O %s%s %s\n' \ - "${dir:+$dir/}" "$remote_dir" "$f" >> "$script" - done < files.txt - printf 'bye\n' >> "$script" - lftp -u "$FTP_USER","$FTP_PASS" -e "source $script" "ftp://$FTP_HOST" - echo "залито файлов: $(wc -l < files.txt)" - - - name: Check the page - run: | - set -eu - page=$(curl -fsS --max-time 30 "$PRELAND_URL/?nc=$(date +%s)") - api=$(printf '%s' "$page" | grep -o 'const createApiUrl = "[^"]*"' || true) - echo "$api" - case "$api" in - *'\/api\/images\/public\/create'*) ;; - *) echo "на странице нет адреса загрузки"; exit 1 ;; - esac - own=$(printf '%s' "$PRELAND_URL" | sed -E 's#^https?://([^/]+).*#\1#') - case "$api" in - *"$own"*) echo "загрузка уходит на сам преленд: на хостинге нет config.json с host"; exit 1 ;; - esac - printf '%s' "$page" | grep -q 'claim?key=' || { echo "переход не на claim — залит старый код?"; exit 1; } - echo "страница в порядке" - deploy-zoomstage: name: deploy zoomstage.online if: github.ref == 'refs/heads/staging' @@ -200,12 +62,26 @@ jobs: known_hosts=$(mktemp) batch=$(mktemp) files=$(mktemp) - trap 'rm -f "$known_hosts" "$batch" "$files"' EXIT + dirs=$(mktemp) + trap 'rm -f "$known_hosts" "$batch" "$files" "$dirs"' EXIT printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts" - git ls-files | grep -vE '^(\.gitea/|README\.md$|AGENTS\.md$|CLAUDE\.md$|\.gitignore$|requisites\.html$|config\.json$)' > "$files" + # Наборы `/N/` на stage — редактируемый контент. Кодовая доставка их + # намеренно не перезаписывает: актуальное содержимое stage фиксируют в Git + # отдельной синхронизацией по задаче. + git ls-files | grep -vE '^(\.gitea/|README\.md$|AGENTS\.md$|CLAUDE\.md$|\.gitignore$|requisites\.html$|config\.json$|[0-9]+/)' > "$files" if grep -q '[^A-Za-z0-9._/-]' "$files"; then echo "имена с недопустимыми символами:"; grep '[^A-Za-z0-9._/-]' "$files"; exit 1; fi printf '%s\n' "-mkdir $REMOTE_DIR" > "$batch" - sed 's#/[^/]*$##' "$files" | grep -v '^$' | sort -u | while read -r dir; do printf '%s\n' "-mkdir $REMOTE_DIR/$dir" >> "$batch"; done + while read -r file; do + case "$file" in + */*) dir=${file%/*} ;; + *) continue ;; + esac + while [ -n "$dir" ]; do + printf '%s\n' "$dir" >> "$dirs" + case "$dir" in */*) dir=${dir%/*} ;; *) break ;; esac + done + done < "$files" + sort -u "$dirs" | while read -r dir; do printf '%s\n' "-mkdir $REMOTE_DIR/$dir" >> "$batch"; done while read -r file; do printf '%s\n' "put -p $file $REMOTE_DIR/$file" >> "$batch"; done < "$files" SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch" echo "залито файлов: $(wc -l < "$files")" @@ -218,35 +94,12 @@ jobs: echo "$api" case "$api" in *'zoomzilla.ru\/api\/images\/public\/create'*) ;; *) echo "на странице нет адреса загрузки Zoomzilla"; exit 1 ;; esac printf '%s' "$page" | grep -q 'claim?key=' || { echo "переход не на claim"; exit 1; } + curl -fsS --max-time 30 "$PRELAND_URL/1/01_desk_1-source.avif" -o /dev/null || { echo "не опубликован первый набор примеров"; exit 1; } + curl -fsS --max-time 30 "$PRELAND_URL/2/01_desk_1-source.avif" -o /dev/null || { echo "не опубликован второй набор примеров"; exit 1; } + page2=$(curl -fsS --max-time 30 "$PRELAND_URL/?indpic=2&nc=$(date +%s)") + printf '%s' "$page2" | grep -q '2/01_mob_1-source.avif' || { echo "indpic=2 не использует второй набор примеров"; exit 1; } - deploy-zoompixel-production: - name: deploy ${{ matrix.name }} - if: github.ref == 'refs/heads/main' - runs-on: zoomzilla-production - strategy: - fail-fast: false - matrix: - include: - - name: zoompixel.ru - url: https://zoompixel.ru - dir: domains/zoompixel.ru - - name: zoompixel.online - url: https://zoompixel.online - dir: domains/zoompixel.online - defaults: - run: - shell: sh - env: - PRELAND_URL: ${{ matrix.url }} - REMOTE_DIR: ${{ matrix.dir }} - steps: - - name: Install tools - run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates git curl openssh-client sshpass php-cli - - - name: Checkout production branch - run: git clone --depth 1 --branch main http://git.dira89kv.beget.tech:3000/anton/preland.git . - - - name: Preserve host config and add focus if absent + - name: Remove obsolete demo directories env: SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }} SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }} @@ -254,50 +107,18 @@ jobs: SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }} run: | set -eu - [ -n "$SFTP_HOST" ] && [ -n "$SFTP_USER" ] && [ -n "$SFTP_PASS" ] && [ -n "$SFTP_KNOWN_HOSTS" ] || { echo "не заданы секреты ZOOMPIXEL_SFTP_*"; exit 1; } - umask 077 - known_hosts=$(mktemp); batch=$(mktemp); config=$(mktemp) - trap 'rm -f "$known_hosts" "$batch" "$config"' EXIT - printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts" - printf '%s\n' "get -p $REMOTE_DIR/config.json $config" > "$batch" - SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch" - changed=0 - php bin/ensure-focus-config.php "$config" || changed=$? - case "$changed" in - 0) ;; - 10) printf '%s\n' "put -p $config $REMOTE_DIR/config.json" > "$batch" - SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch" ;; - *) exit "$changed" ;; - esac - - - name: Upload over SFTP - env: - SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }} - SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }} - SFTP_PASS: ${{ secrets.ZOOMPIXEL_SFTP_PASS }} - SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }} - run: | - set -eu - [ -n "$SFTP_HOST" ] && [ -n "$SFTP_USER" ] && [ -n "$SFTP_PASS" ] && [ -n "$SFTP_KNOWN_HOSTS" ] || { echo "не заданы секреты ZOOMPIXEL_SFTP_*"; exit 1; } umask 077 known_hosts=$(mktemp) batch=$(mktemp) - files=$(mktemp) - trap 'rm -f "$known_hosts" "$batch" "$files"' EXIT + trap 'rm -f "$known_hosts" "$batch"' EXIT printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts" - git ls-files | grep -vE '^(\.gitea/|README\.md$|AGENTS\.md$|CLAUDE\.md$|\.gitignore$|requisites\.html$|config\.json$)' > "$files" - if grep -q '[^A-Za-z0-9._/-]' "$files"; then echo "имена с недопустимыми символами:"; grep '[^A-Za-z0-9._/-]' "$files"; exit 1; fi - printf '%s\n' "-mkdir $REMOTE_DIR" > "$batch" - sed 's#/[^/]*$##' "$files" | grep -v '^$' | sort -u | while read -r dir; do printf '%s\n' "-mkdir $REMOTE_DIR/$dir" >> "$batch"; done - while read -r file; do printf '%s\n' "put -p $file $REMOTE_DIR/$file" >> "$batch"; done < "$files" + for set in demo demo-2; do + for example in 1 2 3 4 5; do + for file in source.avif source.jpg source.webp result.avif result.jpg result.webp thumb.avif thumb.jpg thumb.webp source_mob.avif result_mob.avif thumb_mob.avif; do + printf '%s\n' "-rm $REMOTE_DIR/images/$set/$example/$file" >> "$batch" + done + printf '%s\n' "-rmdir $REMOTE_DIR/images/$set/$example" >> "$batch" + done + printf '%s\n' "-rmdir $REMOTE_DIR/images/$set" >> "$batch" + done SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch" - echo "залито файлов: $(wc -l < "$files")" - - - name: Check the page - run: | - set -eu - page=$(curl -fsS --max-time 30 "$PRELAND_URL/?nc=$(date +%s)") - api=$(printf '%s' "$page" | grep -o 'const createApiUrl = "[^"]*"' || true) - echo "$api" - case "$api" in *'zoomzilla.ru\/api\/images\/public\/create'*) ;; *) echo "на странице нет адреса загрузки Zoomzilla"; exit 1 ;; esac - printf '%s' "$page" | grep -q 'claim?key=' || { echo "переход не на claim"; exit 1; } diff --git a/AGENTS.md b/AGENTS.md index f7b25f9..e8373f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ такого выпуска фактический stage-config следует забрать и зафиксировать в Git. `requisites.html` также живёт только на хостинге и не перезаписывается. Не создавать target-specific конфиги или `config.json.example`. -- Коммит в `main` запускает FTP-автодеплой на все цели workflow; откат через `git revert` +- Коммит в `staging` запускает SFTP-автодеплой только на zoomstage.online; откат через `git revert` также запускает выкат. Коммит, push и изменения хостинга — только по прямому поручению. - Проверки кода и браузера проводить в Docker без установки ПО на хост. Проверка загрузки по боевому адресу вызывает реальную обработку; выполнять её только @@ -38,3 +38,6 @@ План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на политику и документацию не подключают MCP и не дают разрешения на публикацию или выкат. + +## Актуальная маршрутизация (19.10.2026, preland#4) +До отдельного поручения владельца выкатывать только staging → zoomstage.online. Production zoompixel.ru/.online не обновлять. preland.anton.barbariskatest.ru больше не используется, не является целью и не должен упоминаться как требующий выпуска. CI запускает только stage. Сохранение runtime config и stage-наборов /N/ обязательно. Работа и решения — через Gitea. diff --git a/README.md b/README.md index 7063eef..0fc47df 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ Варианты страницы и сайта (A/B) задаются метками в адресе. Код — обычный PHP без фреймворка и сборки; переводы — маленькая заглушка `Yii::t` в `i18n.php`. -Боевой экземпляр: `https://preland.anton.barbariskatest.ru` (хостинг beget). Пример ссылки входа: -`https://preland.anton.barbariskatest.ru/?indpic=1&theme=dark°r=2®istration=after&bchoice=a&tp=1&utm_source=test&subid=CLICK1`. +Активные production: `https://zoompixel.ru`, `https://zoompixel.online`; текущая цель выката — только `https://zoomstage.online`. Пример ссылки входа: +`https://zoomstage.online/?indpic=1&theme=dark°r=2®istration=after&bchoice=a&tp=1&utm_source=test&subid=CLICK1`. ## Как работает @@ -91,10 +91,7 @@ Preland всегда формирует полный resolved-набор сво git clone http://git.dira89kv.beget.tech:3000/anton/preland.git && cd preland # дальше — загрузка на хостинг: rsync/scp по SSH или FTP-клиент ``` -3. **Конфиг и реквизиты.** Runtime `config.json` создаётся при первичной настройке хоста и не заменяется - обычной синхронизацией. Менять в нём можно только ключи, явно согласованные в задаче; после такого выпуска - актуальный stage-снимок фиксируется в Git. Реквизиты задаются ключом `requisites` либо runtime-файлом - `requisites.html`. +3. **Конфиг и реквизиты.** Рабочий `config.json` на хостинге — источник истины. При первичном создании домена его настраивают отдельно; обычный deploy не заменяет его репозиторным снимком и не перезаписывает `requisites.html`. Менять можно только явно согласованные ключи после чтения текущего файла. 4. **nginx без Apache** (если хостинг не обрабатывает `.htaccess`): ```nginx server { @@ -125,42 +122,25 @@ Preland всегда формирует полный resolved-набор сво ## Автодеплой (Gitea Actions) -`.gitea/workflows/deploy.yml`: после каждого коммита в `main` раннер заливает файлы репозитория на хостинги **всех -целей** по FTP с TLS и проверяет страницу каждой. Цель — домен со своим хостингом; список целей — `matrix` в workflow: +Решение владельца от 19.10.2026, задача preland#4: сейчас выкатываем **только stage**. -```yaml -- name: barbariskatest # имя цели (видно в списке прогонов: deploy barbariskatest) - url: https://preland.anton.barbariskatest.ru # адрес преленда — для проверки после заливки - prefix: PRELAND # секреты PRELAND_FTP_HOST, PRELAND_FTP_USER, PRELAND_FTP_PASS - dir: "" # каталог сайта на FTP: "" — корень FTP (beget), иначе, например, public_html -``` +| Экземпляр | Назначение | Выкат | +|---|---|---| +| https://zoomstage.online | Тестовый стенд | Push в staging, SFTP | +| https://zoompixel.ru | Production | Отключён до отдельного поручения | +| https://zoompixel.online | Production | Отключён до отдельного поручения | -Цели выкатываются независимо (`fail-fast: false`): сбой одного хостинга не останавливает остальные. +Прежний preland.anton.barbariskatest.ru выведен из использования и исключён из workflow. +Публикация изменений самого workflow в main не запускает доставку: триггер оставлен только для staging. -**Новый домен на своём хостинге:** -1. Домен и HTTPS — как в «Деплой на произвольный домен», шаг 1. -2. Секреты `<ПРЕФИКС>_FTP_HOST`, `<ПРЕФИКС>_FTP_USER`, `<ПРЕФИКС>_FTP_PASS` (Gitea → настройки репозитория → - Actions → секреты; порт 21, FTP с TLS). В коде реквизитов нет. -3. Один раз создать runtime `config.json` с нужными значениями: последующие обычные deploy его не перезапишут. -4. Запись в `matrix` — коммит в `main`, автодеплой выкатит код на все цели, сохраняя их конфиги. +Workflow `.gitea/workflows/deploy.yml` содержит единственную job deploy-zoomstage на +выделенном runner zoomzilla-production. Production возвращается только отдельной задачей +и прямым поручением владельца. Ошибки старого FTP больше не являются блокером выпуска. -**Что заливается.** Каждый deploy получает файлы репозитория (`git ls-files`), кроме служебных -(`.gitea/`, `README.md`, `.gitignore`), а также живущих на хостинге `requisites.html` и `config.json`. -Перед разрешённым изменением конкретного конфигурационного ключа workflow считывает текущий host-файл и -патчит только этот ключ. -На хостинге ничего не удаляется: файлы, убранные из репозитория, удалять вручную. - -**Проверка после заливки.** На странице цели должен быть адрес загрузки на сайте, а не на самом преленде (иначе нет -`config.json` с `host`), и переход на `claim`. Иначе прогон этой цели падает. - -**Откат** — `git revert` проблемного коммита в `main`: автодеплой зальёт прежнюю версию на все цели. - -**SFTP.** Сейчас только FTP с TLS. Хостинг без FTP — поле `protocol` в цели и `lftp` вместо curl в шаге заливки. - -**Откат вручную.** Перед заменой файлов — копия изменяемых файлов (`index.php`, `components/*`) с хостинга; вернуть их той же -загрузкой. Новый преленд требует на сайте `/api/images/public/claim` (api с 2026-10-05): со старым api переход -по ключу отдаст 404. +Рабочие config.json/requisites.html на хостинге не перезаписываются. Корневые наборы +/N/ принадлежат stage и исключены из кодовой доставки. Родительские каталоги создаются +штатным SFTP-генератором; smoke проверяет страницу и assets наборов1/2. +Откат — отдельный согласованный revert в staging, который обновляет только stage. ## Android: фото и файлы (2026-10-19) Основная кнопка выбирает image/*; отдельная ссылка «Выбрать файлы (PDF и др.)» открывает общий picker без ограничения image/*. PDF в подписи скрывается по runtime API-policy. iOS/iPad/Desktop сохраняют прежний общий выбор. +много сохраняется в одной компактной mobile-строке. components/browser-upload.php и CSS upload-picker-links.php — лёгкие inline-копии соответствующих компонентов API: отдельный PHP-хост не использует Yii AssetManager, новый внешний запрос не добавляется. При изменении общего контракта синхронизировать обе копии; API mobile-picker.cjs покрывает обе реальные страницы. Android byte-copy FileProvider сохраняет исходные байты/имя/MIME и ограничен max_file_size_bytes; до ответа policy используется консервативный15MB предел копирования. UTM/client_key/claim/config.json не меняются. - diff --git a/tests/check-versioned-stage-config.php b/tests/check-versioned-stage-config.php index 20badda..2a3414c 100644 --- a/tests/check-versioned-stage-config.php +++ b/tests/check-versioned-stage-config.php @@ -36,30 +36,20 @@ if (!is_array($config)) { } $workflow = (string) file_get_contents($root . '/.gitea/workflows/deploy.yml'); -$legacyStart = strpos($workflow, ' deploy:'); -$stageStart = strpos($workflow, 'deploy-zoomstage:'); -$productionStart = strpos($workflow, 'deploy-zoompixel-production:'); -$legacyBlock = $legacyStart === false || $stageStart === false - ? '' - : substr($workflow, $legacyStart, $stageStart - $legacyStart); -$stageBlock = $stageStart === false || $productionStart === false - ? '' - : substr($workflow, $stageStart, $productionStart - $stageStart); -$productionBlock = $productionStart === false ? '' : substr($workflow, $productionStart); - -foreach ([ - 'legacy FTP' => $legacyBlock, - 'deploy-zoomstage' => $stageBlock, - 'deploy-zoompixel-production' => $productionBlock, -] as $delivery => $block) { - if ($block === '' - || !str_contains($block, 'config\\.json$') - || !str_contains($block, 'ensure-focus-config.php') - || !str_contains($block, 'Preserve host config and add focus if absent')) { - $failures[] = "{$delivery} обязан сохранять хостовый config.json и менять только явно согласованный focus."; - } +$stageStart = strpos($workflow, ' deploy-zoomstage:'); +$stageBlock = $stageStart === false ? '' : substr($workflow, $stageStart); +if ($stageBlock === '' || !str_contains($stageBlock, 'config\\.json$') + || !str_contains($stageBlock, 'ensure-focus-config.php') + || !str_contains($stageBlock, 'Preserve host config and add focus if absent')) { + $failures[] = 'Stage обязан сохранять хостовый config.json и согласованный focus-контракт.'; +} +if (preg_match_all('/^ [a-z][a-z0-9-]*:/m', substr($workflow, strpos($workflow, 'jobs:')), $jobs) !== 1 + || !str_contains($workflow, " - staging") + || str_contains($workflow, " - main") + || str_contains($workflow, 'deploy-zoompixel-production:') + || str_contains($workflow, 'barbariskatest')) { + $failures[] = 'По решению владельца автодеплой должен содержать только staging/zoomstage.online.'; } - if (is_file($root . '/config.json.example')) { $failures[] = 'config.json.example не должен существовать: рабочий config.json единственный.'; } @@ -69,4 +59,4 @@ if ($failures !== []) { exit(1); } -fwrite(STDOUT, "OK: versioned stage config and ZoomPixel delivery\n"); +fwrite(STDOUT, "OK: host config preserved; deploy targets stage only\n");