S3 Next — автономный runtime и эксплуатационный runbook
Статус: ACTIVE с 2026-07-13
Ветка: next/spec-platform
Runtime root: /opt/gwptd-analytics/repo на S3
Публичный URL: https://next.mp.hyp.ru/new/test
1. Граница автономности
S3 самостоятельно содержит и запускает:
- universal SpecCollector и все его расписания;
- локальные intake, monitoring, kernel и reference schema;
- kernel ETL, freshness, reconciliation и invariant gates;
- Data API и новый Filament frontend;
- публичный Nginx, TLS и автоматическое продление сертификата;
- self-hosted GitHub Actions runner и deploy ветки Next;
mcp-gwptd, Qdrant code index и 15-минутный incremental reindex.
S2 не является частью runtime или deploy path Next. На S2 остаются только
production-система mp.hyp.ru и ссылка «Тест», ведущая на публичный домен S3.
Остановка S2 не должна останавливать сбор, расчёты, API, интерфейс или deploy S3.
2. Топология
| Компонент | Размещение | Привязка |
|---|---|---|
| Public ingress | host Nginx S3 | 80/443, next.mp.hyp.ru |
| Next frontend | gwptd-app-new | 10.0.1.7:8009 → 8000 |
| Data API | gwptd-data-api | 127.0.0.1:8010 → 8088 |
| MySQL | gwptd-v3-mysql | host 3406, закрыт UFW снаружи |
| PostgreSQL | gwptd-v3-postgres | host 5532, закрыт UFW снаружи |
| Object storage | gwptd-v3-minio | 9100/9101, закрыт UFW снаружи |
| GitHub deploy | runner s3-next | systemd service на S3 |
| Code search | mcp-gwptd + Qdrant | ветка next/spec-platform, без S2 |
DNS-канон:
A next.mp.hyp.ru → 46.62.231.51;AAAA next.mp.hyp.ru → 2a01:4f9:c014:e61a::1;- authoritative DNS: Hetzner zone
hyp.ru, TTL 300.
Nginx-конфигурация версионируется в ops/nginx/s3-next.conf и при каждом
успешном deploy устанавливается в /etc/nginx/sites-available/gwptd-next.
Первичный ACME bootstrap описан в ops/nginx/s3-next-bootstrap.conf.
3. Спеки и runtime pointers
Целевой versioned bundle следующего deploy:
- все versioned ProductSpec из
specs/products/; - все ProductSpec, ReportSpec и PageSpec имеют lifecycle
active; - точный набор collector pointer, равный ProductSpec bundle, со значением
next; pointers новых spec-onlygoldapple.snapshot, восьми Amazon ProductSpec (FBA, три Reports group и четыре direct API) bootstrap'ятся additive S3-only миграциями до exact sync; - 39
report_codeв 32 ReportSpec-файлах; - все versioned PageSpec из
specs/pages/; - 3 кабинетных источника Lamoda (
documents,cabinet_prices,funnel) работают через общий CollectionRun/raw/lease/freshness runtime; lamoda.status_datesработает intake-only и больше не изменяет legacy- таблицы старого интерфейса.
Проверка деклараций:
cd /opt/gwptd-analytics/repo
marketplace-collector-v3/venv/bin/python3 \
marketplace-collector-v3/next_collector.py --list
marketplace-collector-v3/venv/bin/python3 \
marketplace-collector-v3/scripts/audit_spec_collector_matrix.py --runtime
next_collector.py не импортирует legacy dispatcher и endpoint-specific
классы; эти файлы физически удалены из Next-ветки. Live-запуск дополнительно требует V3_SPEC_FAIL_CLOSED=1 и exact
совпадение pointer-set со всеми 79 active ProductSpec; fallback отсутствует.
4. Секреты
Единственный operational store — root-owned файл:
/etc/gwptd/secrets/runtime.env
Требования: владелец root:root, режим 0600; значения нельзя выводить в
логи, Git, документацию и аргументы процессов. Deploy проверяет только наличие
обязательных ключей и завершает работу до установки cron, если ключ отсутствует.
Группы ключей: collector/ETL/reference MySQL users; marketplace API auth;
Lamoda cabinet auth; Golden Apple GA_USER/GA_PASS/GA_PROXY; Amazon LWA
AMAZON_LWA_CLIENT_ID/AMAZON_LWA_CLIENT_SECRET/AMAZON_LWA_REFRESH_TOKEN
и обязательный comma-separated account scope AMAZON_MARKETPLACE_IDS;
1C/GWPTD reference sources; S20 PostgreSQL auth.
Все active wrappers принудительно задают локальный MySQL
127.0.0.1:3406 и очищают V3_LEGACY_*. Reference jobs используют отдельного
ограниченного пользователя gwptd_reference.
Deploy создаёт два не содержащих секретов root-owned state:
/etc/gwptd/state/v3-etl-db-attestation.env и
/etc/gwptd/state/v3-monitor-db-attestation.env (root:root, 0644, parent
0755). В них фиксируются exact CURRENT_USER и @@server_uuid, уже прошедшие
полный privilege audit. run_kernel_etl.sh, owner-only rating override и
run_monitor.sh fail-closed при отсутствии/подмене своего state и сверяют эту
пару на той же MySQL connection до первого рабочего запроса, включая каждое
восстановление pooled connection.
Monitor не читает общий runtime secret store. Его единственный secret input —
preprovisioned /etc/gwptd/secrets/monitor.env (root:root 0600) с
V3_MONITOR_PASSWORD, Telegram token и хотя бы одним chat id. Неизвестные или
ETL/marketplace keys запрещены. gwptd_monitor@% имеет table-level SELECT
только на relations четырёх monitor jobs и INSERT только в два comparator
sinks; exact envelope повторно проверяется после pooled reconnect.
5. Расписание
Канонический файл:
marketplace-collector-v3/scripts/crontab.s3-next.txt
Он устанавливается deploy-скриптом только после preflight, build и health checks. В расписании есть:
- spec-сбор 1C, WB, Ozon, YM, Lamoda, Golden Apple и Amazon Reports API;
- архивный
wb.stocksотсутствует в cron/freshness; каноническийwb.warehouse_stocksвходит в утренний WB-пакет; - три cabinet endpoint Lamoda;
build_all, freshness, reconciliation и shadow watch;- ежедневный read-only comparator закрытых фактов S2 Canon ↔ S3 Next;
- локальные reference sync 1C/GWPTD и внешние read-only S20 sources.
Суточное окно UTC: Lamoda cabinet 02:00–02:40, справочники 03:00–04:30,
WB 05:00, Ozon 06:00, YM 07:00, Lamoda 08:00, Golden Apple snapshot
каждые два часа в :20 (включая 08:20 до ETL), kernel ETL 09:15, прямой
Amazon SP-API работает в утверждённом малом режиме: FBA inventory ежедневно в
11:00, orders и Sales & Traffic ежедневно в 11:20, Finances по воскресеньям
в 12:00, settlements по воскресеньям в 13:00, monthly Reports API первого
числа в 17:00. Catalog ranks, Pricing buybox и широкая weekly Reports group
не входят в cron и исключены из freshness.
YM в 07:00 запускается только через run_yandex_daily.sh. Wrapper держит
shared codebase-lock и один exclusive marketplace-lock
/opt/mcp-gwptd/logs/gwptd-v3-ym.collector.lock на всём интервале двух
child-процессов. Lock создаётся только в уже hardened каталоге
/opt/mcp-gwptd/logs (root:gwptd, exact mode 1770): helper проверяет real
parent и его владельца, сохраняет существующий regular inode, приводит сам
lock к owner=EUID, group=parent group и exact mode 0660, затем повторяет
FD↔path inode/owner/group/mode attestation до и после flock. Symlink,
hardlink (nlink != 1) и path/inode swap отклоняются; bounded wait принимается
только как каноническое десятичное число 0..21600 без octal/overflow форм.
Сначала выполняются все основные ym.* с исключением
ym.competitors_position, затем отдельно quota-heavy отчёт конкурентной
позиции. Поэтому его HTTP 420/долгое ожидание или остановка только child не
оставляют цены, остатки, заказы, финансы и остальные основные YM-данные
необновлёнными. Оба return code сохраняются, второй этап запускается и после
ошибки первого, итоговый cron status остаётся non-zero при любой ошибке. Nested
run_collector.sh принимает lock только по exact protocol/scope/path, повторно
сверяет regular inode и FD и не создаёт окна для параллельного ручного YM-run;
ожидание ограничено шестью часами.
Корень alert 2026-07-15 03:45 UTC зафиксирован отдельно от симптома:
предыдущий daily YM дошёл до ym.competitors_position, получил повторные HTTP
420 и был остановлен как устаревший quota-burning run; оставшиеся 15 основных
endpoint'ов после остановки процесса уже не исполнялись. Отдельный canary этого
endpoint затем завершился успешно, то есть причиной stale был порядок общей
цепочки, а не cron, lease или потеря credentials. Golden Apple из того же alert
сам восстановился следующим штатным двухчасовым snapshot. Кодовый контракт
двухстадийного YM и его DB-free тесты готовы; live-gate закрывается только после
deploy и наблюдения следующего полного scheduled цикла без ручного запуска.
Daily Sales & Traffic запускается только для рынков, где за последние 180 дней
были заказы; первого числа месяца выполняется полный discovery всех настроенных
рынков. Между createReport соблюдается отдельный интервал 65 секунд. Поэтому
аккаунт с 13 EU-рынками не выжигает Reports API на нулевых рынках, но новый
рынок автоматически обнаруживается без ручной правки конфигурации. Валидный
пустой orders/Sales & Traffic report считается success, а отказ любой стадии
create/poll/document/download по-прежнему даёт partial.
Оно не пересекается с действующим утренним S2 и поэтому не конкурирует за
общие marketplace-токены. Архивный wb.stocks запускается только вручную при
необходимости аудита старого Statistics API и не участвует в freshness.
Amazon-контур сохраняет восемь versioned ProductSpec и 20 возможных intake sink,
но малый режим регулярно обновляет только 11 sink: FBA inventory; orders;
дневной и ASIN-срез Sales & Traffic; пять monthly; Finances v0 и settlement
getReports. Все запланированные источники используют общий LWA transport,
Amazon host flock, CollectionRun, raw envelope, DB lease и freshness.
amazon.ads и amazon.vat
не маскируются успешными коллекторами; amazon.sqp также остаётся выключенным,
пока не подтверждены Brand Analytics access и single-ASIN scope для
reseller-каталога. Это явные EXTERNAL-BLOCKED placeholders до выдачи доступов
и подтверждения контракта.
Активное расписание намеренно не содержит refresh_s2_*, legacy mirror,
cutover reconciliation и запись в production S2. В 14:30 UTC, после дневных
ETL обоих контуров, S3 передаёт один и тот же versioned exporter на S2 по SSH,
получает только агрегаты окна D-8..D-2 и сравнивает orders, returns,
finance, stocks, analytics, turnover, rating, prices и storage.
Comparator не запускает API/collector, не передаёт raw payload и не пишет на S2;
результат сохраняется локально в
gwptd_monitoring.canon_next_comparison_log и отправляется мониторингом. Перед
чтением плана и exporter фиксируется чистый S3 SHA; точные байты exporter
читаются один раз, передаются обоим контурам через stdin, а их SHA-256 вместе с
SHA обоих checkout сохраняется в каждой строке evidence. Дрейф S3 checkout
после любого capture блокирует запись результата.
6. Deploy
Workflow .github/workflows/deploy-s3-next.yml исполняется на runner
s3-next непосредственно на S3. Runner работает под отдельным пользователем
gwptddeploy; sudo разрешает только root-owned entrypoint:
/usr/local/sbin/gwptd-next-deploy
Entrypoint до чтения или изменения checkout берёт три эксклюзивные блокировки
в фиксированном порядке: legacy docs lock /tmp/gwptd-next-deploy.lock на FD7,
host-wide /run/lock/gwptd-next-deploy.lock на FD8 и затем
/opt/mcp-gwptd/logs/codebase-reindex.lock на FD9. Host lock передаётся как
GWPTD_NEXT_DEPLOY_HOST_LOCK_PROTOCOL=gwptd-next-host-deploy-lock-v1, а
codebase lock — через отдельный версионированный протокол. Legacy lock остаётся
на время перехода старых docs publishers. /opt/mcp-gwptd фиксируется как
root:root 0755, его logs — root:gwptd 1770; symlink/non-regular lock
отклоняется, существующий regular inode при hardening не заменяется. Стандартный
Ubuntu parent /run/lock принимается только как точный root:root 01777:
sticky bit защищает root-owned lock от подмены, а сам lock обязан иметь один
hardlink. Обычный 0777, иной owner/group или writable group отклоняются. Затем
entrypoint проверяет ветку и чистое дерево, выполняет только fast-forward и запускает
scripts/deploy/deploy-s3-next.sh. Внутренний скрипт до любого изменения
сверяет inode FD8 и FD9 и независимыми shared probes доказывает, что оба
унаследованных descriptor уже владели эксклюзивными блокировками. После
отрицательного независимого probe verifier повторно утверждает LOCK_EX
именно на унаследованном open-file description и ещё раз проверяет конфликт;
открытый, но не владевший lock FD поэтому не может приобрести недостающую
блокировку незаметно. Сразу после этих двух verifier-ов inner deploy вызывает
runtime_data_fence.py provision --wait 0 --codebase-fd 9: оба fixed data
fence inode создаются либо повторно аттестуются под уже доказанным exclusive
FD9 до MCP barrier, journal, schema, unit, cron или runtime mutation. Reopen
codebase lock запрещён. Оба docs publisher используют тот же /run host lock.
Прямой запуск внутреннего скрипта завершается fail-closed.
Обычный полный deploy допускается только в установленное окно. При явном распоряжении владельца допускается отдельный вызов root-owned entrypoint:
/usr/local/sbin/gwptd-next-deploy \
--owner-authorized-off-window --expected-sha <40-hex-commit>
Он пропускает только проверку времени. Exact SHA, все три блокировки, transaction/recovery protocol, quarantine cron и проверки activation остаются обязательными. Workflow этот аргумент не передаёт; он предназначен только для осознанного внепланового запуска.
До fast-forward entrypoint атомарно сохраняет exact old/new SHA и root crontab,
удаляет root crontab и доказывает его пустоту в root-owned deploy-транзакции
/etc/gwptd/state/s3-next-deploy-transaction.env. Наличие этого файла после
получения shared codebase-lock блокирует любой runtime wrapper. При любой
ошибке inner deploy entrypoint возвращает branch на old SHA только если замена
images не начиналась либо inner записал durable image_rollback_proven: exact
старые image IDs снова назначены обоим тегам, оба контейнера используют именно
эти IDs и оба healthy. Sentinel удаляется только после доказательства exact
clean checkout. После reset на old SHA entrypoint повторно создаёт оба
контейнера уже через old docker-compose.s3-next.yml и его old bind-mounted
docker/supervisord-next.conf, затем ещё раз сверяет exact old image IDs и
health обоих контейнеров. Только после этого побайтово восстанавливается
crontab. Если image отсутствует,
контейнер unhealthy либо любое другое доказательство не прошло, пишется
image_rollback_failed, новый checkout сохраняется, а
cron остаётся выключенным, backup и sentinel сохраняются для ручного recovery.
После успешной активации app/api и control plane новый SHA считается commit
boundary: сбой только при очистке sentinel уже не откатывает код отдельно от
новых images, а оставляет runtime заблокированным. Одноразовый bootstrap не
выполняет полный deploy старым wrapper. Сначала отдельный commit добавляет
новый ops/bin/gwptd-next-deploy и
scripts/deploy/bootstrap_s3_deploy_wrapper.py. Переходный commit содержит
tracked marker ops/S3-NEXT-WRAPPER-BOOTSTRAP с точным содержимым
gwptd-next-wrapper-bootstrap-v1 и ранний exec helper'а из прежнего inner:
helper наследует уже удерживаемый FD9, атомарно заменяет только
/usr/local/sbin/gwptd-next-deploy, fsync'ит файл и каталог и не меняет Git,
cron, БД, containers или units. Повторный вызов на marker-SHA разрешён только
если marker корректен, а установленный wrapper имеет root:root 0755 и
побайтово совпадает с tracked source. Следующий полный commit удаляет marker;
workflow передаёт --expected-sha ${{ github.sha }}, а wrapper до fast-forward
требует точного совпадения fetched branch SHA с проверенным CI commit. В final
inner нет ORIG_HEAD/reflog и нет legacy/direct fallback.
До первой миграции, способной вставить ProductSpec pointer, deploy атомарно
создаёт root-owned bounded journal
/etc/gwptd/state/s3-next-control-plane-journal.json (0600). Он содержит
exact CURRENT_USER()/@@server_uuid, полный набор (не более 256) строк
collector_runtime_pointer с target, digest и updated_at, полный
column/default/extra/index contract pointer и append-only audit relations,
audit high-water mark, old/new SHA, root crontab, малый allowlist файлов и
стабильные enabled/active states systemd units. В allowlist входит также
v3-etl-db-attestation.env. Snapshot завершается до Golden Apple/Amazon
pointer bootstrap и до ProductSpec digest sync.
При любой штатной ошибке pointer relation возвращается к exact snapshot в
одной serializable transaction и повторно доказывается отдельной post-commit
read-only connection. registry_pointer_log не очищается: события неудачного
deploy сохраняются, а каждому реально восстановленному pointer добавляется
ровно одна детерминированная compensation-запись
deploy-rollback:<new_sha> с old/new SHA. Restore retry-idempotent. Затем
побайтово восстанавливаются файлы, systemd states и crontab. Только доказанный
полный restore удаляет journal. Если доказательство не прошло, сохраняются
journal, /etc/gwptd/state/s3-next-control-plane-failure.env и основной deploy
sentinel, а cron остаётся выключенным; эти файлы нельзя удалять вручную до
разбора и доказанного recovery.
Узкий break-glass путь для фазы control_plane_rollback_failed, возникшей до
первой DB/pointer-мутации, — только versioned
scripts/deploy/recover-s3-next-preimage.sh. Он не выполняет SQL/DML restore:
под теми же тремя exclusive locks повторно доказывает journal old/new SHA,
полную pointer relation с исходными updated_at, отсутствие audit-строк после
high-water, schema/identity БД, control-plane files/units, пустой cron и точные
остановленные app/API container+image IDs. Recovery-код и journal helper
передаются как отдельные root-owned файлы с обязательными SHA-256; эти digests
и все runtime IDs закрепляются в durable prepared marker. Только после этого
разрешены reset на old SHA, запуск тех же контейнеров, health/HTTP proofs и
побайтовое восстановление cron. preimage_recovery_commit_ready становится
resumable commit receipt лишь после всех доказательств; original failure marker
удаляется последним. Неправильный SHA/ID, чужая DB/audit-мутация, иной runtime
или torn receipt оставляют S3 fail-closed и не разрешают догадки/частичный
cleanup. Этот путь требует отдельного явного разрешения владельца на root-side
recovery и не является способом ручного запуска collectors.
Текущая версия break-glass намеренно одноразовая и принимает только incident
pair ca38823c2283116f2e5e59ba2f7178f85c9f3e1a → f7d08cdbb18519ca9b74207b59c86b935868adb1. Она также фиксирует production
checkout /opt/gwptd-analytics/repo, локальный Docker context, безопасный
системный PATH и exact root:root 0755 для /etc/gwptd и
/etc/gwptd/state; ambient Git/Docker/Python/Compose overrides очищаются.
Поскольку production checkout и .git исторически group-writable, bundle
также фиксирует device/inode/owner/group/mode для checkout, .git, config и
index, SHA-256 config/index и отсутствие alternate Git metadata. Все Git-команды
идут с явными --git-dir/--work-tree, отключёнными hooks/fsmonitor и
GIT_OPTIONAL_LOCKS=0: read-only proof не может незаметно обновить index.
Первая попытка повторяет exact index proof непосредственно перед reset; только
retry с уже durable prepared receipt допускает новый index inode после
оборванного предыдущего reset, сохраняя exact owner/mode/device/config proof.
Другую пару SHA этим bundle восстанавливать запрещено: совместимость DB
after-image с другим old code отдельно не доказана.
Recovery-файлы нельзя исполнять из checkout: git reset --hard является
частью восстановления и удалит либо заменит код под выполняющимся процессом.
После final tests их SHA-256 вычисляются на доверенной рабочей станции до
копирования. Digests загруженной S3-копии затем сравниваются с независимо
удерживаемыми локальными значениями; вычислить «expected» из самой remote-копии
недостаточно — это было бы тавтологией. Только после этого файлы устанавливаются
во внешний root-only bundle с exact modes:
HELPER_SHA=$(shasum -a 256 scripts/deploy/deploy_control_plane_journal.py | awk '{print $1}')
RECOVERY_SHA=$(shasum -a 256 scripts/deploy/recover-s3-next-preimage.sh | awk '{print $1}')
printf 'helper=%s\nrecovery=%s\n' "$HELPER_SHA" "$RECOVERY_SHA"
ssh s3 'install -d -o root -g root -m 0700 /root/gwptd-s3-preimage-recovery-v1'
scp scripts/deploy/recover-s3-next-preimage.sh \
s3:/root/gwptd-s3-preimage-recovery-v1/recover-s3-next-preimage.sh
scp scripts/deploy/deploy_control_plane_journal.py \
s3:/root/gwptd-s3-preimage-recovery-v1/deploy_control_plane_journal.py
ssh s3 'chown root:root \
/root/gwptd-s3-preimage-recovery-v1/recover-s3-next-preimage.sh \
/root/gwptd-s3-preimage-recovery-v1/deploy_control_plane_journal.py && \
chmod 0755 /root/gwptd-s3-preimage-recovery-v1/recover-s3-next-preimage.sh && \
chmod 0644 /root/gwptd-s3-preimage-recovery-v1/deploy_control_plane_journal.py'
ssh s3 \
"EXPECTED_HELPER_SHA=$HELPER_SHA" \
"EXPECTED_RECOVERY_SHA=$RECOVERY_SHA" \
'bash -s' <<'REMOTE'
set -eu
B=/root/gwptd-s3-preimage-recovery-v1
test "$(sha256sum "$B/deploy_control_plane_journal.py" | awk '{print $1}')" \
= "$EXPECTED_HELPER_SHA"
test "$(sha256sum "$B/recover-s3-next-preimage.sh" | awk '{print $1}')" \
= "$EXPECTED_RECOVERY_SHA"
test "$(stat -c '%U:%G:%a:%h' "$B/deploy_control_plane_journal.py")" \
= 'root:root:644:1'
test "$(stat -c '%U:%G:%a:%h' "$B/recover-s3-next-preimage.sh")" \
= 'root:root:755:1'
REMOTE
Для зафиксированного F-60 exact runtime identity равна:
app container 147344ba7ee86941886000d94df0c0424d73d996f3357d1d67678ff0099bcdb8
app image sha256:6eed70b6f0424cdd07874cee19583cda11526f1eb917cb9dc9ee54235289a1cc
api container fd32140e7ea708ebd2f00781d02bf724e17ceb08e467328853b086985edca383
api image sha256:e6b4966165ec767d9217e5aed65bc3c67e71f44558e04fa112b6b90e8fc4b7b8
После staging оператор передаёт независимо вычисленные local digests и все идентификаторы явно. Recovery повторно сверяет remote bytes непосредственно перед изменением state. Команда ниже допустима только после отдельного разрешения владельца на root-side recovery:
ssh s3 \
"EXPECTED_HELPER_SHA=$HELPER_SHA" \
"EXPECTED_RECOVERY_SHA=$RECOVERY_SHA" \
'bash -s' <<'REMOTE'
set -eu
B=/root/gwptd-s3-preimage-recovery-v1
exec "$B/recover-s3-next-preimage.sh" \
--expected-old-sha ca38823c2283116f2e5e59ba2f7178f85c9f3e1a \
--expected-new-sha f7d08cdbb18519ca9b74207b59c86b935868adb1 \
--expected-app-container-id 147344ba7ee86941886000d94df0c0424d73d996f3357d1d67678ff0099bcdb8 \
--expected-app-image-id sha256:6eed70b6f0424cdd07874cee19583cda11526f1eb917cb9dc9ee54235289a1cc \
--expected-api-container-id fd32140e7ea708ebd2f00781d02bf724e17ceb08e467328853b086985edca383 \
--expected-api-image-id sha256:e6b4966165ec767d9217e5aed65bc3c67e71f44558e04fa112b6b90e8fc4b7b8 \
--expected-helper-sha256 "$EXPECTED_HELPER_SHA" \
--expected-recovery-sha256 "$EXPECTED_RECOVERY_SHA"
REMOTE
После успешного reset на ca38823 следующий штатный CI deploy не требует
ручного external bridge. Новый inner разрешает ровно один переход от exact
установленного ca-wrapper (SHA-256
453048371f8dad780da581dd2a1514ad5056cb88ff2327325dcacda996530dc2):
он сверяет семистрочный legacy sentinel, старые healthy containers и оба live
image tag, атомарно добавляет четыре runtime ID и дальше использует обычный
transaction protocol. Любой иной old SHA, wrapper, state или runtime
отклоняется до schema/build mutation.
Build app/API выполняется только в SHA-specific tags
gwptd-app-new:candidate-<new_sha> и
gwptd-data-api:candidate-<new_sha> через compose overrides. Поэтому ошибка или
SIGKILL до replacement не меняет live gwptd-*:next; inner повторно доказывает
оба старых tag и running container ID перед тем, как разрешить rollback
checkout/cron. Candidate IDs назначаются live :next только после durable
image_replacement_active и установки image rollback trap. Все normal и
rollback compose up принудительно используют exact gwptd-*:next, а
унаследованные image overrides запрещены outer и inner entrypoint.
Обычный deploy не выполняет крупный bootstrap: pinned OpenClaw,
пользователь/группа/каталоги Boris, точное узкое UFW-правило и docs venv должны
существовать заранее. Режим Boris --require-preprovisioned запрещает их
создание внутри транзакции; изменяемые config/unit файлы и unit states входят
в journal.
Остальные deploy-проверки:
- branch, secret key names, весь active ProductSpec bundle, active ReportSpec/PageSpec;
- CI-тесты collector, Data API, PageSpec, business access и темы;
- Data API import/DB connectivity, миграцию run trace и additive
ui_preferencesmigration до замены live-контейнеров; - production-mode frontend, PageSpec-primary и отсутствие legacy scheduler;
- immutable image build, health обоих containers и отсутствие legacy network;
- отсутствие MySQL triggers на
gwptd_kernel.fact_ratingи append-onlyrating_history_override_log/canon_next_comparison_evidence_v3до выдачи их INSERT grants и публикации DB attestation; - Nginx/TLS smoke, синхронизацию
mcp-gwptd, docs timers, установку нового entrypoint и только затем cron install; app/api image rollback остаётся активным до успешного завершения всей этой control-plane фазы, а bounded control-plane journal — до её commit boundary.
Разовые backfill не входят в active image/cron. Они находятся в
marketplace-collector-v3/archive/maintenance/, собираются отдельным
Dockerfile.kernel-backfill и требуют явного
GWPTD_MAINTENANCE_CONFIRM=YES.
MCP runtime-файлы устанавливает versioned script
scripts/deploy/install-s3-mcp-gwptd.sh. repo_status, ручной sync и
инкрементальный timer используют GWPTD_REPO_BRANCH=next/spec-platform.
Автоматический reindex запускается с --no-pull: он не меняет worktree и не
обращается к S2, а индексирует только уже развёрнутый GitHub runner SHA.
6.1 Исторический P3.7 root seed deadline-dispatcher
Статус: root seed и normal deadline bootstrap уже read-only attested на S3
2026-07-18; повторять seed не нужно и нельзя без отдельного incident review.
Старый live wrapper не умеет вызвать новый deadline-dispatcher. Его нельзя
исправить расширением sudoers: пользователь gwptddeploy по-прежнему имеет
только один разрешённый root entrypoint
/usr/local/sbin/gwptd-next-deploy. В частности, запрещены
sudo /usr/bin/python3, любой путь из $GITHUB_WORKSPACE и изменение sudoers.
Этот раздел сохраняет историческую recovery procedure. Если бы seed не был
attested, его выполнял бы только root в допустимом full-deploy окне:
12:30–22:00 UTC, в
воскресенье после 14:30, за исключением 16:30–18:30 UTC первого числа.
Окно по часам с 06.08 вторично. Обёртка сначала спрашивает
gwptd-collection-quiet-check: при молчащем сборе выкладка проходит вне этих часов. Ограничение осталось только на ночной цикл02:00–09:15 UTC— и по другой причине, не связанной с расписанием. Единое описание — карта ограничений. До него exact SHA должен быть опубликован как текущая головаorigin/next/spec-platformи пройти CI. Первый workflow на этот SHA до seed-а может штатно завершиться fail-closed на неизвестном аргументе старого wrapper; он не изменяет checkout, cron, БД, containers или расписание и не считается deploy attempt.
Root operator создаёт временный root-owned bare Git repository, использует
только fixed origin git@github.com:antongsm/gwptd-analytics.git, fixed branch
next/spec-platform, /usr/bin/git --no-replace-objects, disabled hooks and
fixed SSH command/key/known_hosts. Из exact fetched Git blob, не из live
checkout и не из runner workspace, запускаются последовательно:
scripts/deploy/provision_s3_deadline_bootstrap_dispatcher.py \
--check --candidate-sha <exact-40-lowercase-sha>
scripts/deploy/provision_s3_deadline_bootstrap_dispatcher.py \
--candidate-sha <exact-40-lowercase-sha>
Первый вызов read-only: проверяет current remote SHA и окно, но не меняет refs
live checkout, locks или root targets. Второй под canonical lock-chain ещё раз
fetch-ит и привязывает SHA, доказывает fast-forward от live SHA и окно уже
после ожидания locks. Только затем он пишет durable seed stage, атомарно
ставит /usr/local/libexec/gwptd/deploy-deadline-bootstrap и только после него
/usr/local/sbin/gwptd-next-deploy; оба файла должны быть root:root, 0755,
один hardlink и совпадать с digest receipt. Commit seed-а —
/etc/gwptd/state/s3-next-deadline-bootstrap-dispatcher-provision.env; stage:
/etc/gwptd/state/s3-next-deadline-bootstrap-dispatcher-stage.env.
Сразу после seed-а root фиксирует только метаданные и проверки, без вывода секретов:
stat -c '%U:%G %a %h %n' \
/usr/local/libexec/gwptd/deploy-deadline-bootstrap \
/usr/local/sbin/gwptd-next-deploy \
/etc/gwptd/state/s3-next-deadline-bootstrap-dispatcher-provision.env
sha256sum /usr/local/libexec/gwptd/deploy-deadline-bootstrap \
/usr/local/sbin/gwptd-next-deploy
test ! -e /etc/gwptd/state/s3-next-deadline-bootstrap-dispatcher-stage.env
test ! -L /etc/gwptd/state/s3-next-deadline-bootstrap-dispatcher-stage.env
sudo -l -U gwptddeploy
Receipt должен содержать exact candidate SHA, predecessor live SHA и оба
SHA-256. Затем запускается workflow повторно для того же SHA. Новый outer
передаёт runner only-SHA boundary fixed dispatcher-у через env -i; до первого
canonical normal-bootstrap receipt он требует valid seed receipt и отсутствие
seed stage. После появления
/etc/gwptd/state/s3-next-deploy-deadline-bootstrap.env именно этот normal
receipt становится authority для следующих deploy; старый seed receipt их не
блокирует.
Если процесс оборвался и seed stage существует, нельзя вручную удалять stage, receipt или root targets и нельзя пробовать другой SHA. Root повторяет второй вызов provisioner-а из exact того же Git blob в следующем допустимом окне: он сверит predecessor/candidate/digests, повторно установит полный bundle и атомарно завершит receipt. Любое несовпадение остаётся fail-closed и требует отдельного incident review.
7. Быстрая проверка здоровья
ssh s3-int 'docker ps --format "{{.Names}} {{.Status}}" | grep gwptd'
ssh s3-int 'systemctl is-active nginx certbot.timer \
actions.runner.antongsm-gwptd-analytics.s3-next.service \
mcp-gwptd.service mcp-reindex-auto.timer'
curl -fsS -o /dev/null -w '%{http_code} %{remote_ip}\n' \
https://next.mp.hyp.ru/new/login
После завершения естественных слотов reference 04:30 UTC и kernel 09:15 UTC P1B-3 проверяется только read-only verifier-ом:
ssh s3-int 'cd /opt/gwptd-analytics/repo/marketplace-collector-v3 && \
PYTHONDONTWRITEBYTECODE=1 ./venv/bin/python3 -B \
-m monitoring.p1b3_natural_cycle --slot-date YYYY-MM-DD'
Не запускайте вручную build_all, validate_invariants или collector для
получения P1B-3 evidence: такой запуск не считается естественным и может
сделать весь UTC-день неприемлемым. Исключение возможно только для отдельно
согласованного тестового полигона вне доказательного цикла; перед ним нужно
проверить process, DB lease и последний collection_run.
8. TLS
Certificate lineage находится в
/etc/letsencrypt/live/next.mp.hyp.ru, renewal webroot —
/var/www/html/public_html. certbot.timer активен на S3. Реальный выпуск
после DNS cutover успешно выполнен 2026-07-13; Nginx перезагружен на новый
сертификат. Deploy hook gwptd-next-reload-nginx проверяет конфигурацию и
перезагружает Nginx после каждого будущего renewal. Renewal lineage S2
отключён, но старый сертификат сохранён для rollback.
9. Disaster recovery и внешний backup
Исторический внешний снимок, прошедший полный restore drill:
/Volumes/2TB-64/NEW_MP/ARCHIVE/BACKUPS/s3-next-20260713T1603Z
Снимок содержит четыре прикладные MySQL schema, весь PostgreSQL cluster,
MinIO volume, шесть OCI images, Git bundle, host config и зашифрованные age
секреты/TLS/runner identity. Только secrets/s3-runtime-secrets.tar.age
зашифрован на уровне архива. Database, MinIO, Qdrant, Git, OCI images, runtime,
config и recovery-tools остаются обычными либо zstd-сжатыми файлами; шифрование
внешнего носителя/ExFAT этим workflow не аттестуется. Exact scope фиксируется в
config/encryption-scope.json. Значения секретов не выводятся и не сохраняются
в открытом виде. Проверка включает SHA-256, целостность zstd, Git bundle,
расшифровку+листинг secret tar и полное восстановление MySQL/PostgreSQL/MinIO/OCI.
Канонические команды с доверенного Mac:
scripts/ops/backup-s3-next-offhost.sh /Volumes/2TB-64/NEW_MP/ARCHIVE/BACKUPS/s3-next-<UTC>
scripts/ops/verify-s3-next-backup.sh /path/to/backup
scripts/ops/drill-s3-next-backup.sh /path/to/backup
Restore drill использует только disposable Docker volumes и --network none.
Production volumes не монтируются. Перед docker load сценарий запоминает все
исходные image IDs и в любом исходе возвращает production-теги; это отдельно
проверено mini-drill. Контрольный полный drill 2026-07-13 завершился OK, а
временные контейнеры и volumes были удалены. Этот исторический запуск оставил
только terminal OK, но ещё не создавал durable drill receipt и поэтому не
закрывает новый receipt-gate. Новый drill считается доказанным только при
наличии sibling-файла DRILL-RECEIPTS/<backup>-<run>.json, созданного после
строгого cleanup и доказанного освобождения host lock.
Новый backup держит один exclusive codebase lock непрерывно от Qdrant snapshot
до завершения MySQL/PostgreSQL/MinIO/config streams. MySQL inventory и dump
снимаются под одним bounded FLUSH TABLES WITH READ LOCK; inventory фиксирует
exact schema/table/view identity и row counts всех четырёх прикладных БД, а
drill сравнивает восстановленный inventory целиком. Перед записью durable
receipt финализатор повторно проверяет каждый путь и digest из SHA256SUMS,
полный file-set архива и сохраняет полный восстановленный MCP index receipt.
10. Откат
Порядок отката без изменения production S2:
- штатная ошибка deploy автоматически возвращает оба предыдущих immutable app/api images, exact old Git SHA и сохранённый root crontab под общим exclusive lock; при недоказанном rollback sentinel блокирует runtime, а root cron остаётся quarantined до ручного восстановления;
- для ручного отката кода — вернуть предыдущий
gwptd-app-new:rollback-*иgwptd-data-api:rollback-*либо fast-forward исправляющий commit; - для collector — изменить только pointer конкретного endpoint с записью в audit, не откатывать весь флот;
- для публичного входа — временно вернуть A/AAAA
next.mp.hyp.ruна S2 и включить сохранённый S2 renewal lineage; - не удалять локальные S3 базы: сравнение и forensic должны оставаться доступными.
Внешний baseline старого S3 runtime:
/Users/antonnozdrin/Dropbox/_____0_GWPTD-IT/mp/archives/s3-next-baseline-20260713T0805Z
11. Открытые эксплуатационные гейты
- root seed и normal bootstrap уже attested; не повторять их как routine gate.
Source-ready
p37_installed_helper_canary.pyпроверяет только synthetic digest-bound helper paths и не является live proof. Отдельный owner-approved live proof всё ещё должен документировать outer TERM/SIGKILL -> PDEATHSIG, no-reset unproven state, bounded MCP transient retry и canonical lock contention без запуска collector/ETL; - накопить burn-in нового суточного расписания;
- накопить 7–14 дней результатов автоматического сравнения фактов S2 Canon ↔ S3 Next без запуска дополнительных collectors;
- подтвердить допустимые расхождения business formulas;
- только после этого отдельно решать вопрос production cutover S2.
12. Приёмочный baseline 2026-07-13
Live CollectionRun:
| run_id | endpoint | status | received/parsed/skipped | HTTP 429 |
|---|---|---|---|---|
| 2401 | lamoda.documents | success | 19 / 19 / 0 | 0 |
| 2402 | lamoda.cabinet_prices | success | 1741 / 1741 / 0 | 0 |
| 2405 | lamoda.funnel | success | 0 / 0 / 0 | 0 |
| 2406 | lamoda.status_dates | success | 359 / 359 / 0 | 0 |
| 2445 | wb.finance_reports | success | 3430 / 3430 / 0 | 0 |
| 2467 | lamoda.inventory | success | 1512 / 1512 / 0 | 0 |
| 2468 | lamoda.nomenclatures | success | 1741 / 1741 / 0 | 0 |
| 2469 | lamoda.orders | success | 712 / 712 / 0 | 0 |
| 2470 | lamoda.promotions | success | 563 / 563 / 0 | 0 |
| 2471 | lamoda.returns | success | 0 / 0 / 0 | 0 |
Нулевой lamoda.funnel допустим: пять доступных export уже были загружены.
lamoda.status_dates обработал 44 808 status items без hard cap.
После run 2406 выполнен полный build_all: все 14 шагов OK. Три hard
invariant (stocks_canon_dup, stocks_alias_split, orders_cross_mp) прошли;
gate прошёл. Остался предупреждающий soft-сигнал revenue_daily_jump (9
случаев), который не блокирует расчёт и требует отдельного бизнес-разбора.
wb.finance_reports после исправления idempotent upsert прошёл контрольный
S3-прогон без пропусков. Пять последних Lamoda endpoint, у которых до этого
последний run принадлежал старому классу, прошли живой canary через единый
SpecCollector и записали spec_digest + runtime_commit. На этой живой
контрольной точке, до добавления goldapple.snapshot, 69/70 прежних endpoint
имели последний run через SpecCollector и статус success.
Исторический wb.stocks run 2326 завершился partial с 0 строк после пяти
HTTP 429. Это не блокирует факты: endpoint архивный/manual-only, удалён из cron
и freshness, а канонический wb.warehouse_stocks собирается утренним WB-пакетом
и питает fact_stocks_daily.
| Факт | Последняя дата | Всего строк | Строк последней даты |
|---|---|---|---|
| orders | 2026-07-13 | 62 914 | 94 |
| returns | 2026-07-13 | 4 214 | 20 |
| finance | 2026-07-13 | 50 632 | 70 |
| stocks | 2026-07-13 | 1 429 759 | 12 927 |
| turnover | 2026-07-13 | 104 319 | 8 240 |
| rating | 2026-07 | 6 165 | 548 |
Deploy commit 1d387b0 успешно прошёл на self-hosted runner S3 (GitHub Actions
run 29250169106). CI выполнил 286 collector tests, 69 Data API tests и
PageSpec/access/theme PHP tests до deploy. Живой smoke 146 GET-маршрутов
/api/v1/reports/* дал 134 ответа 2xx, 12 ожидаемых 4xx для обязательных
параметров и ни одного 5xx. Публичный TLS-smoke дал HTTP 200 с адреса S3.
На работающем frontend подтверждены production mode, encrypted/secure session,
SPEC_PLATFORM_PRIMARY=true, 49 active PageSpec и отсутствие пользователей без
роли (15/15 имеют роль). Legacy Laravel scheduler в контейнере отсутствует.
mcp-gwptd обслуживает ветку next/spec-platform. Его systemd sandbox разрешает
запись только в logs/state и checkout; concurrent timer-run не перезаписывает
статус активной индексации, а background reindex переживает рестарт MCP-сервиса.