Files
codex 8748760594
CI / tests-and-images (pull_request) Successful in 2m14s
Runner smoke test / runner-smoke (pull_request) Successful in 2s
Align Windows release bundle mode validation
2026-11-03 10:12:52 +03:00

16 KiB

Deployment

Status and authorization

The release and fixed-receiver code from PR #59 is on main, but has not been installed or exercised against the VPS. This PR's compatibility and Windows bootstrap changes are still proposed. No release, backup, restore, or production service operation has been performed by this change. Production deployment remains blocked until the owner has verified host state, registry access, protected configuration, corpus/index readiness, RU/KY canaries, an external backup, and restore on a separate target. See current state and the owner evidence packet.

The existing codex account has no Docker socket access and receives no new privilege in this change. The external receiver requires an owner-controlled SSH entry that runs the fixed root command; no SSH key, authorized_keys entry, sudo rule, or root alias is created here. If akyldash_vps logs in as codex, it cannot run the root-only receiver and the client fails closed. Do not grant general sudo or Docker access to make the flow work.

Release verification and image publication

.gitea/workflows/ci.yaml remains the required test and image-build workflow. The release workflow runs after successful main CI, verifies the source run and commit, and publishes digest-pinned images plus a small release descriptor. The descriptor records both the release workflow run ID and the successful CI source run ID. PR builds do not publish images. Production runtime secrets are not supplied to CI.

Read-only Gitea check on 2026-11-03 for current main SHA f19faf628e67015febac22d0ec93ce4782caf4bf: CI run #706 succeeded, release run #708 failed at the explicit registry-credential-presence check because REGISTRY_USERNAME and REGISTRY_PASSWORD are not configured, and handoff run #709 was skipped. No secret values were read. Image publication therefore remains blocked until the owner provisions the required registry secrets.

On the host, the fixed root receiver uses the installed gitea-api helper to re-fetch and verify the exact successful CI and release runs, current main, artifact name, checksum, manifest schema, builder pin, image policy, and commit binding. Its binary DOWNLOAD extension accepts HTTPS and at most one allowlisted HTTPS redirect, does not forward the token across origins, bounds the response, and refuses to overwrite an existing file. The installer checks the exact audited helper fixture hash before applying its patch. The helper and source code are not installed on the VPS by this PR.

One-shot runtime binding and bootstrap compatibility

main includes a root-only, read-only status command and a fixed SSH dispatcher. The dispatcher accepts only the literal status or deploy command in SSH_ORIGINAL_COMMAND; it accepts no arguments and performs no shell evaluation. Status verifies successful current-main provenance, artifact metadata/ZIP, and host runtime compatibility before returning only a run ID, commit, and descriptor digest in a prepared response. It fails closed on a recovery journal, incomplete Gitea run listing, invalid release data, changed host runtime configuration, or an already applied release. The root receiver independently repeats release verification and the existing backup/ACK/cutover guards.

The Windows --once mode only polls the fixed read-only status endpoint and stores a protected local PREPARED tuple. It never sends a deploy request. Automatic deployment uses a second task and a separate release-auto-apply.json gate; both config files and both scheduler XML templates default to disabled. The apply task acts only when its current verified tuple exactly matches a tuple saved by a previous poll. An owner can also use --apply-prepared, which requires an interactive exact-SHA confirmation. Both deploy paths re-enter the existing root receiver and all backup/ACK/cutover guards. Rollback remains a separate explicit owner-run командой. Планировщик запускает задачи от интерактивного владельца с минимальными правами, пропускает повторный запуск при активном экземпляре и не будит компьютер. Этот PR не зарегистрировал и не включил ни одну задачу.

