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
|
name: deploy-preland
|
||||||
|
|
||||||
# После коммита в main сохраняется legacy-выпуск на Beget. Новые домены ZoomPixel
|
# Решение владельца 19.10.2026, preland#4: только stage.
|
||||||
# доставляются отдельными SFTP job: staging — только на stage, main — только на production.
|
# 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. Прочие файлы на хостинге не удаляются.
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
|
||||||
- staging
|
- staging
|
||||||
|
|
||||||
jobs:
|
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:
|
deploy-zoomstage:
|
||||||
name: deploy zoomstage.online
|
name: deploy zoomstage.online
|
||||||
if: github.ref == 'refs/heads/staging'
|
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
|
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
|
- 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:
|
env:
|
||||||
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
|
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
|
||||||
SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }}
|
SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }}
|
||||||
@@ -138,22 +50,24 @@ jobs:
|
|||||||
SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }}
|
SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }}
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
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
|
umask 077
|
||||||
known_hosts=$(mktemp); batch=$(mktemp); config=$(mktemp)
|
known_hosts=$(mktemp); batch=$(mktemp); files=$(mktemp)
|
||||||
trap 'rm -f "$known_hosts" "$batch" "$config"' EXIT
|
trap 'rm -f "$known_hosts" "$batch" "$files"' EXIT
|
||||||
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts"
|
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts"
|
||||||
printf '%s\n' "get -p $REMOTE_DIR/config.json $config" > "$batch"
|
php bin/release-brand.php --files "$PRELAND_URL" > "$files"
|
||||||
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch"
|
revision=$(git rev-parse HEAD)
|
||||||
changed=0
|
while read -r source target; do
|
||||||
php bin/ensure-focus-config.php "$config" || changed=$?
|
# Оборванный put не должен оставлять усечённый действующий SVG.
|
||||||
case "$changed" in
|
printf '%s\n' "put -p $source $REMOTE_DIR/$target.upload-$revision" >> "$batch"
|
||||||
0) ;;
|
printf '%s\n' "rename $REMOTE_DIR/$target.upload-$revision $REMOTE_DIR/$target" >> "$batch"
|
||||||
10) printf '%s\n' "put -p $config $REMOTE_DIR/config.json" > "$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"
|
||||||
*) exit "$changed" ;;
|
php bin/release-brand.php --status "$PRELAND_URL"
|
||||||
esac
|
|
||||||
|
|
||||||
- name: Upload over SFTP
|
- name: Upload over SFTP
|
||||||
env:
|
env:
|
||||||
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
|
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
|
||||||
@@ -170,7 +84,11 @@ jobs:
|
|||||||
dirs=$(mktemp)
|
dirs=$(mktemp)
|
||||||
trap 'rm -f "$known_hosts" "$batch" "$files" "$dirs"' EXIT
|
trap 'rm -f "$known_hosts" "$batch" "$files" "$dirs"' EXIT
|
||||||
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts"
|
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
|
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"
|
printf '%s\n' "-mkdir $REMOTE_DIR" > "$batch"
|
||||||
while read -r file; do
|
while read -r file; do
|
||||||
@@ -185,9 +103,36 @@ jobs:
|
|||||||
done < "$files"
|
done < "$files"
|
||||||
sort -u "$dirs" | while read -r dir; do printf '%s\n' "-mkdir $REMOTE_DIR/$dir" >> "$batch"; done
|
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"
|
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")"
|
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
|
- name: Check the page
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
@@ -198,8 +143,29 @@ jobs:
|
|||||||
printf '%s' "$page" | grep -q 'claim?key=' || { echo "переход не на claim"; exit 1; }
|
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/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; }
|
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; }
|
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
|
- name: Remove obsolete demo directories
|
||||||
env:
|
env:
|
||||||
@@ -223,98 +189,4 @@ jobs:
|
|||||||
done
|
done
|
||||||
printf '%s\n' "-rmdir $REMOTE_DIR/images/$set" >> "$batch"
|
printf '%s\n' "-rmdir $REMOTE_DIR/images/$set" >> "$batch"
|
||||||
done
|
done
|
||||||
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"
|
||||||
|
|
||||||
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; }
|
|
||||||
|
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`
|
# Инструкции агентов для `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/`. Общие правила рабочей области находятся в
|
Область действия — репозиторий `preland/`. Общие правила рабочей области находятся в
|
||||||
[AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей —
|
[AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей —
|
||||||
[в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md).
|
[в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md).
|
||||||
@@ -30,7 +43,11 @@
|
|||||||
такого выпуска фактический stage-config следует забрать и зафиксировать в Git.
|
такого выпуска фактический stage-config следует забрать и зафиксировать в Git.
|
||||||
`requisites.html` также живёт только на хостинге и не перезаписывается. Не
|
`requisites.html` также живёт только на хостинге и не перезаписывается. Не
|
||||||
создавать target-specific конфиги или `config.json.example`.
|
создавать 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 и изменения хостинга — только по прямому поручению.
|
также запускает выкат. Коммит, push и изменения хостинга — только по прямому поручению.
|
||||||
- Проверки кода и браузера проводить в Docker без установки ПО на хост.
|
- Проверки кода и браузера проводить в Docker без установки ПО на хост.
|
||||||
Проверка загрузки по боевому адресу вызывает реальную обработку; выполнять её только
|
Проверка загрузки по боевому адресу вызывает реальную обработку; выполнять её только
|
||||||
@@ -38,3 +55,28 @@
|
|||||||
|
|
||||||
План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на
|
План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на
|
||||||
политику и документацию не подключают MCP и не дают разрешения на публикацию или выкат.
|
политику и документацию не подключают 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`
|
# Инструкции агентов для `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/`. Общие правила рабочей области находятся в
|
Область действия — репозиторий `preland/`. Общие правила рабочей области находятся в
|
||||||
[AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей —
|
[AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей —
|
||||||
[в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md).
|
[в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md).
|
||||||
Этот файл и [AGENTS.md](AGENTS.md) содержат одинаковые локальные правила; обновлять их вместе.
|
Этот файл и [CLAUDE.md](CLAUDE.md) содержат одинаковые локальные правила; обновлять их вместе.
|
||||||
|
|
||||||
## Перед работой
|
## Перед работой
|
||||||
|
|
||||||
@@ -21,10 +34,19 @@
|
|||||||
переход напрямую на `/public/upscale` не заменяет передачу владельца через claim.
|
переход напрямую на `/public/upscale` не заменяет передачу владельца через claim.
|
||||||
- `host` задаёт сайт назначения; HTTPS необходим для `crypto.randomUUID()`.
|
- `host` задаёт сайт назначения; HTTPS необходим для `crypto.randomUUID()`.
|
||||||
Не обходить серверные правила доступа к загрузке и результату обработки.
|
Не обходить серверные правила доступа к загрузке и результату обработки.
|
||||||
- Рабочий `config.json` единственный для всех прилендов: это точная проверенная
|
- Рабочий `config.json` на каждом хостинге настраивает владелец. Деплой не имеет
|
||||||
конфигурация `zoomstage.online`, и каждый FTP/SFTP-deploy обязан доставлять её на
|
права перезаписывать его значений. При появлении новой обязательной UTM workflow
|
||||||
stage и production. `requisites.html` по-прежнему живёт только на хостинге и не
|
получает текущий хостовый конфиг и меняет только ключи, которые прямо
|
||||||
перезаписывается. Не создавать target-specific конфиги или `config.json.example`.
|
согласованы в задаче. Иные значения, включая уже настроенные 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`
|
- Коммит в `main` запускает FTP-автодеплой на все цели workflow; откат через `git revert`
|
||||||
также запускает выкат. Коммит, push и изменения хостинга — только по прямому поручению.
|
также запускает выкат. Коммит, push и изменения хостинга — только по прямому поручению.
|
||||||
- Проверки кода и браузера проводить в Docker без установки ПО на хост.
|
- Проверки кода и браузера проводить в Docker без установки ПО на хост.
|
||||||
@@ -33,3 +55,25 @@
|
|||||||
|
|
||||||
План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на
|
План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на
|
||||||
политику и документацию не подключают MCP и не дают разрешения на публикацию или выкат.
|
политику и документацию не подключают 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
|
# Преленд 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`): посетитель с рекламы выбирает фото прямо на
|
Одностраничный лендинг перед основным сайтом (`zoomzilla.ru`): посетитель с рекламы выбирает фото прямо на
|
||||||
преленде, файл сразу уходит в API сайта, а сам посетитель переходит на сайт — на страницу обработки этого фото.
|
преленде, файл сразу уходит в API сайта, а сам посетитель переходит на сайт — на страницу обработки этого фото.
|
||||||
Варианты страницы и сайта (A/B) задаются метками в адресе. Код — обычный PHP без фреймворка и сборки; переводы —
|
Варианты страницы и сайта (A/B) задаются метками в адресе. Код — обычный PHP без фреймворка и сборки; переводы —
|
||||||
маленькая заглушка `Yii::t` в `i18n.php`.
|
маленькая заглушка `Yii::t` в `i18n.php`.
|
||||||
|
|
||||||
Боевой экземпляр: `https://preland.anton.barbariskatest.ru` (хостинг beget). Пример ссылки входа:
|
Активные production: `https://zoompixel.ru`, `https://zoompixel.online`; текущая цель выката — только `https://zoomstage.online`. Пример ссылки входа:
|
||||||
`https://preland.anton.barbariskatest.ru/?indpic=1&theme=dark°r=2®istration=after&bchoice=a&tp=1&utm_source=test&subid=CLICK1`.
|
`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` и любые другие) проходят как есть. Перед переходом
|
`config.json` хранит публичные числовые `weights/default`. Внутри PHP сохраняются прежние semantic enum. Runtime-конфиг мигрируется `bin/ensure-utm-config.php`: только согласованные UTM, с сохранением весов, default и JSON-типов соседних полей. Скрипт идемпотентен; старые focus/cons patch-скрипты больше не вызываются доставкой.
|
||||||
Preland всегда формирует полный resolved-набор своих восьми меток: явное допустимое значение остаётся, `rnd`
|
|
||||||
разрешается по весам, отсутствующее получает `default`. Вместе с внешними рекламными параметрами этот набор
|
|
||||||
уходит в `claim`, а затем сайт переносит его в `upscale`; ни один из семи параметров не должен пропадать только
|
|
||||||
потому, что был назначен default.
|
|
||||||
|
|
||||||
| Метка | Что задаёт | Где |
|
В api#327 публичное `chob` заменено на `banch`; внутренний `bchoice` сохраняется. Мигратор переименовывает текущий host-ключ токенно, сохраняя все остальные байты, включая веса и default. Одновременные `banch/chob/bchoice` блокируют patch без записи. До миграции код читает прежний host-config; сохранённые recovery-записи с `utmDigits.chob` или `redirectUtm` продолжают выбранный вариант и выпускают уже `banch`.
|
||||||
|---|---|---|
|
|
||||||
| `indpic` | картинка-пример на преленде | корневой каталог сета `/1/`, `/2/` и т. д. |
|
Перед каждым переходом Preland создаёт для всех восьми меток новый шум из10 ASCII букв/цифр, с уже выбранной цифрой в пятой позиции. Retry и recovery меняют только шум; вариант повторно не выбирается. Выбранные цифры сохраняются в upload record. Старые записи IndexedDB адаптируются только на пути recovery, без публичных alias старого URL.
|
||||||
| `theme` | `light` / `dark` | преленд и сайт |
|
|
||||||
| `degr` | пресет ухудшения превью «До» | сайт (#42) |
|
Чужие параметры (`utm_source`, `keyword`, `subid` и любые другие) сохраняют значения после обычного URL-кодирования. Технические `key/id/stage/shown_theme` не маскируются; внешний `key` не может подменить одноразовый ключ загрузки. `shown_theme` берётся из фактически показанной темы непосредственно при переходе.
|
||||||
| `registration` | `before` — PIN до оплаты, `after` — после | сайт, страница результата |
|
|
||||||
| `bchoice` | попап выбора банка (`a` — показать) | сайт, оплата (#44) |
|
|
||||||
| `chb` | галочки согласия на странице оплаты (`no` — не обязательны) | сайт, оплата |
|
|
||||||
| `tp` | группа тарифов | сайт, cpanel «Группы тарифов (tp)» |
|
|
||||||
| `focus` | первоначальная область лупы Join: `center`/`face` | сайт, cpanel после `tp` |
|
|
||||||
|
|
||||||
### Загрузка и переход на сайт
|
### Загрузка и переход на сайт
|
||||||
|
|
||||||
@@ -76,7 +115,7 @@ Preland всегда формирует полный resolved-набор сво
|
|||||||
| `requisites` | нет | реквизиты в подвале — список строк, каждая отдельным абзацем (текст экранируется). Важнее `requisites.html`; без обоих — заглушка «ИП Иванов» |
|
| `requisites` | нет | реквизиты в подвале — список строк, каждая отдельным абзацем (текст экранируется). Важнее `requisites.html`; без обоих — заглушка «ИП Иванов» |
|
||||||
| `assetsUriPrefix` | нет | откуда брать статику. Пусто — из своего каталога; если на хостинге нет каталога `images`, статика берётся с сайта (`https://<host>`) |
|
| `assetsUriPrefix` | нет | откуда брать статику. Пусто — из своего каталога; если на хостинге нет каталога `images`, статика берётся с сайта (`https://<host>`) |
|
||||||
| `fontsUrl`, `faviconUrl`, `spritePath` | нет | адреса шрифтов, иконок и SVG-спрайта, если они не в `assetsUriPrefix` |
|
| `fontsUrl`, `faviconUrl`, `spritePath` | нет | адреса шрифтов, иконок и SVG-спрайта, если они не в `assetsUriPrefix` |
|
||||||
| `logoAltExt` | нет | расширение второго логотипа (`svg` по умолчанию) |
|
| `logoAltExt` | устарело | После preland#11 обе темы используют проверенные SVG Zoomzilla; расширение не меняется runtime-конфигом. |
|
||||||
| `title` | нет | заголовок страницы («Улучшение изображения») |
|
| `title` | нет | заголовок страницы («Улучшение изображения») |
|
||||||
| `default_language` | нет | сейчас не действует: язык всегда `ru` |
|
| `default_language` | нет | сейчас не действует: язык всегда `ru` |
|
||||||
|
|
||||||
@@ -99,10 +138,16 @@ Preland хранит наборы ровно как Index, поэтому их
|
|||||||
N/05_...
|
N/05_...
|
||||||
```
|
```
|
||||||
|
|
||||||
`indpic=N` выбирает корневой каталог `/N/`. Набор копируется с Index в корень
|
`indpic=abcdNefghi` выбирает корневой каталог `/N/`. Набор копируется с Index в корень
|
||||||
домена целиком: без переименования, вложения в `images/` и промежуточных
|
домена целиком: без переименования, вложения в `images/` и промежуточных
|
||||||
каталогов. Устаревшие `images/demo*` не являются частью поставки.
|
каталогов. Устаревшие `images/demo*` не являются частью поставки.
|
||||||
|
|
||||||
|
Для `zoomstage.online` обычный deploy ветки `staging` намеренно **не загружает**
|
||||||
|
корневые числовые каталоги `/N/`: это редактируемый контент stage, а не артефакт
|
||||||
|
кодовой доставки. Workflow продолжает проверять их доступность HTTP-smoke. Чтобы
|
||||||
|
зафиксировать обновлённый набор, сначала синхронизируйте его со stage в Git по
|
||||||
|
отдельной задаче; следующая заливка не перетрёт его старой ревизией.
|
||||||
|
|
||||||
## Деплой на произвольный домен
|
## Деплой на произвольный домен
|
||||||
|
|
||||||
**Требования.** PHP 8.0+ (боевой хостинг — 8.2), веб-сервер с `index.php` в роли индексного файла и **HTTPS**:
|
**Требования.** 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
|
git clone http://git.dira89kv.beget.tech:3000/anton/preland.git && cd preland
|
||||||
# дальше — загрузка на хостинг: rsync/scp по SSH или FTP-клиент
|
# дальше — загрузка на хостинг: rsync/scp по SSH или FTP-клиент
|
||||||
```
|
```
|
||||||
3. **Конфиг и реквизиты.** Рабочий `config.json` уже лежит в репозитории и обязателен к доставке на каждый
|
3. **Конфиг и реквизиты.** Рабочий `config.json` на хостинге — источник истины. При первичном создании домена его настраивают отдельно; обычный deploy не заменяет его репозиторным снимком и не перезаписывает `requisites.html`. Менять можно только явно согласованные ключи после чтения текущего файла.
|
||||||
приленд. Его не заменяют локальным вариантом и не исключают из синхронизации. Реквизиты задаются ключом
|
|
||||||
`requisites` в нём либо runtime-файлом `requisites.html`. 2026-10-05 конфиг пропал после заливки,
|
|
||||||
и преленд слал загрузку на собственный домен; это больше не допускается.
|
|
||||||
4. **nginx без Apache** (если хостинг не обрабатывает `.htaccess`):
|
4. **nginx без Apache** (если хостинг не обрабатывает `.htaccess`):
|
||||||
```nginx
|
```nginx
|
||||||
server {
|
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'
|
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' 'https://<host>/api/images/public/claim?key=x'
|
||||||
# 302 на https://<host>/ — точка передачи на сайте есть
|
# 302 на https://<host>/ — точка передачи на сайте есть
|
||||||
```
|
```
|
||||||
В браузере: открыть `https://<домен>/?indpic=1&theme=dark&subid=TEST`, выбрать фото — адрес сменится на
|
В браузере: открыть `https://<домен>/?indpic=abcd1efghi&theme=abcd1efghi&subid=TEST`, выбрать фото — адрес сменится на
|
||||||
`https://<host>/public/upscale?id=…&indpic=1&theme=dark&subid=TEST…` (без `key=`), пойдёт обработка.
|
`https://<host>/public/upscale?id=…&indpic=<новый шум с1>&theme=<новый шум с1>&subid=TEST…` (без `key=`), пойдёт обработка.
|
||||||
Это настоящая загрузка и обработка на сайте.
|
Это настоящая загрузка и обработка на сайте.
|
||||||
|
|
||||||
## Автодеплой (Gitea Actions)
|
## Автодеплой (Gitea Actions)
|
||||||
|
|
||||||
`.gitea/workflows/deploy.yml`: после каждого коммита в `main` раннер заливает файлы репозитория на хостинги **всех
|
Решение владельца от 19.10.2026, задача preland#4: сейчас выкатываем **только stage**.
|
||||||
целей** по FTP с TLS и проверяет страницу каждой. Цель — домен со своим хостингом; список целей — `matrix` в workflow:
|
|
||||||
|
|
||||||
```yaml
|
| Экземпляр | Назначение | Выкат |
|
||||||
- name: barbariskatest # имя цели (видно в списке прогонов: deploy barbariskatest)
|
|---|---|---|
|
||||||
url: https://preland.anton.barbariskatest.ru # адрес преленда — для проверки после заливки
|
| https://zoomstage.online | Тестовый стенд | Push в staging, SFTP |
|
||||||
prefix: PRELAND # секреты PRELAND_FTP_HOST, PRELAND_FTP_USER, PRELAND_FTP_PASS
|
| https://zoompixel.ru | Production | Отключён до отдельного поручения |
|
||||||
dir: "" # каталог сайта на FTP: "" — корень FTP (beget), иначе, например, public_html
|
| https://zoompixel.online | Production | Отключён до отдельного поручения |
|
||||||
```
|
|
||||||
|
|
||||||
Цели выкатываются независимо (`fail-fast: false`): сбой одного хостинга не останавливает остальные.
|
Прежний preland.anton.barbariskatest.ru выведен из использования и исключён из workflow.
|
||||||
|
Публикация изменений самого workflow в main не запускает доставку: триггер оставлен только для staging.
|
||||||
|
|
||||||
**Новый домен на своём хостинге:**
|
Workflow `.gitea/workflows/deploy.yml` содержит единственную job deploy-zoomstage на
|
||||||
1. Домен и HTTPS — как в «Деплой на произвольный домен», шаг 1.
|
выделенном runner zoomzilla-production. Production возвращается только отдельной задачей
|
||||||
2. Секреты `<ПРЕФИКС>_FTP_HOST`, `<ПРЕФИКС>_FTP_USER`, `<ПРЕФИКС>_FTP_PASS` (Gitea → настройки репозитория →
|
и прямым поручением владельца. Ошибки старого FTP больше не являются блокером выпуска.
|
||||||
Actions → секреты; порт 21, FTP с TLS). В коде реквизитов нет.
|
|
||||||
3. Никаких ручных конфигов не создавать: единственный `config.json` приезжает вместе с кодом.
|
|
||||||
4. Запись в `matrix` — коммит в `main`, автодеплой выкатит все цели вместе с конфигом.
|
|
||||||
|
|
||||||
**Что заливается.** Каждый deploy получает файлы репозитория (`git ls-files`), кроме служебных
|
Рабочие config.json/requisites.html на хостинге не перезаписываются. Корневые наборы
|
||||||
(`.gitea/`, `README.md`, `.gitignore`) и живущего на хостинге `requisites.html`. Единственный
|
/N/ принадлежат stage и исключены из кодовой доставки. Родительские каталоги создаются
|
||||||
версионируемый `config.json` всегда доставляется вместе с кодом.
|
штатным SFTP-генератором; smoke проверяет страницу и assets наборов1/2.
|
||||||
На хостинге ничего не удаляется: файлы, убранные из репозитория, удалять вручную.
|
Откат — отдельный согласованный revert в staging, который обновляет только stage.
|
||||||
|
|
||||||
**Проверка после заливки.** На странице цели должен быть адрес загрузки на сайте, а не на самом преленде (иначе нет
|
## Android: фото и файлы (2026-10-19)
|
||||||
`config.json` с `host`), и переход на `claim`. Иначе прогон этой цели падает.
|
Основная кнопка выбирает 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/*`) с хостинга; вернуть их той же
|
Повторная проверка выпуска: php bin/release-assets.php --stage-requisites https://zoomstage.online проверяет именно runtime config; успешная загрузка requisites.html сама по себе недостаточна. Все обязательные SFTP-команды выполняются через batch -b и прекращают выкат при ошибке put.
|
||||||
загрузкой. Новый преленд требует на сайте `/api/images/public/claim` (api с 2026-10-05): со старым api переход
|
|
||||||
по ключу отдаст 404.
|
## Доменный бренд и минимальный подвал (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); }
|
||||||