Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
847c642ca6 | ||
|
|
a1aa25671f | ||
|
|
965eb2e6a5 | ||
|
|
08febba368 | ||
|
|
81e189ca67 | ||
|
|
f30742c2fb | ||
|
|
f4ce5afdc4 | ||
|
|
89cc16eb4a | ||
|
|
188b78a115 | ||
|
|
f87bef2443 | ||
|
|
0e6611da73 | ||
|
|
198ba573f9 | ||
|
|
01933546a7 | ||
|
|
ad8c8eb849 | ||
|
|
5ffb0f37e9 | ||
|
|
c6b2b227c0 | ||
|
|
9c06977e03 | ||
|
|
3740839321 | ||
|
|
ec087ea636 | ||
|
|
a2c3197a16 | ||
|
|
57196e32a9 | ||
|
|
f2ad7ecb4c | ||
|
|
e9a2a0d568 | ||
|
|
144dda6d88 | ||
|
|
0c9a406d68 | ||
|
|
75ccf14889 | ||
|
|
6a2532cb87 | ||
|
|
4c5d3de7bc | ||
|
|
5afa71ec84 | ||
|
|
d70d396670 | ||
|
|
1a20ce17d5 | ||
|
|
f2e6877013 | ||
|
|
7eaf1c12b0 | ||
|
|
c30e3239a0 | ||
|
|
8274946610 | ||
|
|
d2637e8f61 | ||
|
|
0f06a18622 | ||
|
|
0274d5d921 | ||
|
|
dcb914a429 | ||
|
|
ab84dc1629 | ||
|
|
029cd16d46 | ||
|
|
3319499d5e | ||
|
|
953c7dd8f2 | ||
|
|
21f8fbb6a4 | ||
|
|
3ad7be0db3 | ||
|
|
b4dedcfea5 | ||
|
|
19547d1797 | ||
|
|
56456bb0cb | ||
|
|
78b24c91f8 | ||
|
|
be030f25a0 | ||
|
|
498e2df7b9 | ||
|
|
97fe9646b7 | ||
|
|
d646f98aa7 | ||
|
|
35b6a334ba | ||
|
|
a0dee79b22 | ||
|
|
730042385d | ||
|
|
a86978a499 | ||
|
|
ac4cc21361 | ||
|
|
f14ce88cc6 | ||
|
|
9835bb0ef8 | ||
|
|
2180d9e37b |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
components/theme-client.php text eol=lf
|
||||
components/theme-switch.php text eol=lf
|
||||
components/theme-style.php text eol=lf
|
||||
@@ -1,118 +1,13 @@
|
||||
name: deploy-preland
|
||||
|
||||
# После коммита в main сохраняется legacy-выпуск на Beget. Новые домены ZoomPixel
|
||||
# доставляются отдельными SFTP job: staging — только на stage, main — только на production.
|
||||
# Цель — домен со своим хостингом (список — matrix ниже): реквизиты — секреты репозитория <prefix>_FTP_HOST,
|
||||
# <prefix>_FTP_USER, <prefix>_FTP_PASS (порт 21). Цели независимы: сбой одного хостинга остальные не останавливает.
|
||||
# Заливаются файлы репозитория (git ls-files), кроме служебных (.gitea/, README.md, AGENTS.md, CLAUDE.md, .gitignore) и тех, что живут
|
||||
# только на хостинге: requisites.html (реквизиты заполняют на месте — пустой файл из git их бы затёр).
|
||||
# Рабочий config.json настраивается владельцем на каждом хостинге. Деплой не
|
||||
# перезаписывает его: он добавляет только отсутствующие обязательные UTM-ключи.
|
||||
# Единственное контролируемое удаление — завершённая миграция legacy images/demo* после
|
||||
# smoke новых корневых наборов /1, /2. Прочие файлы на хостинге не удаляются.
|
||||
|
||||
# Решение владельца 19.10.2026, preland#4: только stage.
|
||||
# Production выкаты временно отключены; Барбариска больше не используется.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- staging
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: deploy ${{ matrix.name }}
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# name — имя цели, url — адрес преленда (для проверки), prefix — префикс секретов <prefix>_FTP_*,
|
||||
# dir — каталог сайта на FTP ("" — корень FTP, как на beget). Новый домен — новая запись и три секрета;
|
||||
# requisites.html создают на хостинге цели. Рабочий config.json приходит из Git.
|
||||
- name: barbariskatest
|
||||
url: https://preland.anton.barbariskatest.ru
|
||||
prefix: PRELAND
|
||||
dir: ""
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: sh
|
||||
|
||||
env:
|
||||
PRELAND_URL: ${{ matrix.url }}
|
||||
REMOTE_DIR: ${{ matrix.dir }}
|
||||
|
||||
steps:
|
||||
- name: Install tools (Alpine)
|
||||
run: apk add --no-cache git curl php83-cli
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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
|
||||
netrc=$(mktemp); list=$(mktemp)
|
||||
trap 'rm -f "$netrc" "$list"' EXIT
|
||||
printf 'machine %s login %s password %s\n' "$FTP_HOST" "$FTP_USER" "$FTP_PASS" > "$netrc"
|
||||
git ls-files | grep -vE '^(\.gitea/|README\.md$|AGENTS\.md$|CLAUDE\.md$|\.gitignore$|requisites\.html$|config\.json$)' > files.txt
|
||||
# имя уходит в адрес ftp:// как есть — пробелы и прочее экранирование не поддерживаем
|
||||
if grep -q '[^A-Za-z0-9._/-]' files.txt; then echo "имена с недопустимыми символами:"; grep '[^A-Za-z0-9._/-]' files.txt; exit 1; fi
|
||||
# один curl на все файлы — одно соединение FTP+TLS вместо соединения на файл
|
||||
while read -r f; do printf 'upload-file = "%s"\nurl = "ftp://%s/%s%s"\n' "$f" "$FTP_HOST" "${dir:+$dir/}" "$f"; done < files.txt > "$list"
|
||||
curl -sS --ssl-reqd --netrc-file "$netrc" --ftp-create-dirs --retry 3 --fail-early -K "$list"
|
||||
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'
|
||||
@@ -128,9 +23,26 @@ jobs:
|
||||
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 staging branch
|
||||
run: git clone --depth 1 --branch staging http://git.dira89kv.beget.tech:3000/anton/preland.git .
|
||||
env:
|
||||
RELEASE_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -eu
|
||||
git clone --depth 1 --branch staging http://git.dira89kv.beget.tech:3000/anton/preland.git .
|
||||
git fetch --depth 1 origin "$RELEASE_SHA"
|
||||
git checkout --detach "$RELEASE_SHA"
|
||||
[ "$(git rev-parse HEAD)" = "$RELEASE_SHA" ]
|
||||
|
||||
- name: Preserve host config and add focus if absent
|
||||
- name: Validate release and current stage sets before upload
|
||||
run: |
|
||||
set -eu
|
||||
php bin/release-assets.php --check
|
||||
for test in tests/check-*.php; do php "$test"; done
|
||||
php bin/release-assets.php --primary
|
||||
# /N/ по-прежнему принадлежит stage. Любое расхождение требует
|
||||
# сначала перенести актуальный снимок в Git по задаче.
|
||||
php bin/release-assets.php --stage-sets "$PRELAND_URL"
|
||||
|
||||
- name: Apply approved domain brand only if needed
|
||||
env:
|
||||
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
|
||||
SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }}
|
||||
@@ -138,22 +50,24 @@ 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; }
|
||||
status=0
|
||||
php bin/release-brand.php --status "$PRELAND_URL" || status=$?
|
||||
case "$status" in 0) exit 0 ;; 10) ;; *) exit "$status" ;; esac
|
||||
# Только точная ранее согласованная пара заменяется по preland#12.
|
||||
# Обычные выпуски с правильным брендом сюда не доходят.
|
||||
umask 077
|
||||
known_hosts=$(mktemp); batch=$(mktemp); config=$(mktemp)
|
||||
trap 'rm -f "$known_hosts" "$batch" "$config"' EXIT
|
||||
known_hosts=$(mktemp); batch=$(mktemp); files=$(mktemp)
|
||||
trap 'rm -f "$known_hosts" "$batch" "$files"' 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
|
||||
|
||||
php bin/release-brand.php --files "$PRELAND_URL" > "$files"
|
||||
revision=$(git rev-parse HEAD)
|
||||
while read -r source target; do
|
||||
# Оборванный put не должен оставлять усечённый действующий SVG.
|
||||
printf '%s\n' "put -p $source $REMOTE_DIR/$target.upload-$revision" >> "$batch"
|
||||
printf '%s\n' "rename $REMOTE_DIR/$target.upload-$revision $REMOTE_DIR/$target" >> "$batch"
|
||||
done < "$files"
|
||||
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST"
|
||||
php bin/release-brand.php --status "$PRELAND_URL"
|
||||
- name: Upload over SFTP
|
||||
env:
|
||||
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
|
||||
@@ -170,7 +84,11 @@ jobs:
|
||||
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
|
||||
# отдельной синхронизацией по задаче.
|
||||
# Положительный список: tests/bin/планы не публикуются.
|
||||
php bin/release-assets.php --files | grep -vE '^(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"
|
||||
while read -r file; do
|
||||
@@ -185,9 +103,36 @@ jobs:
|
||||
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"
|
||||
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST"
|
||||
echo "залито файлов: $(wc -l < "$files")"
|
||||
|
||||
# api#326/#327: сначала совместимый PHP-код, затем точечная миграция UTM.
|
||||
# chob→banch меняет только токен имени; веса/default владельца сохраняются.
|
||||
# Остальные поля владельца и их JSON-типы сохраняются.
|
||||
- name: Migrate numeric UTM in preserved host config
|
||||
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
|
||||
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" -b "$batch" "$SFTP_USER@$SFTP_HOST"
|
||||
changed=0
|
||||
php bin/ensure-utm-config.php "$config" || changed=$?
|
||||
case "$changed" in
|
||||
0) ;;
|
||||
10) revision=$(git rev-parse HEAD)
|
||||
printf '%s\n' "put -p $config $REMOTE_DIR/config.json.upload-$revision" "rename $REMOTE_DIR/config.json.upload-$revision $REMOTE_DIR/config.json" > "$batch"
|
||||
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST" ;;
|
||||
*) exit "$changed" ;;
|
||||
esac
|
||||
|
||||
- name: Check the page
|
||||
run: |
|
||||
set -eu
|
||||
@@ -198,8 +143,29 @@ jobs:
|
||||
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)")
|
||||
page2=$(curl -fsS --max-time 30 "$PRELAND_URL/?indpic=abcd2efghi&nc=$(date +%s)")
|
||||
printf '%s' "$page2" | grep -q '2/01_mob_1-source.avif' || { echo "indpic=2 не использует второй набор примеров"; exit 1; }
|
||||
php bin/release-assets.php --remote "$PRELAND_URL"
|
||||
php bin/release-brand.php --status "$PRELAND_URL"
|
||||
|
||||
- name: Remove previously published internal files
|
||||
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
|
||||
umask 077
|
||||
known_hosts=$(mktemp); batch=$(mktemp)
|
||||
trap 'rm -f "$known_hosts" "$batch"' EXIT
|
||||
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts"
|
||||
# Только точно известные служебные файлы прежнего deploy, без рекурсии.
|
||||
printf '%s\n' "-rm $REMOTE_DIR/requisites.html" > "$batch"
|
||||
git ls-files tests bin docs/superpowers | while read -r file; do
|
||||
printf '%s\n' "-rm $REMOTE_DIR/$file" >> "$batch"
|
||||
done
|
||||
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST"
|
||||
|
||||
- name: Remove obsolete demo directories
|
||||
env:
|
||||
@@ -223,98 +189,4 @@ jobs:
|
||||
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"
|
||||
|
||||
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
|
||||
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); 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)
|
||||
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"
|
||||
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"
|
||||
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")"
|
||||
|
||||
- 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; }
|
||||
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST"
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 523 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 719 KiB After Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 298 KiB After Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 303 KiB After Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 344 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 533 KiB After Width: | Height: | Size: 716 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 332 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 535 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,5 +1,18 @@
|
||||
# Инструкции агентов для `preland`
|
||||
|
||||
## Поручение 25.10.2026 — preland#21
|
||||
|
||||
Боевые цели теперь `zoompixel.ru` на основном сервере и `zoomila.ru`/`zoomika.ru`
|
||||
на двух REG.RU-хостингах. Владелец явно разрешил первоначальное размещение этих трёх
|
||||
целей; прежняя заморозка #4 для этой задачи снята. Единственный активный `.online` —
|
||||
`zoomstage.online` (stage). `zoompixel.online`, `zoomika.online`, `zoomila.online` отключены,
|
||||
каталоги убраны из рабочих FTP-папок с резервным сохранением; не включать без поручения.
|
||||
На обоих REG.RU-хостингах владельцу нужен SFTP или FTP; существующие доступы описаны
|
||||
в актуальном разделе README, пароли остаются в REG.RU. Доменные готовые SVG — в
|
||||
`brands/`, существующий runtime-config сохранять. Обычный автодеплой остаётся только
|
||||
на stage; новое поручение не включает production-автодеплой. При расхождении со старыми
|
||||
разделами ниже действует это датированное решение и задача #21.
|
||||
|
||||
Область действия — репозиторий `preland/`. Общие правила рабочей области находятся в
|
||||
[AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей —
|
||||
[в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md).
|
||||
@@ -30,7 +43,11 @@
|
||||
такого выпуска фактический stage-config следует забрать и зафиксировать в Git.
|
||||
`requisites.html` также живёт только на хостинге и не перезаписывается. Не
|
||||
создавать target-specific конфиги или `config.json.example`.
|
||||
- Коммит в `main` запускает FTP-автодеплой на все цели workflow; откат через `git revert`
|
||||
- Корневые числовые наборы примеров `/N/` на `zoomstage.online` — редактируемый
|
||||
контент stage. Обычный push в `staging` должен исключать `[0-9]+/` из SFTP-заливки:
|
||||
актуальные примеры нельзя перезаписывать Git-версией. Снимок stage фиксируется в
|
||||
Git отдельной синхронизацией по задаче.
|
||||
- Коммит в `staging` запускает SFTP-автодеплой только на zoomstage.online; откат через `git revert`
|
||||
также запускает выкат. Коммит, push и изменения хостинга — только по прямому поручению.
|
||||
- Проверки кода и браузера проводить в Docker без установки ПО на хост.
|
||||
Проверка загрузки по боевому адресу вызывает реальную обработку; выполнять её только
|
||||
@@ -38,3 +55,28 @@
|
||||
|
||||
План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на
|
||||
политику и документацию не подключают MCP и не дают разрешения на публикацию или выкат.
|
||||
|
||||
## Актуальная маршрутизация (19.10.2026, preland#4)
|
||||
До отдельного поручения владельца выкатывать только staging → zoomstage.online. Production zoompixel.ru/.online не обновлять. preland.anton.barbariskatest.ru больше не используется, не является целью и не должен упоминаться как требующий выпуска. CI запускает только stage. Сохранение runtime config и stage-наборов /N/ обязательно. Работа и решения — через Gitea.
|
||||
|
||||
## Актуальные ресурсы (21.10.2026, preland#11)
|
||||
|
||||
- Git main и staging содержат одно проверенное дерево. Источники: текущая zoomzilla.ru — два логотипа Zoomzilla, четыре PDF, favicon и реквизиты; zoomstage.online — все файлы наборов /1 и /2. История Git не переписывается; известная старая заглушка запрещена в рабочей поставке.
|
||||
- release/assets.json фиксирует SHA-256 всех статических ресурсов (текстовые SVG/CSS/HTML/webmanifest нормализуют CRLF→LF). До выпуска выполнить девять tests/check-*.php, php bin/release-assets.php --check, --primary и --stage-sets https://zoomstage.online. Если источник изменился, сначала синхронизировать актуальные файлы и manifest по задаче, затем повторить проверки. Не отключать guard ради выката.
|
||||
- --primary читает именно текущую страницу основного сайта и проверяет её актуальные URL/реквизиты: старый сохранённый asset URL не доказывает актуальность. Новые логотипы должны пройти визуальную проверку обеих тем/шапки/подвала. Cache version в components/asset-url.php зависит от содержимого.
|
||||
- По прямому выбору владельца21.10 requisites.html теперь входит в каноническую поставку. Единственное новое разрешённое изменение runtime-config — ключ requisites из release/requisites.json через bin/ensure-requisites-config.php; остальные байты полей сохраняются, повторный запуск ничего не меняет. Это явное исключение к прежнему host-only правилу реквизитов, а не разрешение заменять весь config.json. Git config.json хранит согласованный снимок; после выпуска сверять runtime в памяти, не сохранять потенциальные секреты в логи.
|
||||
- /N/ по-прежнему редактируется на stage и не перезаписывается обычной кодовой доставкой; все150файлов перед выкатом сверяются с Git. Drift блокирует выпуск до импорта актуального набора. При полном переносе репозитория набор уже должен быть правильным.
|
||||
- SFTP берёт положительный список из bin/release-assets.php --files: никакие tests/bin/release/plans не публикуются. Старые известные служебные файлы удаляются только по точному списку git ls-files, без рекурсивной очистки. После выпуска --remote сверяет все184ресурса.
|
||||
- Production-преленды остаются замороженными по preland#4. Push main ничего не публикует; staging обновляет только zoomstage.online из точного event SHA.
|
||||
|
||||
## Доменный бренд и минимальный подвал (21.10.2026, preland#12)
|
||||
|
||||
Новое поручение владельца заменяет часть требований #11: в подвале остаются логотип и копирайт, без реквизитов и платёжных значков. Копирайт использует текущий HTTP_HOST без порта/trailing-dot, отдельно от config.host API; непроверенные Host и X-Forwarded-Host не выводятся.
|
||||
|
||||
Логотипы — готовые SVG-контуры, не текст на лету. Для zoomstage.online утверждено имя ZoomStage, слитно без зоны, обе темы. Источники лежат в brands/zoomstage.online/light.svg и dark.svg; release/brands.json содержит явную цель, имя и SHA. В docs/logo.svg и logo-alt.svg хранятся правильные canonical stage-копии для полного переноса. Их размеры182×47 совпадают с viewBox, чтобы загрузка SVG не меняла геометрию.
|
||||
|
||||
Обычный bin/release-assets.php --files исключает оба logo-alias из записи; runtime cache-version рассчитывается по реальному файлу хоста. Отдельный шаг release-brand.php --status разрешает только заранее согласованную миграцию #12:0 — оба SVG уже правильные, записи нет;10 — каждый файл имеет свой известный старый или новый SHA, можно закончить миграцию (включая частичный put);1 — неизвестный домен/хеш, остановка без подмены. --files выдаёт ровно утверждённые source→target. Иные логотипы не перезаписывать общим выкатом. Новые production-домены требуют отдельного добавления готовых пар и явного первоначального размещения; текущий stage-процесс их не активирует.
|
||||
|
||||
primary guard теперь сверяет PDF/favicon, а не заставляет возвращать Zoomzilla и реквизиты на преленд. Реквизиты не отображаются и больше автоматически не синхронизируются; хостовый config и его существующие значения не меняются этим поручением. Устаревшие requisites.html и patch-скрипт удалены из поставки. Статический manifest183, отдельный каталог брендов проверяется по собственным SHA. История #11 сохранена; её прежние инструкции о видимом бренде/реквизитах заменены этим датированным решением.
|
||||
|
||||
Перед выпуском: девять tests/check-*.php и lint, --check/--primary/--stage-sets, оба бренда шапки/подвала на desktop/mobile, перенос Host, отсутствие карт/реквизитов, холодная/медленная загрузка, реальные локальные Preland→Tour→Join/error/retry и геометрия. Production-преленды по #4 не включать; выпускать только staging→zoomstage.online. API продуктовый код не меняется.
|
||||
@@ -1,9 +1,22 @@
|
||||
# Инструкции агентов для `preland`
|
||||
|
||||
## Поручение 25.10.2026 — preland#21
|
||||
|
||||
Боевые цели теперь `zoompixel.ru` на основном сервере и `zoomila.ru`/`zoomika.ru`
|
||||
на двух REG.RU-хостингах. Владелец явно разрешил первоначальное размещение этих трёх
|
||||
целей; прежняя заморозка #4 для этой задачи снята. Единственный активный `.online` —
|
||||
`zoomstage.online` (stage). `zoompixel.online`, `zoomika.online`, `zoomila.online` отключены,
|
||||
каталоги убраны из рабочих FTP-папок с резервным сохранением; не включать без поручения.
|
||||
На обоих REG.RU-хостингах владельцу нужен SFTP или FTP; существующие доступы описаны
|
||||
в актуальном разделе README, пароли остаются в REG.RU. Доменные готовые SVG — в
|
||||
`brands/`, существующий runtime-config сохранять. Обычный автодеплой остаётся только
|
||||
на stage; новое поручение не включает production-автодеплой. При расхождении со старыми
|
||||
разделами ниже действует это датированное решение и задача #21.
|
||||
|
||||
Область действия — репозиторий `preland/`. Общие правила рабочей области находятся в
|
||||
[AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей —
|
||||
[в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md).
|
||||
Этот файл и [AGENTS.md](AGENTS.md) содержат одинаковые локальные правила; обновлять их вместе.
|
||||
Этот файл и [CLAUDE.md](CLAUDE.md) содержат одинаковые локальные правила; обновлять их вместе.
|
||||
|
||||
## Перед работой
|
||||
|
||||
@@ -21,10 +34,19 @@
|
||||
переход напрямую на `/public/upscale` не заменяет передачу владельца через claim.
|
||||
- `host` задаёт сайт назначения; HTTPS необходим для `crypto.randomUUID()`.
|
||||
Не обходить серверные правила доступа к загрузке и результату обработки.
|
||||
- Рабочий `config.json` единственный для всех прилендов: это точная проверенная
|
||||
конфигурация `zoomstage.online`, и каждый FTP/SFTP-deploy обязан доставлять её на
|
||||
stage и production. `requisites.html` по-прежнему живёт только на хостинге и не
|
||||
перезаписывается. Не создавать target-specific конфиги или `config.json.example`.
|
||||
- Рабочий `config.json` на каждом хостинге настраивает владелец. Деплой не имеет
|
||||
права перезаписывать его значений. При появлении новой обязательной UTM workflow
|
||||
получает текущий хостовый конфиг и меняет только ключи, которые прямо
|
||||
согласованы в задаче. Иные значения, включая уже настроенные UTM, остаются
|
||||
байт-в-байт прежними. Для #274 `bin/ensure-focus-config.php` — явное
|
||||
исключение: он задаёт только новый `focus=face` (default и rnd). После каждого
|
||||
такого выпуска фактический stage-config следует забрать и зафиксировать в Git.
|
||||
`requisites.html` также живёт только на хостинге и не перезаписывается. Не
|
||||
создавать target-specific конфиги или `config.json.example`.
|
||||
- Корневые числовые наборы примеров `/N/` на `zoomstage.online` — редактируемый
|
||||
контент stage. Обычный push в `staging` должен исключать `[0-9]+/` из SFTP-заливки:
|
||||
актуальные примеры нельзя перезаписывать Git-версией. Снимок stage фиксируется в
|
||||
Git отдельной синхронизацией по задаче.
|
||||
- Коммит в `main` запускает FTP-автодеплой на все цели workflow; откат через `git revert`
|
||||
также запускает выкат. Коммит, push и изменения хостинга — только по прямому поручению.
|
||||
- Проверки кода и браузера проводить в Docker без установки ПО на хост.
|
||||
@@ -33,3 +55,25 @@
|
||||
|
||||
План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на
|
||||
политику и документацию не подключают MCP и не дают разрешения на публикацию или выкат.
|
||||
|
||||
## Актуальные ресурсы (21.10.2026, preland#11)
|
||||
|
||||
- Git main и staging содержат одно проверенное дерево. Источники: текущая zoomzilla.ru — два логотипа Zoomzilla, четыре PDF, favicon и реквизиты; zoomstage.online — все файлы наборов /1 и /2. История Git не переписывается; известная старая заглушка запрещена в рабочей поставке.
|
||||
- release/assets.json фиксирует SHA-256 всех статических ресурсов (текстовые SVG/CSS/HTML/webmanifest нормализуют CRLF→LF). До выпуска выполнить девять tests/check-*.php, php bin/release-assets.php --check, --primary и --stage-sets https://zoomstage.online. Если источник изменился, сначала синхронизировать актуальные файлы и manifest по задаче, затем повторить проверки. Не отключать guard ради выката.
|
||||
- --primary читает именно текущую страницу основного сайта и проверяет её актуальные URL/реквизиты: старый сохранённый asset URL не доказывает актуальность. Новые логотипы должны пройти визуальную проверку обеих тем/шапки/подвала. Cache version в components/asset-url.php зависит от содержимого.
|
||||
- По прямому выбору владельца21.10 requisites.html теперь входит в каноническую поставку. Единственное новое разрешённое изменение runtime-config — ключ requisites из release/requisites.json через bin/ensure-requisites-config.php; остальные байты полей сохраняются, повторный запуск ничего не меняет. Это явное исключение к прежнему host-only правилу реквизитов, а не разрешение заменять весь config.json. Git config.json хранит согласованный снимок; после выпуска сверять runtime в памяти, не сохранять потенциальные секреты в логи.
|
||||
- /N/ по-прежнему редактируется на stage и не перезаписывается обычной кодовой доставкой; все150файлов перед выкатом сверяются с Git. Drift блокирует выпуск до импорта актуального набора. При полном переносе репозитория набор уже должен быть правильным.
|
||||
- SFTP берёт положительный список из bin/release-assets.php --files: никакие tests/bin/release/plans не публикуются. Старые известные служебные файлы удаляются только по точному списку git ls-files, без рекурсивной очистки. После выпуска --remote сверяет все184ресурса.
|
||||
- Production-преленды остаются замороженными по preland#4. Push main ничего не публикует; staging обновляет только zoomstage.online из точного event SHA.
|
||||
|
||||
## Доменный бренд и минимальный подвал (21.10.2026, preland#12)
|
||||
|
||||
Новое поручение владельца заменяет часть требований #11: в подвале остаются логотип и копирайт, без реквизитов и платёжных значков. Копирайт использует текущий HTTP_HOST без порта/trailing-dot, отдельно от config.host API; непроверенные Host и X-Forwarded-Host не выводятся.
|
||||
|
||||
Логотипы — готовые SVG-контуры, не текст на лету. Для zoomstage.online утверждено имя ZoomStage, слитно без зоны, обе темы. Источники лежат в brands/zoomstage.online/light.svg и dark.svg; release/brands.json содержит явную цель, имя и SHA. В docs/logo.svg и logo-alt.svg хранятся правильные canonical stage-копии для полного переноса. Их размеры182×47 совпадают с viewBox, чтобы загрузка SVG не меняла геометрию.
|
||||
|
||||
Обычный bin/release-assets.php --files исключает оба logo-alias из записи; runtime cache-version рассчитывается по реальному файлу хоста. Отдельный шаг release-brand.php --status разрешает только заранее согласованную миграцию #12:0 — оба SVG уже правильные, записи нет;10 — каждый файл имеет свой известный старый или новый SHA, можно закончить миграцию (включая частичный put);1 — неизвестный домен/хеш, остановка без подмены. --files выдаёт ровно утверждённые source→target. Иные логотипы не перезаписывать общим выкатом. Новые production-домены требуют отдельного добавления готовых пар и явного первоначального размещения; текущий stage-процесс их не активирует.
|
||||
|
||||
primary guard теперь сверяет PDF/favicon, а не заставляет возвращать Zoomzilla и реквизиты на преленд. Реквизиты не отображаются и больше автоматически не синхронизируются; хостовый config и его существующие значения не меняются этим поручением. Устаревшие requisites.html и patch-скрипт удалены из поставки. Статический manifest183, отдельный каталог брендов проверяется по собственным SHA. История #11 сохранена; её прежние инструкции о видимом бренде/реквизитах заменены этим датированным решением.
|
||||
|
||||
Перед выпуском: девять tests/check-*.php и lint, --check/--primary/--stage-sets, оба бренда шапки/подвала на desktop/mobile, перенос Host, отсутствие карт/реквизитов, холодная/медленная загрузка, реальные локальные Preland→Tour→Join/error/retry и геометрия. Production-преленды по #4 не включать; выпускать только staging→zoomstage.online. API продуктовый код не меняется.
|
||||
@@ -1,39 +1,78 @@
|
||||
# Преленд zoomzilla
|
||||
|
||||
## Боевые домены — поручение 25.10.2026, preland#21
|
||||
|
||||
Три боевые цели: `zoompixel.ru` на основном сервере, `zoomila.ru` и `zoomika.ru`
|
||||
на двух отдельных хостингах REG.RU. Это отдельное разрешение владельца на первоначальное
|
||||
размещение/обновление этих целей после заморозки #4. Единственный активный `.online` —
|
||||
`zoomstage.online`, staging прелендингов. `zoompixel.online`, `zoomika.online` и
|
||||
`zoomila.online` отключены по дополнительному решению владельца; их регистрация сохранена.
|
||||
Рабочие FTP/SFTP-каталоги не должны содержать директории отключённых `.online`.
|
||||
|
||||
| Домен | Размещение | Пользователь SFTP/FTP | Каталог сайта |
|
||||
|---|---|---|---|
|
||||
| `zoomika.ru` | `server197.hosting.reg.ru`, `31.31.196.183` | `u3695919` | `/www/zoomika.ru` относительно домашнего каталога |
|
||||
| `zoomila.ru` | `server124.hosting.reg.ru`, `37.140.192.195` | `u3695920` | `/www/zoomila.ru` относительно домашнего каталога |
|
||||
| `zoompixel.ru` | основной сервер, `/srv/preland/domains/zoompixel.ru` | существующий административный доступ | отдельный контейнер `preland-zoompixel-ru-1` |
|
||||
|
||||
REG.RU выдаёт SFTP/SSH и FTP отдельно: SFTP использует порт 22 и пароль из блока
|
||||
«Доступ в панель управления, SSH, SFTP»; FTP — порт 21 и отдельный пароль из блока
|
||||
«Доступ к FTP». Пароли смотреть в кабинете REG.RU → нужный хостинг → «Доступы»;
|
||||
не помещать в Git и журналы. Оба аккаунта уже созданы провайдером, сбрасывать пароли
|
||||
для размещения не требуется. Полные домашние каталоги — `/var/www/<пользователь>/data`.
|
||||
В `www` каждого REG.RU-аккаунта оставлен только соответствующий `.ru`. Сайты `.online`
|
||||
выключены через ispmanager, их исходные файлы и установочные архивы сохранены вне webroot
|
||||
в скрытом `.preland21-backup/` (каталог `site`, файлы `release.zip`, `initial-index.html`).
|
||||
На основном сервере `zoompixel.online` отвечает HTTP/HTTPS 410; его контейнер удалён,
|
||||
сервис исключён из Compose, исходники сохранены в `/srv/preland/backups/preland21-disabled-online/site`.
|
||||
|
||||
Логотипы для обеих тем — готовые SVG в `brands/<домен>/`, реестр — `release/brands.json`.
|
||||
При первоначальном размещении копировать только положительный список `releaseFiles()`
|
||||
и проверенные статические ресурсы (включая `/1`, `/2`), затем доменную пару в
|
||||
`docs/logo.svg`/`docs/logo-alt.svg`. Служебные `bin/`, `tests/`, `.git/`, планы и
|
||||
установочные архивы в webroot не входят. Архивы REG.RU хранить вне `/www`.
|
||||
У новых пустых сайтов исходный config — проверенный stage-снимок; существующий
|
||||
`zoompixel.ru/config.json` сохраняется байт-в-байт, старые UTM нормализуются в памяти.
|
||||
|
||||
Перед трафиком обязательны доверенный TLS (самоподписанный сертификат REG.RU не подходит),
|
||||
HTTP→HTTPS, проверка двух тем/ширин, примеров, upload→claim→Tour→Join с UTM,
|
||||
видимой ошибки и повторной попытки. Точный состав, коммиты, результаты и ограничения — #21.
|
||||
Обычная автоматическая доставка по-прежнему только `staging` → `zoomstage.online`;
|
||||
данное первоначальное размещение не включает production-автодеплой по push `main`.
|
||||
Исторические разделы ниже описывают прежние цели; при расхождении действует этот раздел.
|
||||
|
||||
Одностраничный лендинг перед основным сайтом (`zoomzilla.ru`): посетитель с рекламы выбирает фото прямо на
|
||||
преленде, файл сразу уходит в API сайта, а сам посетитель переходит на сайт — на страницу обработки этого фото.
|
||||
Варианты страницы и сайта (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=e9st1mm12b&theme=e9st1mm12b°r=e9st2mm12b®=e9st2mm12b&banch=e9st2mm12b&tp=e9st1mm12b&utm_source=test&subid=CLICK1`.
|
||||
|
||||
## Как работает
|
||||
|
||||
### Метки
|
||||
### Метки (api#326)
|
||||
|
||||
`index.php` разбирает параметры адреса по разделу `utm` из `config.json`. Для каждой настроенной метки:
|
||||
Из публичного значения учитывается только пятый символ: цифра1–9 выбирает вариант,0 на Preland выбирает его по прежним `weights`. Отсутствующий, короткий, массивный и невалидный ввод получает `default`. Открытые `1`, `rnd`, `dark`, старые имена `registration/bchoice/chob/chb` больше не выбирают варианты. Точная длина входной строки не ограничена; Unicode-шум не изменяет позицию управляющего символа.
|
||||
|
||||
- значение из адреса принимается, если оно есть в `weights`;
|
||||
- `rnd` — случайное значение по весам (`weights`), так делится трафик между вариантами;
|
||||
- неизвестное значение отбрасывается, отсутствующее — заменяется на `default`.
|
||||
| Метка | Числовые значения |
|
||||
|---|---|
|
||||
| `indpic` | номер настроенного набора1–9 (`/1/`, `/2/` и т.д.) |
|
||||
| `theme` |1 dark,2 light |
|
||||
| `degr` | номер настроенной группы1–9 |
|
||||
| `reg` |1 before (PIN до оплаты),2 after |
|
||||
| `banch` |1 без попапа банка,2 с попапом |
|
||||
| `tp` | номер настроенной тарифной группы1–9 |
|
||||
| `cons` | номер группы согласий1–9; существование проверяет API |
|
||||
| `focus` |1 face,2 center |
|
||||
|
||||
Ненастроенные параметры (`utm_source`, `keyword`, `subid` и любые другие) проходят как есть. Перед переходом
|
||||
Preland всегда формирует полный resolved-набор своих восьми меток: явное допустимое значение остаётся, `rnd`
|
||||
разрешается по весам, отсутствующее получает `default`. Вместе с внешними рекламными параметрами этот набор
|
||||
уходит в `claim`, а затем сайт переносит его в `upscale`; ни один из семи параметров не должен пропадать только
|
||||
потому, что был назначен default.
|
||||
`config.json` хранит публичные числовые `weights/default`. Внутри PHP сохраняются прежние semantic enum. Runtime-конфиг мигрируется `bin/ensure-utm-config.php`: только согласованные UTM, с сохранением весов, default и JSON-типов соседних полей. Скрипт идемпотентен; старые focus/cons patch-скрипты больше не вызываются доставкой.
|
||||
|
||||
| Метка | Что задаёт | Где |
|
||||
|---|---|---|
|
||||
| `indpic` | картинка-пример на преленде | корневой каталог сета `/1/`, `/2/` и т. д. |
|
||||
| `theme` | `light` / `dark` | преленд и сайт |
|
||||
| `degr` | пресет ухудшения превью «До» | сайт (#42) |
|
||||
| `registration` | `before` — PIN до оплаты, `after` — после | сайт, страница результата |
|
||||
| `bchoice` | попап выбора банка (`a` — показать) | сайт, оплата (#44) |
|
||||
| `chb` | галочки согласия на странице оплаты (`no` — не обязательны) | сайт, оплата |
|
||||
| `tp` | группа тарифов | сайт, cpanel «Группы тарифов (tp)» |
|
||||
| `focus` | первоначальная область лупы Join: `center`/`face` | сайт, cpanel после `tp` |
|
||||
В api#327 публичное `chob` заменено на `banch`; внутренний `bchoice` сохраняется. Мигратор переименовывает текущий host-ключ токенно, сохраняя все остальные байты, включая веса и default. Одновременные `banch/chob/bchoice` блокируют patch без записи. До миграции код читает прежний host-config; сохранённые recovery-записи с `utmDigits.chob` или `redirectUtm` продолжают выбранный вариант и выпускают уже `banch`.
|
||||
|
||||
Перед каждым переходом Preland создаёт для всех восьми меток новый шум из10 ASCII букв/цифр, с уже выбранной цифрой в пятой позиции. Retry и recovery меняют только шум; вариант повторно не выбирается. Выбранные цифры сохраняются в upload record. Старые записи IndexedDB адаптируются только на пути recovery, без публичных alias старого URL.
|
||||
|
||||
Чужие параметры (`utm_source`, `keyword`, `subid` и любые другие) сохраняют значения после обычного URL-кодирования. Технические `key/id/stage/shown_theme` не маскируются; внешний `key` не может подменить одноразовый ключ загрузки. `shown_theme` берётся из фактически показанной темы непосредственно при переходе.
|
||||
|
||||
### Загрузка и переход на сайт
|
||||
|
||||
@@ -76,7 +115,7 @@ Preland всегда формирует полный resolved-набор сво
|
||||
| `requisites` | нет | реквизиты в подвале — список строк, каждая отдельным абзацем (текст экранируется). Важнее `requisites.html`; без обоих — заглушка «ИП Иванов» |
|
||||
| `assetsUriPrefix` | нет | откуда брать статику. Пусто — из своего каталога; если на хостинге нет каталога `images`, статика берётся с сайта (`https://<host>`) |
|
||||
| `fontsUrl`, `faviconUrl`, `spritePath` | нет | адреса шрифтов, иконок и SVG-спрайта, если они не в `assetsUriPrefix` |
|
||||
| `logoAltExt` | нет | расширение второго логотипа (`svg` по умолчанию) |
|
||||
| `logoAltExt` | устарело | После preland#11 обе темы используют проверенные SVG Zoomzilla; расширение не меняется runtime-конфигом. |
|
||||
| `title` | нет | заголовок страницы («Улучшение изображения») |
|
||||
| `default_language` | нет | сейчас не действует: язык всегда `ru` |
|
||||
|
||||
@@ -99,10 +138,16 @@ Preland хранит наборы ровно как Index, поэтому их
|
||||
N/05_...
|
||||
```
|
||||
|
||||
`indpic=N` выбирает корневой каталог `/N/`. Набор копируется с Index в корень
|
||||
`indpic=abcdNefghi` выбирает корневой каталог `/N/`. Набор копируется с Index в корень
|
||||
домена целиком: без переименования, вложения в `images/` и промежуточных
|
||||
каталогов. Устаревшие `images/demo*` не являются частью поставки.
|
||||
|
||||
Для `zoomstage.online` обычный deploy ветки `staging` намеренно **не загружает**
|
||||
корневые числовые каталоги `/N/`: это редактируемый контент stage, а не артефакт
|
||||
кодовой доставки. Workflow продолжает проверять их доступность HTTP-smoke. Чтобы
|
||||
зафиксировать обновлённый набор, сначала синхронизируйте его со stage в Git по
|
||||
отдельной задаче; следующая заливка не перетрёт его старой ревизией.
|
||||
|
||||
## Деплой на произвольный домен
|
||||
|
||||
**Требования.** PHP 8.0+ (боевой хостинг — 8.2), веб-сервер с `index.php` в роли индексного файла и **HTTPS**:
|
||||
@@ -114,10 +159,7 @@ N/05_...
|
||||
git clone http://git.dira89kv.beget.tech:3000/anton/preland.git && cd preland
|
||||
# дальше — загрузка на хостинг: rsync/scp по SSH или FTP-клиент
|
||||
```
|
||||
3. **Конфиг и реквизиты.** Рабочий `config.json` уже лежит в репозитории и обязателен к доставке на каждый
|
||||
приленд. Его не заменяют локальным вариантом и не исключают из синхронизации. Реквизиты задаются ключом
|
||||
`requisites` в нём либо runtime-файлом `requisites.html`. 2026-10-05 конфиг пропал после заливки,
|
||||
и преленд слал загрузку на собственный домен; это больше не допускается.
|
||||
3. **Конфиг и реквизиты.** Рабочий `config.json` на хостинге — источник истины. При первичном создании домена его настраивают отдельно; обычный deploy не заменяет его репозиторным снимком и не перезаписывает `requisites.html`. Менять можно только явно согласованные ключи после чтения текущего файла.
|
||||
4. **nginx без Apache** (если хостинг не обрабатывает `.htaccess`):
|
||||
```nginx
|
||||
server {
|
||||
@@ -142,43 +184,57 @@ N/05_...
|
||||
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' 'https://<host>/api/images/public/claim?key=x'
|
||||
# 302 на https://<host>/ — точка передачи на сайте есть
|
||||
```
|
||||
В браузере: открыть `https://<домен>/?indpic=1&theme=dark&subid=TEST`, выбрать фото — адрес сменится на
|
||||
`https://<host>/public/upscale?id=…&indpic=1&theme=dark&subid=TEST…` (без `key=`), пойдёт обработка.
|
||||
В браузере: открыть `https://<домен>/?indpic=abcd1efghi&theme=abcd1efghi&subid=TEST`, выбрать фото — адрес сменится на
|
||||
`https://<host>/public/upscale?id=…&indpic=<новый шум с1>&theme=<новый шум с1>&subid=TEST…` (без `key=`), пойдёт обработка.
|
||||
Это настоящая загрузка и обработка на сайте.
|
||||
|
||||
## Автодеплой (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. Никаких ручных конфигов не создавать: единственный `config.json` приезжает вместе с кодом.
|
||||
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` всегда доставляется вместе с кодом.
|
||||
На хостинге ничего не удаляется: файлы, убранные из репозитория, удалять вручную.
|
||||
Рабочие config.json/requisites.html на хостинге не перезаписываются. Корневые наборы
|
||||
/N/ принадлежат stage и исключены из кодовой доставки. Родительские каталоги создаются
|
||||
штатным SFTP-генератором; smoke проверяет страницу и assets наборов1/2.
|
||||
Откат — отдельный согласованный revert в staging, который обновляет только stage.
|
||||
|
||||
**Проверка после заливки.** На странице цели должен быть адрес загрузки на сайте, а не на самом преленде (иначе нет
|
||||
`config.json` с `host`), и переход на `claim`. Иначе прогон этой цели падает.
|
||||
## 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 не меняются.
|
||||
|
||||
**Откат** — `git revert` проблемного коммита в `main`: автодеплой зальёт прежнюю версию на все цели.
|
||||
## Актуальные ресурсы (21.10.2026, preland#11)
|
||||
|
||||
**SFTP.** Сейчас только FTP с TLS. Хостинг без FTP — поле `protocol` в цели и `lftp` вместо curl в шаге заливки.
|
||||
- Git main и staging содержат одно проверенное дерево. Источники: текущая zoomzilla.ru — два логотипа Zoomzilla, четыре PDF, favicon и реквизиты; zoomstage.online — все файлы наборов /1 и /2. История Git не переписывается; известная старая заглушка запрещена в рабочей поставке.
|
||||
- release/assets.json фиксирует SHA-256 всех статических ресурсов (текстовые SVG/CSS/HTML/webmanifest нормализуют CRLF→LF). До выпуска выполнить девять tests/check-*.php, php bin/release-assets.php --check, --primary и --stage-sets https://zoomstage.online. Если источник изменился, сначала синхронизировать актуальные файлы и manifest по задаче, затем повторить проверки. Не отключать guard ради выката.
|
||||
- --primary читает именно текущую страницу основного сайта и проверяет её актуальные URL/реквизиты: старый сохранённый asset URL не доказывает актуальность. Новые логотипы должны пройти визуальную проверку обеих тем/шапки/подвала. Cache version в components/asset-url.php зависит от содержимого.
|
||||
- По прямому выбору владельца21.10 requisites.html теперь входит в каноническую поставку. Единственное новое разрешённое изменение runtime-config — ключ requisites из release/requisites.json через bin/ensure-requisites-config.php; остальные байты полей сохраняются, повторный запуск ничего не меняет. Это явное исключение к прежнему host-only правилу реквизитов, а не разрешение заменять весь config.json. Git config.json хранит согласованный снимок; после выпуска сверять runtime в памяти, не сохранять потенциальные секреты в логи.
|
||||
- /N/ по-прежнему редактируется на stage и не перезаписывается обычной кодовой доставкой; все150файлов перед выкатом сверяются с Git. Drift блокирует выпуск до импорта актуального набора. При полном переносе репозитория набор уже должен быть правильным.
|
||||
- SFTP берёт положительный список из bin/release-assets.php --files: никакие tests/bin/release/plans не публикуются. Старые известные служебные файлы удаляются только по точному списку git ls-files, без рекурсивной очистки. После выпуска --remote сверяет все184ресурса.
|
||||
- Production-преленды остаются замороженными по preland#4. Push main ничего не публикует; staging обновляет только zoomstage.online из точного event SHA.
|
||||
|
||||
**Откат вручную.** Перед заменой файлов — копия изменяемых файлов (`index.php`, `components/*`) с хостинга; вернуть их той же
|
||||
загрузкой. Новый преленд требует на сайте `/api/images/public/claim` (api с 2026-10-05): со старым api переход
|
||||
по ключу отдаст 404.
|
||||
Повторная проверка выпуска: php bin/release-assets.php --stage-requisites https://zoomstage.online проверяет именно runtime config; успешная загрузка requisites.html сама по себе недостаточна. Все обязательные SFTP-команды выполняются через batch -b и прекращают выкат при ошибке put.
|
||||
|
||||
## Доменный бренд и минимальный подвал (21.10.2026, preland#12)
|
||||
|
||||
Новое поручение владельца заменяет часть требований #11: в подвале остаются логотип и копирайт, без реквизитов и платёжных значков. Копирайт использует текущий HTTP_HOST без порта/trailing-dot, отдельно от config.host API; непроверенные Host и X-Forwarded-Host не выводятся.
|
||||
|
||||
Логотипы — готовые SVG-контуры, не текст на лету. Для zoomstage.online утверждено имя ZoomStage, слитно без зоны, обе темы. Источники лежат в brands/zoomstage.online/light.svg и dark.svg; release/brands.json содержит явную цель, имя и SHA. В docs/logo.svg и logo-alt.svg хранятся правильные canonical stage-копии для полного переноса. Их размеры182×47 совпадают с viewBox, чтобы загрузка SVG не меняла геометрию.
|
||||
|
||||
Обычный bin/release-assets.php --files исключает оба logo-alias из записи; runtime cache-version рассчитывается по реальному файлу хоста. Отдельный шаг release-brand.php --status разрешает только заранее согласованную миграцию #12:0 — оба SVG уже правильные, записи нет;10 — каждый файл имеет свой известный старый или новый SHA, можно закончить миграцию (включая частичный put);1 — неизвестный домен/хеш, остановка без подмены. --files выдаёт ровно утверждённые source→target. Иные логотипы не перезаписывать общим выкатом. Новые production-домены требуют отдельного добавления готовых пар и явного первоначального размещения; текущий stage-процесс их не активирует.
|
||||
|
||||
primary guard теперь сверяет PDF/favicon, а не заставляет возвращать Zoomzilla и реквизиты на преленд. Реквизиты не отображаются и больше автоматически не синхронизируются; хостовый config и его существующие значения не меняются этим поручением. Устаревшие requisites.html и patch-скрипт удалены из поставки. Статический manifest183, отдельный каталог брендов проверяется по собственным SHA. История #11 сохранена; её прежние инструкции о видимом бренде/реквизитах заменены этим датированным решением.
|
||||
|
||||
Перед выпуском: девять tests/check-*.php и lint, --check/--primary/--stage-sets, оба бренда шапки/подвала на desktop/mobile, перенос Host, отсутствие карт/реквизитов, холодная/медленная загрузка, реальные локальные Preland→Tour→Join/error/retry и геометрия. Production-преленды по #4 не включать; выпускать только staging→zoomstage.online. API продуктовый код не меняется.
|
||||
|
||||
Дополнение приёмки #12: tests/e2e/brand-footer.cjs проверяет именно видимый SVG по центру шапки, а не только headerbox. PUBLISHED=1 использует zoomstage.online (или PRELAND_URL); без него — изолированный pre301.localhost QA proxy. Запускать в имеющемся Docker Playwright с /candidate=репозиторий,/qa=артефакты; POST/PUT блокируются. Пропущенное при compact CSS правило Bootstrap navbar>.container восстановлено во всех3CSS: API уже имеет его из полного Bootstrap. Это устраняет переход logo y0→13.5px, который рамочный тест не замечал.
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/** Готовые доменные SVG, без генерации логотипа при запросе страницы. */
|
||||
declare(strict_types=1);
|
||||
function prelandBrand(string $root, string $domain): array
|
||||
{
|
||||
$catalog = json_decode(file_get_contents($root . '/release/brands.json'), true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!isset($catalog[$domain])) throw new RuntimeException('Для домена не утверждён бренд: ' . $domain);
|
||||
$brand = $catalog[$domain];
|
||||
if (array_keys($brand['logos']) !== ['docs/logo.svg', 'docs/logo-alt.svg']) throw new RuntimeException('Нужна полная пара SVG');
|
||||
foreach ($brand['logos'] as $target => $record) {
|
||||
if (!preg_match('~^brands/' . preg_quote($domain, '~') . '/(?:light|dark)\.svg$~D', $record['file'])) throw new RuntimeException('Некорректный путь бренда');
|
||||
$body = str_replace("\r\n", "\n", file_get_contents($root . '/' . $record['file']));
|
||||
if (hash('sha256', $body) !== $record['sha256'] || str_contains($body, '<text') || !str_contains($body, '<title>' . $brand['label'] . '</title>')) throw new RuntimeException('Не совпал утверждённый готовый SVG: ' . $target);
|
||||
}
|
||||
return $brand;
|
||||
}
|
||||
|
||||
function prelandBrandState(array $actual, array $brand): int
|
||||
{
|
||||
$expected = array_map(static fn(array $record): string => $record['sha256'], $brand['logos']);
|
||||
if ($actual === $expected) return 0;
|
||||
if (array_keys($actual) !== array_keys($expected)) return 1;
|
||||
foreach ($expected as $path => $hash) {
|
||||
if ($actual[$path] !== $hash && $actual[$path] !== ($brand['replace_from'][$path] ?? null)) return 1;
|
||||
}
|
||||
return 10;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/** preland#17: миграция только utm.chb → utm.cons; остальные значения владельца сохраняются. */
|
||||
declare(strict_types=1);
|
||||
if ($argc !== 2) {
|
||||
fwrite(STDERR, "Использование: php bin/ensure-cons-config.php /path/to/config.json\n");
|
||||
exit(2);
|
||||
}
|
||||
try {
|
||||
$path = $argv[1];
|
||||
$raw = @file_get_contents($path);
|
||||
if ($raw === false) throw new RuntimeException('Не удалось прочитать config.json.');
|
||||
// Object decode сохраняет типы соседних {} и объектов с числовыми ключами.
|
||||
$config = json_decode($raw, false, 512, JSON_THROW_ON_ERROR);
|
||||
if (!$config instanceof stdClass || !($config->utm ?? null) instanceof stdClass) throw new RuntimeException('В config.json отсутствует объект utm.');
|
||||
$hasCons = property_exists($config->utm, 'cons');
|
||||
$cons = $hasCons ? $config->utm->cons : (object) ['weights' => (object) ['1' => 100, '2' => 0], 'default' => '2'];
|
||||
if (!$cons instanceof stdClass || !(is_array($cons->weights ?? null) || ($cons->weights ?? null) instanceof stdClass)
|
||||
|| !(is_string($cons->default ?? null) || is_int($cons->default ?? null))
|
||||
|| !preg_match('/^[1-9][0-9]*$/D', (string) $cons->default)
|
||||
|| (float) $cons->default > 2147483647) {
|
||||
throw new RuntimeException('Существующий utm.cons некорректен; автоматическая перезапись запрещена.');
|
||||
}
|
||||
if (!property_exists($config->utm, 'chb') && $hasCons) {
|
||||
fwrite(STDOUT, "cons уже настроен: config не изменён.\n");
|
||||
exit(0);
|
||||
}
|
||||
unset($config->utm->chb);
|
||||
$config->utm->cons = $cons;
|
||||
$encoded = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n";
|
||||
if (file_put_contents($path, $encoded, LOCK_EX) === false) throw new RuntimeException('Не удалось записать config.json.');
|
||||
fwrite(STDOUT, "Мигрированы только utm.chb/cons; остальные поля и существующие настройки cons сохранены.\n");
|
||||
exit(10);
|
||||
} catch (Throwable $error) {
|
||||
fwrite(STDERR, $error->getMessage() . "\n");
|
||||
exit(2);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
/** api#326: заменяет только согласованные JSON-токены, сохраняя прочие байты. */
|
||||
declare(strict_types=1);
|
||||
|
||||
/** Карта диапазонов исходного JSON; числовые токены никогда не сериализуются. */
|
||||
function utmConfigNode(string $raw, int &$offset): array
|
||||
{
|
||||
$length=strlen($raw);
|
||||
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
|
||||
$start=$offset; $char=$raw[$offset] ?? '';
|
||||
if ($char==='"') {
|
||||
$offset++;
|
||||
while ($offset<$length) {
|
||||
if ($raw[$offset]==='\\') { $offset+=2; continue; }
|
||||
if ($raw[$offset++]==='"') break;
|
||||
}
|
||||
return ['start'=>$start,'end'=>$offset,'type'=>'string','value'=>json_decode(substr($raw,$start,$offset-$start),true,512,JSON_THROW_ON_ERROR)];
|
||||
}
|
||||
if ($char==='{' || $char==='[') {
|
||||
$object=$char==='{'; $close=$object ? '}' : ']'; $offset++; $properties=[];
|
||||
while (true) {
|
||||
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
|
||||
if (($raw[$offset] ?? '')===$close) { $offset++; break; }
|
||||
if ($object) {
|
||||
$key=utmConfigNode($raw,$offset);
|
||||
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
|
||||
$offset++; // Двоеточие уже проверено полным JSON validation.
|
||||
$value=utmConfigNode($raw,$offset);
|
||||
$properties[]=['key'=>$key,'value'=>$value];
|
||||
} else { utmConfigNode($raw,$offset); }
|
||||
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
|
||||
if (($raw[$offset] ?? '')===',') { $offset++; continue; }
|
||||
$offset++; break;
|
||||
}
|
||||
return ['start'=>$start,'end'=>$offset,'type'=>$object ? 'object' : 'array','properties'=>$properties];
|
||||
}
|
||||
while ($offset<$length && !str_contains(",]} \t\r\n",$raw[$offset])) $offset++;
|
||||
return ['start'=>$start,'end'=>$offset,'type'=>'scalar'];
|
||||
}
|
||||
|
||||
/** Выбирает единственное свойство; неоднозначные управляющие поля не мигрирует. */
|
||||
function utmConfigProperty(array $node, string $key): ?array
|
||||
{
|
||||
$found=null;
|
||||
foreach ($node['properties'] ?? [] as $property) {
|
||||
if ($property['key']['value']!==$key) continue;
|
||||
if ($found!==null) throw new RuntimeException('Повторное управляющее поле: '.$key);
|
||||
$found=$property;
|
||||
}
|
||||
return $found;
|
||||
}
|
||||
|
||||
if ($argc!==2) { fwrite(STDERR,"Использование: php bin/ensure-utm-config.php config.json\n"); exit(2); }
|
||||
try {
|
||||
$path=$argv[1]; $raw=file_get_contents($path);
|
||||
// Валидация всей грамматики без последующей сериализации decoded данных.
|
||||
json_decode($raw,false,512,JSON_THROW_ON_ERROR|JSON_BIGINT_AS_STRING);
|
||||
$offset=0; $root=utmConfigNode($raw,$offset);
|
||||
$utm=utmConfigProperty($root,'utm')['value'] ?? null;
|
||||
if (($root['type'] ?? null)!=='object' || ($utm['type'] ?? null)!=='object') throw new RuntimeException('Отсутствует объект utm');
|
||||
$replacements=[];
|
||||
$replace=static function(array $node,string $value) use (&$replacements): void {
|
||||
$replacements[]=['start'=>$node['start'],'end'=>$node['end'],'text'=>json_encode($value,JSON_THROW_ON_ERROR)];
|
||||
};
|
||||
foreach (['theme'=>['dark'=>'1','light'=>'2'],'reg'=>['before'=>'1','after'=>'2'],'banch'=>['b'=>'1','a'=>'2'],'focus'=>['face'=>'1','center'=>'2']] as $key=>$map) {
|
||||
$property=utmConfigProperty($utm,$key);
|
||||
$aliases=['reg'=>['registration'],'banch'=>['chob','bchoice']][$key] ?? [];
|
||||
foreach ($aliases as $old) {
|
||||
$legacy=utmConfigProperty($utm,$old);
|
||||
if ($property && $legacy) throw new RuntimeException('Конфликт старого и нового UTM: '.$key);
|
||||
if ($legacy) { $replace($legacy['key'],$key); $property=$legacy; }
|
||||
}
|
||||
if (!$property) continue;
|
||||
$setting=$property['value'];
|
||||
$weights=utmConfigProperty($setting,'weights')['value'] ?? null;
|
||||
$default=utmConfigProperty($setting,'default')['value'] ?? null;
|
||||
if (($setting['type'] ?? null)!=='object' || ($weights['type'] ?? null)!=='object' || !$default) throw new RuntimeException('Некорректный UTM: '.$key);
|
||||
$seen=[];
|
||||
foreach ($weights['properties'] as $variant) {
|
||||
$name=$variant['key']['value']; $digit=$map[$name] ?? $name;
|
||||
if (!in_array($digit,['1','2'],true) || isset($seen[$digit])) throw new RuntimeException('Неоднозначный вариант UTM: '.$key);
|
||||
$seen[$digit]=true;
|
||||
if ($digit!==$name) $replace($variant['key'],$digit);
|
||||
}
|
||||
$value=json_decode(substr($raw,$default['start'],$default['end']-$default['start']),false,512,JSON_THROW_ON_ERROR);
|
||||
if (!(is_string($value)||is_int($value))) throw new RuntimeException('Некорректный default UTM: '.$key);
|
||||
$digit=$map[$value] ?? $value;
|
||||
if (!in_array((string)$digit,['1','2'],true)) throw new RuntimeException('Некорректный default UTM: '.$key);
|
||||
if ($digit!==$value) $replace($default,$digit);
|
||||
}
|
||||
if (!$replacements) { echo "Числовые UTM уже настроены; config не изменён.\n"; exit(0); }
|
||||
usort($replacements,static fn(array $a,array $b): int=>$b['start']<=>$a['start']);
|
||||
foreach ($replacements as $replacement) $raw=substr_replace($raw,$replacement['text'],$replacement['start'],$replacement['end']-$replacement['start']);
|
||||
if (file_put_contents($path,$raw,LOCK_EX)===false) throw new RuntimeException('Не удалось записать config');
|
||||
echo "Мигрированы только согласованные имена и enum UTM; остальные байты сохранены.\n"; exit(10);
|
||||
} catch(Throwable $error) { fwrite(STDERR,$error->getMessage()."\n"); exit(2); }
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/** preland#11: проверенный состав ресурсов; исполняется до SFTP. */
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/brand-contract.php';
|
||||
|
||||
function releaseAssetBytes(string $path, string $bytes): string
|
||||
{
|
||||
return preg_match('/\.(svg|css|html|webmanifest)$/', $path) ? str_replace("\r\n", "\n", $bytes) : $bytes;
|
||||
}
|
||||
|
||||
function releaseStaticFiles(string $root): array
|
||||
{
|
||||
$files = [];
|
||||
foreach (scandir($root) as $name) {
|
||||
if (in_array($name, ['docs', 'favicon', 'fonts', 'images'], true) || ctype_digit($name)) {
|
||||
$directory = $root . '/' . $name;
|
||||
if (!is_dir($directory)) continue;
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS));
|
||||
foreach ($iterator as $entry) {
|
||||
$path = str_replace('\\', '/', substr($entry->getPathname(), strlen($root) + 1));
|
||||
if ($name === 'docs' && !preg_match('~^docs/[^/]+\.(svg|pdf)$~', $path)) continue;
|
||||
if ($entry->isFile()) $files[] = $path;
|
||||
}
|
||||
} elseif (preg_match('/\.css$/', $name)) {
|
||||
$files[] = $name;
|
||||
}
|
||||
}
|
||||
sort($files, SORT_STRING);
|
||||
return $files;
|
||||
}
|
||||
|
||||
function validateReleaseAssets(string $root): array
|
||||
{
|
||||
$manifest = json_decode((string) file_get_contents($root . '/release/assets.json'), true, 512, JSON_THROW_ON_ERROR);
|
||||
$files = $manifest['files'] ?? [];
|
||||
$brands = json_decode(file_get_contents($root . '/release/brands.json'), true, 512, JSON_THROW_ON_ERROR);
|
||||
foreach (array_keys($brands) as $domain) prelandBrand($root, $domain);
|
||||
$required = ['docs/logo.svg', 'docs/logo-alt.svg', 'docs/privacy_policy.pdf', 'docs/public_offer.pdf', 'docs/tariffs.pdf', 'docs/user_agreement.pdf'];
|
||||
foreach ($required as $path) {
|
||||
if (!isset($files[$path])) throw new RuntimeException('В manifest отсутствует обязательный ресурс ' . $path);
|
||||
}
|
||||
$names = array_keys($files); sort($names, SORT_STRING);
|
||||
if ($names !== releaseStaticFiles($root)) throw new RuntimeException('Состав статических ресурсов отличается от manifest; синхронизируйте актуальный набор');
|
||||
foreach ($files as $path => $record) {
|
||||
if (!preg_match('~^[A-Za-z0-9._/-]+$~', $path) || str_contains($path, '..')) throw new RuntimeException('Недопустимый путь ресурса');
|
||||
$bytes = releaseAssetBytes($path, (string) file_get_contents($root . '/' . $path));
|
||||
$blob = sha1('blob ' . strlen($bytes) . chr(0) . $bytes);
|
||||
if ($blob === '91267982e1ad373b6ebd5fb2b761b34fe2a0c2a5' || preg_match('/WBE\s*70/i', $bytes)) {
|
||||
throw new RuntimeException('Запрещённая заглушка бренда: ' . $path);
|
||||
}
|
||||
if (($record['sha256'] ?? '') !== hash('sha256', $bytes)) throw new RuntimeException('Ресурс не соответствует согласованному снимку: ' . $path);
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
function releaseFiles(string $root): array
|
||||
{
|
||||
$files = array_values(array_filter(releaseStaticFiles($root), static fn(string $path): bool => !preg_match('~^(?:[0-9]+/|docs/logo(?:-alt)?\.svg$)~', $path)));
|
||||
foreach (['.htaccess', 'index.php', 'i18n.php', 'template.php'] as $path) $files[] = $path;
|
||||
foreach (['components', 'messages'] as $directory) {
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . '/' . $directory, FilesystemIterator::SKIP_DOTS));
|
||||
foreach ($iterator as $entry) if ($entry->isFile() && $entry->getExtension() === 'php') {
|
||||
$files[] = str_replace('\\', '/', substr($entry->getPathname(), strlen($root) + 1));
|
||||
}
|
||||
}
|
||||
sort($files, SORT_STRING);
|
||||
return $files;
|
||||
}
|
||||
|
||||
function verifyRemoteReleaseAssets(array $files, string $base, bool $setsOnly): void
|
||||
{
|
||||
if (!preg_match('~^https://[A-Za-z0-9.-]+$~', $base)) throw new RuntimeException('Требуется HTTPS origin');
|
||||
if (!$setsOnly) {
|
||||
$brand = prelandBrand(dirname(__DIR__), (string) parse_url($base, PHP_URL_HOST));
|
||||
foreach ($brand['logos'] as $path => $record) $files[$path]['sha256'] = $record['sha256'];
|
||||
}
|
||||
$count = 0;
|
||||
foreach ($files as $path => $record) {
|
||||
if ($setsOnly && !preg_match('~^[0-9]+/~', $path)) continue;
|
||||
$context = stream_context_create(['http' => ['timeout' => 30, 'header' => "Cache-Control: no-cache\r\n"], 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true]]);
|
||||
$bytes = @file_get_contents($base . '/' . $path . '?release_sha=' . $record['sha256'], false, $context);
|
||||
if ($bytes === false || hash('sha256', releaseAssetBytes($path, $bytes)) !== $record['sha256']) {
|
||||
throw new RuntimeException('Хост отличается от Git: ' . $path . '. Сначала синхронизируйте актуальный снимок по задаче.');
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
fwrite(STDERR, "OK: remote {$count} resources\n");
|
||||
}
|
||||
|
||||
/** Сверка с текущей страницей основного сайта, а не с оставшимся старым asset URL. */
|
||||
function verifyPrimaryAssets(string $root, array $files): void
|
||||
{
|
||||
$origin = 'https://zoomzilla.ru';
|
||||
$context = stream_context_create(['http' => ['timeout' => 30, 'header' => "Cache-Control: no-cache\r\n"]]);
|
||||
$html = @file_get_contents($origin . '/?theme=light&preland_asset_check=1', false, $context);
|
||||
if ($html === false) throw new RuntimeException('Основной сайт недоступен для сверки ресурсов');
|
||||
foreach ($files as $path => $record) {
|
||||
if (str_starts_with($path, 'docs/logo')) continue;
|
||||
if (!str_starts_with($path, 'docs/') && !str_starts_with($path, 'favicon/')) continue;
|
||||
$name = basename($path === 'docs/logo.svg' ? 'logo-light.svg' : $path);
|
||||
$pattern = '~(?:src|href)=["\']([^"\']*/' . preg_quote($name, '~') . '(?:\?[^"\']*)?)["\']~';
|
||||
if (!preg_match($pattern, $html, $match)) {
|
||||
// Часть размеров favicon ссылается только из manifest; общий каталог тот же.
|
||||
if (!str_starts_with($path, 'favicon/') || !preg_match('~href=["\']([^"\']*/favicon\.ico)["\']~', $html, $base)) throw new RuntimeException('Текущая ссылка основного сайта не найдена: ' . $path);
|
||||
$url = dirname($base[1]) . '/' . $name;
|
||||
} else $url = html_entity_decode($match[1], ENT_QUOTES, 'UTF-8');
|
||||
if (str_starts_with($url, '/')) $url = $origin . $url;
|
||||
if (!str_starts_with($url, $origin . '/')) throw new RuntimeException('Неожиданный origin ресурса: ' . $path);
|
||||
$bytes = @file_get_contents($url, false, $context);
|
||||
if ($bytes === false || hash('sha256', releaseAssetBytes($path, $bytes)) !== $record['sha256']) {
|
||||
throw new RuntimeException('Основной сайт обновлён: синхронизируйте ' . $path . ' в Git перед выпуском');
|
||||
}
|
||||
}
|
||||
fwrite(STDERR, "OK: primary PDF/favicon актуальны\n");
|
||||
}
|
||||
|
||||
if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
|
||||
try {
|
||||
$root = dirname(__DIR__);
|
||||
$files = validateReleaseAssets($root);
|
||||
$mode = $argv[1] ?? '--check';
|
||||
if ($mode === '--files') echo implode("\n", releaseFiles($root)), "\n";
|
||||
elseif ($mode === '--primary') verifyPrimaryAssets($root, $files);
|
||||
elseif ($mode === '--remote' || $mode === '--stage-sets') verifyRemoteReleaseAssets($files, $argv[2] ?? '', $mode === '--stage-sets');
|
||||
elseif ($mode === '--check') echo 'OK: release assets ', count($files), "\n";
|
||||
else throw new RuntimeException('Неизвестный режим');
|
||||
} catch (Throwable $error) {
|
||||
fwrite(STDERR, $error->getMessage() . "\n"); exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/** Отдельная явно согласованная миграция бренда. Обычная code-поставка SVG не пишет. */
|
||||
declare(strict_types=1);
|
||||
require __DIR__ . '/brand-contract.php';
|
||||
try {
|
||||
$mode = $argv[1] ?? ''; $url = $argv[2] ?? '';
|
||||
if (!preg_match('~^https://([a-z0-9.-]+)$~D', $url, $match)) throw new RuntimeException('Требуется HTTPS origin цели');
|
||||
$brand = prelandBrand(dirname(__DIR__), $match[1]);
|
||||
if ($mode === '--files') {
|
||||
foreach ($brand['logos'] as $target => $record) echo $record['file'], ' ', $target, "\n";
|
||||
exit(0);
|
||||
}
|
||||
if ($mode !== '--status') throw new RuntimeException('Неизвестный режим бренда');
|
||||
$actual = []; $expected = [];
|
||||
foreach ($brand['logos'] as $target => $record) {
|
||||
$context = stream_context_create(['http' => ['timeout' => 30, 'header' => "Cache-Control: no-cache\r\n"]]);
|
||||
$body = @file_get_contents($url . '/' . $target . '?brand_check=' . $record['sha256'], false, $context);
|
||||
if ($body === false) throw new RuntimeException('Не удалось прочитать бренд цели: ' . $target);
|
||||
$actual[$target] = hash('sha256', str_replace("\r\n", "\n", $body));
|
||||
$expected[$target] = $record['sha256'];
|
||||
}
|
||||
$state = prelandBrandState($actual, $brand);
|
||||
if ($state === 0) { echo "OK: доменный бренд совпал; SVG не перезаписываются\n"; exit(0); }
|
||||
if ($state === 10) { echo "Разрешена точная миграция бренда по preland#12\n"; exit(10); }
|
||||
throw new RuntimeException('На хосте другой бренд: обычный deploy не имеет права его менять');
|
||||
} catch (Throwable $error) { fwrite(STDERR, $error->getMessage() . "\n"); exit(1); }
|
||||