Сначала подготовьте bundle под обычной учётной записью из точной локальной вершины main командой prepare_windows_release_bundle.py. Упаковщик читает только Git objects указанного полного commit SHA, сверяет Git blob identity исходников и создаёт небольшой ZIP с SHA-256; рабочее дерево он не читает и не копирует. Elevated installer получает путь к ZIP, ожидаемый commit и SHA-256. Он один раз открывает пакет с запретом записи/удаления, проверяет SHA-256 архива, manifest и Git blob IDs в памяти, затем копирует только проверенные байты. Дополнительно installer проверяет цепочку Git objects commit → tree → файлы. На elevated этапе Git не запускается; .git/config, hooks и состояние checkout не используются. Не изменяйте ZIP между подготовкой и установкой. Упаковщик ставит всем ZIP entries тип regular file и mode 0100600; installer проверяет тот же mode (0x8180) до чтения содержимого. Для проверки стыка упаковщика и фактической функции mode-валидации на Windows используйте powershell.exe -NoProfile -File infra/deploy/test_windows_release_bundle_mode.ps1 -PythonPath C:\Python313\python.exe; git.exe должен уже разрешаться обычным PATH. Harness создаёт синтетический Git репозиторий и временный ZIP, извлекает только эту функцию из PowerShell AST и проверяет принятие штатного пакета и отказ после изменения mode. Он не запускает installer, не требует elevation, не меняет ACL и не регистрирует задачи; политику выполнения PowerShell менять для него не следует.

Проверенный bootstrap installer проверяет подпись PSF у C:\Python313\python.exe и защищённые ACL, затем размещает файлы клиента в SHA-версионном каталоге Program Files, не перезаписывая существующую версию. Для новых файлов кода владельцем назначается Administrators. У кода владельцем могут быть только SYSTEM, Administrators или TrustedInstaller; учётная запись задачи допускается владельцем только для отдельного private state/config. Проверка ACL файла и защищённого каталога запрещает недоверенным субъектам запись, добавление, удаление, удаление дочерних объектов, смену прав или владельца. Для родительских каталогов проверяются права, способные подменить защищённый путь; разрешение создать посторонний соседний каталог подменой не считается. Installer сохраняет существующий canonical-файл C:\ProgramData\Akyldash\release-trigger.json, только если он выключен; второй disabled auto-apply config создаётся только при отсутствии. Installer формирует два disabled task XML для проверки владельцем, но не регистрирует и не включает задачи. Launcher запускает Python с -B -E -s -S, оставляет в import path только стандартную библиотеку и защищённый каталог клиента, затем проверяет происхождение импортированных модулей. До импорта receiver он заменяет унаследованный command lookup фиксированными каталогами Windows, OpenSSH и Git, очищает GIT_* и проверяет, что ssh.exe, git.exe и powershell.exe разрешаются в заданные пути. Для самих бинарников и их родительских каталогов проверяются reparse points, владельцы и ACL по правилам выше. Тесты подмены Python imports и command paths выполнены на Linux; installer и launcher на Windows не запускались. Оставляйте оба config gate и обе задачи выключенными до фиксации владельцем обычных backup/restore и production readiness gate. Не привязывайте Windows-хост к Gitea workflow или PR runner.

The root status/deploy helper remains the reviewed root-installed bootstrap code; it never checks out or executes target release code. A newer app release is eligible without manually advancing /opt/akyldash/current only when the host's Compose file, Caddyfile, and installed release policy match the exact bytes at the prepared commit in Gitea. File metadata is pinned to that commit; the Git blob SHA and size are verified. Status checks before preparing the tuple; the receiver repeats the check before creating a journal or stopping services, and cutover checks again after the verified external backup and immediately before switching image references. A Compose/Caddy mismatch returns runtime_config_changed; a policy mismatch returns release_policy_changed and requires a reviewed owner bootstrap update. No host checkout or root helper is automatically changed. The release must still be current main; CI/release provenance, digest, backup, canary, and rollback checks remain required. Gitea's filtered workflow-run listing is bounded to ten entries; if Gitea reports more entries or an inconsistent page, status returns unavailable rather than guessing.

Automation boundary: deployment is not active

Until the owner separately activates both Windows tasks and the auto-apply configuration after the backup/restore and production gates, the chain ends at release publication. After that explicit one-time owner authorization, the prepared-state poll and separate apply task can process later matching application releases without advancing /opt/akyldash/current or approving each commit individually. A release that changes protected runtime files still requires a reviewed bootstrap update.

  1. Successful main CI triggers image builds and publishes digest references plus the release descriptor artifact.
  2. deploy-handoff.yaml verifies the source run and may read artifact metadata. Its job is explicitly status-only: NOT DEPLOYED. It does not download the descriptor ZIP, call the Windows client, open SSH, or invoke the root receiver. A green result means only that this status check completed.
  3. The fixed receiver is physically started only by the separately enabled owner-controlled auto-apply task or by an interactive owner command. The --once preparation task stores a verified tuple and never sends deploy; the separate apply task requires the tuple to match a fresh status response. No repository workflow invokes either task. Both scheduler templates and both configuration gates remain disabled, and neither task has been installed or exercised.

