Перейти к основному содержимому

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 ingresshost Nginx S380/443, next.mp.hyp.ru
Next frontendgwptd-app-new10.0.1.7:8009 → 8000
Data APIgwptd-data-api127.0.0.1:8010 → 8088
MySQLgwptd-v3-mysqlhost 3406, закрыт UFW снаружи
PostgreSQLgwptd-v3-postgreshost 5532, закрыт UFW снаружи
Object storagegwptd-v3-minio9100/9101, закрыт UFW снаружи
GitHub deployrunner s3-nextsystemd service на S3
Code searchmcp-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-only goldapple.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, его logsroot: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-проверки:

  1. branch, secret key names, весь active ProductSpec bundle, active ReportSpec/PageSpec;
  2. CI-тесты collector, Data API, PageSpec, business access и темы;
  3. Data API import/DB connectivity, миграцию run trace и additive ui_preferences migration до замены live-контейнеров;
  4. production-mode frontend, PageSpec-primary и отсутствие legacy scheduler;
  5. immutable image build, health обоих containers и отсутствие legacy network;
  6. отсутствие MySQL triggers на gwptd_kernel.fact_rating и append-only rating_history_override_log/canon_next_comparison_evidence_v3 до выдачи их INSERT grants и публикации DB attestation;
  7. 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:

  1. штатная ошибка deploy автоматически возвращает оба предыдущих immutable app/api images, exact old Git SHA и сохранённый root crontab под общим exclusive lock; при недоказанном rollback sentinel блокирует runtime, а root cron остаётся quarantined до ручного восстановления;
  2. для ручного отката кода — вернуть предыдущий gwptd-app-new:rollback-* и gwptd-data-api:rollback-* либо fast-forward исправляющий commit;
  3. для collector — изменить только pointer конкретного endpoint с записью в audit, не откатывать весь флот;
  4. для публичного входа — временно вернуть A/AAAA next.mp.hyp.ru на S2 и включить сохранённый S2 renewal lineage;
  5. не удалять локальные 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_idendpointstatusreceived/parsed/skippedHTTP 429
2401lamoda.documentssuccess19 / 19 / 00
2402lamoda.cabinet_pricessuccess1741 / 1741 / 00
2405lamoda.funnelsuccess0 / 0 / 00
2406lamoda.status_datessuccess359 / 359 / 00
2445wb.finance_reportssuccess3430 / 3430 / 00
2467lamoda.inventorysuccess1512 / 1512 / 00
2468lamoda.nomenclaturessuccess1741 / 1741 / 00
2469lamoda.orderssuccess712 / 712 / 00
2470lamoda.promotionssuccess563 / 563 / 00
2471lamoda.returnssuccess0 / 0 / 00

Нулевой 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.

ФактПоследняя датаВсего строкСтрок последней даты
orders2026-07-1362 91494
returns2026-07-134 21420
finance2026-07-1350 63270
stocks2026-07-131 429 75912 927
turnover2026-07-13104 3198 240
rating2026-076 165548

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-сервиса.