Production CD is still not active: task/credential setup has not been verified, and no VPS or live D: operation has been performed. This design avoids a Windows Actions runner entirely; the owner-controlled local task is not exposed to PR jobs. The SSH account/key must be restricted to the exact dispatcher commands and verified host key. Do not use an unrestricted authorized_keys entry or a persistent host-mode runner with D: and SSH access.

External backup and cutover

The fixed root release receiver accepts only a bounded protocol request over stdin. It does not accept a remote shell command, artifact path, or image reference from the caller. It rechecks the release and current main, takes a shared lock, captures running image IDs, and stops the stack. It streams the cold archive directly to the owner's Windows client at D:\Dev\akyldash\.backups; the VPS does not hold a full archive, including temporarily. The Windows client requires a real NTFS destination with private ACLs and a precise local Git exclude rule. It publishes only after hash, archive, and manifest verification, and acknowledges the exact live session nonce and backup identity.

Only after the acknowledgement does the host pull approved digest references, atomically replace both image refs, and start Compose with --no-build. It checks running image IDs, /review, and non-empty Russian and Kyrgyz search canaries. A stale main, source mismatch, invalid archive, image mismatch, or failed health/canary check stops cutover and attempts recovery using the pre-change references. A recovery journal blocks another operation if automatic recovery cannot be verified.

Rollback uses the root-pinned prior external archive and creates a fresh external backup of current state before switching refs. It restores images only, not data. Old local VPS backup paths and legacy release states without a pinned external backup identity are rejected. See backup and restore and rollback.

The receiver records a durable pending recovery journal before stopping services. A retry of the exact release already recorded in pinned state is rejected before backup or downtime. Gitea source verification requires status=completed and conclusion=success, and resolves current main from the explicit production checkout. The Windows client checks the destination, ACL, ignore rule, and full protocol archive capacity before opening SSH.

Owner bootstrap still required

After review and merge, the owner must decide whether to install the helper from a root-owned checkout and provision, without exposing values in logs:

  • a read-only Gitea API token in /etc/akyldash/gitea-api-token, root-owned mode 0600;
  • the exact artifact redirect origin allowlist in /etc/akyldash/gitea-download-origins, if the Gitea artifact endpoint redirects;
  • approved Russian and Kyrgyz search canaries in /etc/akyldash/release-canaries.json, root-owned mode 0600;
  • a root-only SSH path to /usr/local/sbin/akyldash-release-deploy and /usr/local/sbin/akyldash-release-rollback using the existing owner key and verified host key.
  • a dedicated SSH alias akyldash_release backed by a key restricted to /usr/local/sbin/akyldash-release-dispatch; its forced command must preserve exact SSH_ORIGINAL_COMMAND matching and grant no shell, forwarding, PTY, or arbitrary command access;
  • a protected installation of the Windows client under C:\Program Files\Akyldash, with both config files and both Task Scheduler XML templates reviewed. Keep the preparation task/config separate from the auto-apply task/config; the auto-apply pair remains disabled until the owner explicitly accepts unattended production deployment.

The installer intentionally does not create credentials, SSH entries, canary values, registry permissions, or a new privilege rule. Existing files with unexpected owner, mode, or content cause installation to stop for owner review. Do not run the installer or receiver from CI. The owner's first use must be a separate-target restore check and a reviewed dry run; this PR does not authorize a production cutover.

The Windows client uses the existing OpenSSH host-key checking. Its configured alias must reach a root-controlled endpoint capable of invoking the fixed receiver; no new key or sudo permission is requested. Windows ACL and NTFS checks, SSH host configuration, artifact redirect origin, registry pull access, and every production health gate remain unverified until the owner checks them.