Prepare deploy writes before stopping the service #748
Closed
kayg
wants to merge 11 commits from
fix/deploy-688-round5 into dev
pull from: fix/deploy-688-round5
merge into: kayg:dev
kayg:main
kayg:t3/paged-search-reconcile-final
kayg:feat/style-sheet-fixes
kayg:storage/wal-checkpoint-off-save-1051
kayg:feat/contrast
kayg:feat/pass8
kayg:review/calendar-2
kayg:fix/shared-row-swipes
kayg:dev
kayg:feat/collections-db
kayg:fix/instant-keys-1056
kayg:feat/money-round
kayg:perf/stalls-1054
kayg:wip/tiering-v3-cold4
kayg:wip/tiering-v3-cold5
kayg:port/rows-actions
kayg:port/rows-drop
kayg:port/hidden-blocks
kayg:port/rows
kayg:port/port-tables
kayg:port/port-tables-2
kayg:t3/mail-1011-liveness
kayg:port/devices
kayg:port/port-attachments
kayg:port/port-attachments-2
kayg:port/port-attachments-3
kayg:docs/design-consistency
kayg:docs/engineering-standards
kayg:fix/rusqlite-0.41-rustsec-2026-0459
kayg:feat/iphone-fixes
kayg:shared/restore-and-stale-918-919
kayg:fix/phone-top-band-contrast
kayg:feat/notes-approve
kayg:perf/soak-max-gate
kayg:feat/files-s1-identity
kayg:fix/lifecycle-event-id
kayg:feat/quickfix
kayg:wip/tiering-v3-cold
kayg:perf/web-mockup-frame-fixes
kayg:port/contacts-merge
kayg:port/contacts-edit
kayg:test/shared-room-no-deleted-text
kayg:perf/first-paint-credits
kayg:fix/shared-history-after-grant
kayg:fix/phone-swipe-regressions
kayg:feat/contrast-r1
kayg:review/sharing-pr3-review
kayg:feat/tiering-v3-s2-tags
kayg:feat/folder-drop
kayg:fix/deskpills-lazy-share
kayg:feat/sharing-s1-pr3
kayg:tmp/money-round-before
kayg:fix/phone-pane-backdrop-roots
kayg:fix/palette-preload-chips
kayg:fix/invite-defaults
kayg:port/tasks-matrix
kayg:fix/restore-green-gates
kayg:fix/sharing-fast-path-hardening
kayg:t3/photos-s1
kayg:feat/tiering-v3-s1-namespace
kayg:notes-archive-hide
kayg:feat/tasks-actions
kayg:t3/photos-plan-slice-one
kayg:feat/whiteboard-s1
kayg:t3/paged-search-reconcile
kayg:t3/smooth-pane-transitions
kayg:t3/index-position-startup-scan
kayg:fix/private-staging-759b-r2
kayg:feat/dav-transport
kayg:feat/imap-notes-fetch
kayg:feat/imap-notes-projection
kayg:feat/imap-notes-readonly
kayg:feat/imap-notes-auth
kayg:feat/imap-notes-server-foundation
kayg:docs/undo-block-ids-plan
kayg:fix/undo-768
kayg:proto/block-identity-v7d
kayg:issue/carddav-read-perf-fix-r2
kayg:issue/carddav-read-r2
kayg:docs/imap-766-plan-v2
kayg:docs/imap-notes-plan
kayg:docs/quota-plan-2
kayg:fix/parallel-test-flakes
kayg:feat/contacts-companion-pure
kayg:docs/calendar-backend-v2-plan
kayg:feat/calendar-ical-converter
kayg:docs/tasks-writes-v2-plan
kayg:docs/carddav-writes-v2-plan
kayg:docs/contacts-import-v2-plan
kayg:issue/610-pm-midnight
kayg:feat/calendar-v2-s1-containment
kayg:docs/dav-recorder-v2-plan
kayg:plan/previews-865
kayg:plan/save-log-durable-entry
kayg:fix/e2e-data-on-ram
kayg:feat/appwindow3
kayg:feat/appwindow2
kayg:feat/appwindow
kayg:feat/money-p1
kayg:feat/keys-sheet
kayg:review/calendar-2-themes
kayg:feat/smooth-cursor
kayg:fix/float-style
kayg:review/calendar-2-float
kayg:release/2026-11-06-hotfix
kayg:archive/port/rows-drop-code
kayg:archive/port/rows-actions
kayg:archive/port/rows
kayg:scratch/tasks-894-ui-gates
kayg:review/calendar-2-night-wip
kayg:release/2026-11-05
kayg:archive/feat/tiering-v3-s1-namespace
kayg:archive/flaky/rows-drop-fixed
kayg:archive/flaky/rows-actions
kayg:archive/feat/tiering-v3-reader
kayg:archive/feat/tiering-v3-roots
kayg:archive/flaky/rows
kayg:sim/bm15-final
kayg:sim/bm15-fix
kayg:sim/bm15-bis-932
kayg:sim/bm15-bis-909
kayg:sim/bm15
kayg:sim/bm15-bis-887
kayg:perf/tiering-option-b
kayg:port/style-3
kayg:port/style-2
kayg:port/style
kayg:fix/sw-lazy-precache
kayg:fix/vim-w-waits-for-save
kayg:fix/signout-test-races
kayg:fp-scratch/all
kayg:sim/bm15-first-paint
kayg:archive/bm15/rows-drop
kayg:archive/bm15/rows-actions
kayg:fix/palette-snippet-contrast
kayg:archive/bm15/rows
kayg:archive/bm15/hidden
kayg:archive/plan/save-log-durable-entry
kayg:mock/money-fix2
kayg:archive/fix/e2e-data-on-ram
kayg:mock/appearance-all
kayg:mock/pill-press
kayg:archive/docs/design-consistency
kayg:mock/auto-swipes
kayg:mock/mail-actions
kayg:archive/feat/mail-p1
kayg:mock/money-static
kayg:archive/review/standards-917
kayg:archive/docs/engineering-standards
kayg:perf/tiering-761-fix
kayg:mock/type-audit
kayg:mock/third-pane
kayg:feat/contacts-import-v2-s1
kayg:perf/executor-hot-path
kayg:archive/port/hidden-blocks
kayg:archive/port/devices
kayg:scratch/executor-on-894
kayg:scratch/executor-spans2
kayg:scratch/executor-spans
kayg:train/2026-11-03-bm14c
kayg:archive/port/rows-drop
kayg:feat/tiering-761-d-scale
kayg:feat/tiering-761-c-index
kayg:feat/tiering-761-b-reader
kayg:archive/review/sharing-pr3-review-9fec3d1f
kayg:train/2026-11-03-bm14b
kayg:perf/ports-lazy-budgets
kayg:feat/tiering-761-a-migration
kayg:train/2026-11-03-bm14
kayg:train/2026-11-03-bm12
kayg:archive/review/sharing-pr3-review
kayg:archive/feat/sharing-s1-pr3
kayg:train/2026-11-03-bm11
kayg:archive/feat/imap-notes-fetch
kayg:train/2026-11-03-bm10
kayg:archive/review/sharing-pr3-review-code
kayg:archive/plan/previews-865
kayg:train/2026-11-02-bm9
kayg:archive/port/port-attachments-3
kayg:archive/port/contacts-merge
kayg:fix/motion-audit-859
kayg:train/2026-11-02-bm8
kayg:archive/scratch/bm8-prep
kayg:archive/port/port-pickers
kayg:archive/port/port-tables-2
kayg:archive/port/port-tables
kayg:archive/feat/sharing-s1-pr3-code
kayg:archive/scratch/bm7-fixed
kayg:train/2026-11-02-bm7
kayg:archive/fix/parallel-test-flakes-code-minipc01
kayg:train/2026-11-02-bm6
kayg:docs/imap-766-readonly-slice
kayg:feat/dav-recorder-v3
kayg:research/788-diagnosis
kayg:archive/fix/parallel-test-flakes
kayg:archive/perf/pr792-cold-index-edit
kayg:docs/sharing-slice-1
kayg:archive/docs/imap-766-plan-v2
kayg:feat/dav-recorder-v2
kayg:archive/docs/dav-recorder-v2-plan
kayg:spike/imap-766-s0-cases
kayg:archive/fix/undo-768
kayg:feat/dav-recorder
kayg:archive/docs/contacts-import-v2-plan
kayg:archive/feat/calendar-ical-converter
kayg:feat/contacts-import
kayg:perf/cold-growth-on-764
kayg:perf/cold-growth-on-tiering
kayg:archive/docs/tasks-writes-v2-plan
kayg:archive/docs/calendar-backend-v2-plan
kayg:archive/feat/contacts-companion-pure
kayg:feat/tasks-set-status
kayg:feat/tasks-move-reorder
kayg:archive/docs/carddav-writes-v2-plan
kayg:archive/feat/calendar-v2-s1-containment
kayg:feat/tasks-delete-promote-duplicate
kayg:archive/feat/dav-transport
kayg:archive/fix/carddav-rebuild-r1
kayg:archive/fix/carddav-retention-r1
kayg:feat/carddav-writes
kayg:perf/quota-on-tiering
kayg:perf/quota-simple
kayg:perf/cold-growth-on-quota-stack
kayg:feat/calendar-identity
kayg:feat/calendar-event-parser
kayg:feat/calendar-props-maps
kayg:port/glass-when-ring
kayg:feat/tiering-761-s1
kayg:wip/10by32-01-impl-759b-scratch-20261030
kayg:wip/6by32-01-bm3-webkit-pause-20261030
kayg:wip/10by32-01-bm3-chromium-pause-20261030
kayg:wip/6by32-01-tier-761-s1-pause-20261030
kayg:spike/imap-766-s0
kayg:wip/6by32-02-calternal-pause-20261030
kayg:wip/8by64-01-review-688-pause-20261030
kayg:wip/6by32-01-verify-744-pause-20261030
kayg:wip/6by32-01-verify-742-pause-20261030
kayg:fix/private-staging-759b
kayg:wip/6by32-01-review-748-pause-20261030
kayg:wip/6by32-01-review-733c-pause-20261030
kayg:wip/6by32-01-review-733b-pause-20261030
kayg:wip/6by32-01-perf-idle-8-pause-20261030
kayg:wip/6by32-01-perf767-base-pause-20261030
kayg:wip/6by32-01-perf767b-767-pause-20261030
kayg:wip/6by32-01-perf767b-764-pause-20261030
kayg:wip/6by32-01-perf767-767-pause-20261030
kayg:wip/6by32-01-perf767-764-pause-20261030
kayg:wip/6by32-01-lists-b-pause-20261030
kayg:wip/6by32-01-ids-verify5-pause-20261030
kayg:wip/6by32-01-ids-verify4-pause-20261030
kayg:review/calendar-month
kayg:wip/10by32-02-undo-768-20261030
kayg:test/ids-verify3
kayg:wip/10by32-02-bm3-chromium-20261030
kayg:wip/10by32-01-review-753-20261030
kayg:wip/10by32-01-perf-idle-2-20261030
kayg:wip/10by32-01-bm2-webkit2-20261030
kayg:plan/759b-staging
kayg:train/2026-10-30-bm2
kayg:review/758b-tests
kayg:review/758-tests
kayg:review/764d-tests
kayg:review/767b-tests
kayg:review/preview-2026-10-29
kayg:review/767-tests
kayg:docs/tiering-731-plan
kayg:wip/c6b02-when2
kayg:wip/c6b02-tasks-create-fix5
kayg:wip/c6b02-settings-frost-mock
kayg:wip/c6b02-settings-frost-base
kayg:wip/c6b02-settings-frost
kayg:wip/c6b02-ring
kayg:wip/c6b02-review-733d
kayg:wip/c6b02-bm1-full-a
kayg:wip/compute02-when-field
kayg:wip/compute02-webkit-lists-repro
kayg:wip/compute02-train-bl2-dev
kayg:wip/compute02-dev-green-pw164
kayg:docs/deploy-plan
kayg:review/764c-tests
kayg:review/764b-tests
kayg:review/748g-tests
kayg:review/764-tests
kayg:review/759b-tests
kayg:fix/storage-owner-liveness-642
kayg:fix/storage-fix-7
kayg:wip/compute02-storage-fix-7
kayg:review/759-tests
kayg:feat/tiering-731
kayg:wip/tiering-731-first-attempt
kayg:wip/compute02-storage-fix-1
kayg:review/754g-tests
kayg:perf/idle-honest-2
kayg:review/754f-tests
kayg:review/754e-tests
kayg:review/755-tests
kayg:review/754d-tests
kayg:review/754c-tests
kayg:review/754b-tests
kayg:review/754-tests
kayg:fix/storage-private-tmp-642-dev
kayg:wip/compute02-verify-750
kayg:test/ids-verify5
kayg:test/ids-verify4
kayg:fix/storage-exact-file-quarantine-642
kayg:wip/compute02-verify-740
kayg:perf/one-flush-save
kayg:review/calendar-static
kayg:wip/compute02-verify-737
kayg:review/indexing-ring
kayg:review/when-field-2
kayg:wip/compute02-review-733
kayg:proto/block-identity-2-review
kayg:proto/block-identity-review
kayg:wip/c6b02-tc-design
kayg:review/when-field
kayg:issue/tasks-create-ids
kayg:issue/tasks-create
kayg:review/combined
kayg:wip/compute02-review-combined
kayg:review/task-polish
kayg:review/top-buttons
kayg:review/q1-q5
kayg:train/2026-10-26-bl2
kayg:archive/wt-398h
kayg:archive/train-bm-proptest-seed
kayg:archive/integ325-293
kayg:archive/integ325-ci
kayg:archive/client-auth-review-baseline
kayg:archive/wt-sim-4853
kayg:archive/wt-sim-4811
kayg:archive/wt-448m
kayg:archive/wt-398a-allocator
kayg:issue/calendar-adr
kayg:archive/old/train/2026-10-23bk2
kayg:archive/old/train/2026-10-23bk
kayg:issue/469-mockup-9a
kayg:issue/smart-tasks-mockup
kayg:issue/469-menus
kayg:issue/652-signout-flake
kayg:issue/652-history-fix7
kayg:issue/652-history-fix6
kayg:archive/old/issue/spring-check-perf
kayg:issue/spring-check-perf
kayg:issue/659-rename-fast
kayg:archive/old/train/2026-10-21ar
kayg:issue/search-hardening-3
kayg:issue/652-history-fix5
kayg:issue/660-port-sheets-glass
kayg:issue/652-history-fix4
kayg:issue/652-history-fix3
kayg:issue/652-history-fix2
kayg:issue/652-history-flake
kayg:train/2026-10-21an
kayg:issue/settings-admin-port
kayg:issue/search-port
kayg:train/2026-10-20am
kayg:issue/storage-split-fix2
kayg:issue/storage-split
kayg:archive/old/issue/folder-ids-fix
kayg:train/2026-10-20al
kayg:archive/old/issue/folder-ids
kayg:train/2026-10-20ak
kayg:issue/tables-mockup
kayg:train/2026-10-19aj
kayg:train/2026-10-19ai
kayg:issue/469-merge-fixes
kayg:issue/sidebar-band
kayg:archive/old/issue/608-settings-spec
kayg:train/2026-10-19ah
kayg:issue/swipe-offscreen
kayg:train/2026-10-19ag
kayg:archive/old/issue/590-nlp-adr
kayg:archive/old/issue/587-event-after-write-2
kayg:train/2026-10-19af
kayg:archive/old/issue/587-event-after-write
kayg:issue/469-pill-room-c
kayg:issue/469-pill-room
kayg:train/2026-10-19ae
kayg:issue/469-narrow-handle
kayg:issue/397-ios-swipe-2
kayg:archive/old/issue/adrs-photos-files-tasks
kayg:issue/469-desk-fixes
kayg:archive/old/issue/auto-update-adr
kayg:issue/469-filter-gap
kayg:issue/469-port-1b
kayg:issue/442-opacity-flake
kayg:archive/397-ios-swipe-2
kayg:issue/327-web-fast-3
kayg:archive/old/issue/469-port-1
kayg:issue/469-port-1
kayg:issue/327-web-fast-2
kayg:archive/old/issue/327-web-fast
kayg:archive/old/issue/425-stt-bench-vq1
kayg:archive/old/issue/445-no-bleed
kayg:archive/old/issue/400-swipe-mockup-2
kayg:issue/426-client-review
kayg:issue/426-client-2
kayg:issue/426-client
kayg:archive/old/issue/476-safe-area-lab
kayg:archive/445-top-light-2
kayg:archive/old/issue/445-top-light
kayg:issue/471-blocks-mockup
kayg:archive/old/issue/448-links-app-2
kayg:archive/old/issue/448-links-app
kayg:archive/old/issue/398-cold-links
kayg:issue/434-442-small-2
kayg:issue/434-442-small
kayg:archive/old/issue/398-unload
kayg:archive/old/issue/445-bottom-band
kayg:issue/445-bottom-band
kayg:issue/351-rem-3
kayg:archive/312-views-mockup
kayg:issue/351-rem-2
kayg:issue/351-rem
kayg:archive/old/issue/327-first-note
kayg:issue/395-width-phone
kayg:archive/old/issue/327-typing
kayg:issue/396-ios-edges
kayg:issue/397-ios-swipe
kayg:archive/398-allocator
kayg:archive/old/issue/327-notes-index
kayg:issue/345-347-contrast-empty-2
kayg:archive/old/issue/44-arenas
kayg:issue/44-arenas
kayg:archive/integ325
kayg:archive/old/issue/327-perf-harness
kayg:archive/old/issue/44-thp
kayg:issue/44-thp
kayg:archive/old/issue/285-sticky-pills
kayg:archive/old/issue/44-trim-3
kayg:issue/44-trim-3
kayg:archive/old/issue/165-cards
kayg:archive/old/issue/165-picker-2
kayg:archive/old/issue/212-multi-select
No reviewers
Labels
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
kayg/calternal!748
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/deploy-688-round5"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Deploys could stop the only healthy process before image-store and rollback writes had finished. This completes preparation under one target lock before stopping, then commits a prepared image record by rename.
Validation: bash syntax, shellcheck, deploy regression gate (2,402 injected cases), updated transport checks with real user systemd, relay checks, and 768 additional persistent fallback cases pass: 3,170 injected cases total. All fixture paths are under /home. The final real staging replay passes 13 rows, including persistent ENOSPC, failed health, real generator-output loss and an SSH disconnect while the old service is stopped. Staging ends healthy on A with rollback B; its unit bytes and mount identities are preserved.
Final logs and rehearsal source. Related to #688; replacement for #733 because this forge ignores pushes to an open PR branch. Head
d2ade1fb93. No production contact, merge, or production deploy.Independent sixth-round review started. Branch fix/deploy-688-round5; review head
d2ade1fb93; merge base3d0c6e33d5; dev at review startd83e54dbe5. I will inspect every commit and touched deploy file, run local failure injection and gates, and rehearse only against the configured staging target on 8by64-compute01. No push, merge, production contact, or userdata operations.Review base: merge-base
3d0c6e33d5; current dev reported by PR APId83e54dbe5. Head remainsd2ade1fb93. Staging rehearsal is currently blocked: NetBird SSH to agent@8by64-compute01.vpn.kayg.org fails DNS lookup (no such host). NetBird management/signal are connected and list that peer. I am checking its reported VPN address while continuing local checks. The initial gate stopped at missing shellcheck; a private extracted shellcheck 0.10.0 is now supplied on PATH for the unmodified gate.P2 — First normal deployment after bootstrap requires an undocumented predecessor
Location: deploy/next/switch.sh:102–104; deploy/next/README.md:166–169.
When the image record is absent, preparation unconditionally requires
localhost/$NAME:rollbackto exist and differ from the current image. A fresh installation, or the documented supervised bootstrap, can have one labelled healthy current image and no rollback tag. Its next normal deploy fails withbootstrap predecessor missing, before stopping. The new transaction already has the healthy current digest needed for candidate recovery and for the successor rollback record, so requiring a still older image prevents the documented first normal deployment. The supplied fixtures always seed two distinct tags and miss this case. Support an absent entry predecessor, or explicitly provision/document a safe bootstrap record without pretending the current image is its own predecessor.Repro:
python3 review-artifacts/independent-probe.py, caseno-predecessor: baseline is active, reports full SHA a40, current digest is sha256:164, no pointer file and no rollback tag; submit valid revision b*40 bundle and unit. Exit 1; outputbootstrap predecessor missingthentransaction failed before switch; old service unchanged; zero stop calls. Same harness's normal case with a predecessor succeeds, rerun keeps the record and performs no stop/start/tag, and code rollback succeeds. Unlabelled baseline case refuses withbootstrap first, with zero stop calls, as documented.Reviewer probe source and output.
Independent checks update: 13 reviewer-written scenarios completed. Candidate load/tag/unit-copy/reservation/stop-after-effect/unit-rename/reload/start/health/pointer-commit failures preserve the entry image and userdata sentinel. Success, same-SHA rerun and code rollback pass; unlabelled baseline refuses before stopping. The real user-systemd transport checks also pass (abandoned FIFO, normal, failed transaction, closed output, dropped caller, locked receipt). A private extracted Quadlet 5.4.2 passes real generation and invalid-candidate rejection. These local checks do not satisfy the required staging rehearsal: hostname lookup still fails; both NetBird IPv4 and IPv6 attempts time out. No staging mutation has been confirmed; no production contact. The unmodified exhaustive gate is still running in one build slot.
P1 — The documented first-use bootstrap can strand the old service on a full disk
Location: deploy/next/README.md:160–174 (preparation at 140–159).
Normal deploy and rollback correctly refuse an unlabelled running image, so this supervised procedure is the documented route to the first provenance-aware production image. Unlike the automated switch, it does not reserve candidate/recovery runtime space, and step 6 stops the service before writing the new current tag. If storage fills at that boundary, tagging fails and both primary and pinned fallback startup can fail because Podman still needs runtime/container metadata space. There is no protected recovery allocation to release. The earlier storage-reservation section describes space.sh's automated transaction; the bootstrap steps never invoke or reproduce that preparation. Calling the procedure supervised does not preserve the old process under this failure.
Repro:
python3 review-artifacts/bootstrap-probe.pyencodes README steps 6–8, with local mocks only. The control switches successfully. With persistent ENOSPC injected immediately after the first stop, new tagging fails, restoration tagging fails, primary startup fails, and fallback startup fails. Output:bootstrap recovery failed; old process is stopped; exit 28; running image is null. The model also attempts the pinned fallback after a failed primary start, giving recovery that additional chance. This is a model of the documented procedure, not execution of a repository bootstrap command (none exists), and it has not been confirmed on staging because SSH is blocked. The runtime-write premise is also acknowledged in space.sh:4–7.Apply the same preparation and reserved-space recovery guarantees to first-use bootstrap, including immutable unit identities prepared before stop, and rehearse that route on staging. The unlabelled refusal itself is safe and is not the finding.
Reviewer probe source and output.
Verdict: fix first. One P1 and three P2 findings.
Independent sixth-round review of head
d2ade1fb93on fix/deploy-688-round5, into dev. Merge base:3d0c6e33d5. All 11 commits, every touched deploy file and added test were inspected.P1, deploy/next/README.md:160–174: the documented bootstrap for an unlabelled running image stops before retagging and lacks the automated path's protected runtime reserves. In the reviewer model of steps 6–8, persistent ENOSPC after stop rejects new tagging and both old-image startup paths, leaving the old process stopped. This is a model of the written procedure, not execution of a repository bootstrap command; none exists. The ordinary command's unlabelled refusal is safe. Finding and reproduction.
P2, deploy/next/switch.sh:102–104: a healthy labelled baseline without an image record or older rollback tag cannot perform its first normal update. The actual Bash transaction returns
bootstrap predecessor missing, with zero stops. The guide does not provision that predecessor, and the supplied fixtures always seed two tags. Finding and reproduction.P2, deploy/next/target.sh:35–46: the detached receiver does not enforce umask 077. The actual worker under real user systemd creates its receipt with mode 0664 (manager umask 0002), while the enclosing directory remains 0700. A private positive control adding only the worker umask creates mode 0600. The parent umask does not cross this boundary. Finding and real systemd reproduction.
P2, deploy/next/target.sh:43–59: if the receiver fails before opening the FIFO and exits before the producer opens it, ordinary
>creates a regular .stream file. The real-systemd probe retained all 262144 artifact bytes in the control directory after failure, with the worker gone and lock free. Finding and real systemd reproduction.Reviewer probe source and outputs. Fifteen independent transaction scenarios cover success, same-SHA rerun, code rollback, failure recovery, pinned fallback, unlabelled refusal and the missing-predecessor defect. Userdata sentinels remain unchanged. Two separate bootstrap-guide model cases cover the control and persistent ENOSPC.
Local validation is complete. Bash syntax and shellcheck pass. The original unmodified gate reached its 2400-second watchdog and exited 124, confirming 1,488 injection positions; it is not a full gate pass. A separate resumed run executed the remaining 1,682 positions through the original injection function and three-worker limit, completing all 3,170 combined cases with userdata unchanged. Remaining relay, artifact-build and next/cloud dry-run checks pass. The Rust HTTP health test passes with the reviewed full SHA supplied at compile time: 1 passed, 0 failed. That SHA is present in the passed test binary.
The supplied real user-systemd checks and private Quadlet 5.4.2 generation checks pass. The additional real-systemd probes reproduce the two transport defects above. Passing mocks do not resolve those findings. The separate committed-diff whitespace check reports one cosmetic blank line at space.sh:46.
The required real staging rehearsal remains blocked. Initial hostname attempts failed DNS lookup; the final bounded attempts timed out on agent@8by64-compute01.vpn.kayg.org and its current peer VPN IPv4 and IPv6 addresses. No remote command or mutation was confirmed. No real staging Podman/application deploy, rollback, first-use or full-disk rehearsal was completed. Resolve staging access and rehearse those paths before using this deploy route.
The reviewed head and PR head remain
d2ade1fb93. No production contact, push, merge, deployment or issue closure was performed. Product files and the index are unchanged. Findings are in the job's untracked findings.md; evidence is attached here and is not committed.Final verification, verbatim gate output and evidence.
P2 — Detached receipt worker does not enforce private file modes
Location: deploy/next/target.sh:35–46.
The caller sets umask 077, but systemd starts the detached worker with the service manager's own umask. The worker sets no umask and no UMask property before cat creates $stage/input. Its receipt file therefore does not reliably have mode 0600. The real user-systemd probe measured worker umask 0002, receipt mode 0664 and directory mode 0700. A private copy with only
umask 077added inside the worker measured receipt mode 0600. No tracked product file was edited. The enclosing directory stays private, so no direct data exposure was observed; the explicit 0700/0600 staging contract is nevertheless broken.Repro:
python3 review-artifacts/receipt-mode-probe.py. This executes the actual target.sh receiver via real user systemd, with filesystem prefixes and the required root owner relocated to this account's private fixture. Only systemd-run/systemctl wrappers select the user manager. The payload records umask and stat metadata, with no application, network or secret access. The second run is an explicitly labelled positive control with the worker umask added to the private script copy. It returnsreceiver umask=0002 receipt mode=664 directory mode=700for the PR andreceiver umask=0077 receipt mode=600 directory mode=700for the control.Set umask 077 inside the detached worker or set its systemd UMask property. Add receipt-mode assertions to the real-systemd transport test; the ordinary process mock inherits the caller's umask and misses this boundary. Production/staging manager defaults have not been measured because target SSH is blocked.
Real systemd probe source and output.
Validation update: the unmodified gate reached its 2400-second watchdog and exited 124. No assertion failure was printed. Its log explicitly confirms 1,488 injection positions. This is not a full gate pass. I am preserving the original output, running the targeted added Rust health test, then resuming only unconfirmed positions through the original probe with its unchanged three-worker limit. The final comment will include the original exit status and gate output verbatim, plus separate resumed coverage. Staging SSH remains blocked; the verdict stays fix first.
The targeted added Rust HTTP health test passes with CALTERNAL_REVISION=d2ade1fb931b4ac74fb698015fcc95c3e7cf69c5: 1 passed, 0 failed, 5 filtered out. Cargo ran with --locked -j3 through the local build slot. Resuming the remaining 1,682 unconfirmed injection positions now, using the original probe, with its three-worker limit; relay/build/dry-run checks follow. The original gate exit 124 and its output remain preserved separately.
P2 — A receiver setup failure can turn the FIFO into a retained artifact file outside the lock
Location: deploy/next/target.sh:43–46 and 59; cleanup at 16–19.
The worker's EXIT trap removes stream when setup fails before opening the FIFO. If it finishes before the caller reaches line 59,
cat > "$job/stream"creates a regular file at that now-missing path. The worker has already released the transaction lock. The producer writes the artifact into the control directory under /run, then the caller returns failure and retains it because started=1. No worker remains to remove the recreated file. This breaks the locked-upload and bounded-control-storage guarantees; image bytes can consume /run rather than the intended receipt filesystem.Repro:
python3 review-artifacts/receiver-exit-probe.pyruns actual target.sh through real user systemd, with private filesystem/owner relocation. The normal control transfers and removes stream. The failure case leaves the private fixture Quadlet directory absent to force receiver mktemp failure, then uses an inotify handshake after the caller unlocks to schedule producer opening after the worker exits. This controls the race ordering without changing transaction logic or adding fixed sleeps. Before producer open: result=001, stream absent, lock available. Afterward: caller exit=1, transaction never entered, stream is a regular file retaining all 262144 input bytes. No app or target was contacted, and no service outage is claimed from this small fixture.Open the FIFO without O_CREAT so a vanished receiver cannot redirect the artifact to a regular control file. A status/type check followed by ordinary
>still has an unlink race. Cover receiver failure before FIFO open, as well as caller disappearance, and remove any failed upload residue.Real systemd probe source and output.
Independent review finished. Verdict: fix first. One P1 and three P2 findings are in the review and attached findings.md.
Head:
d2ade1fb93. Branch: fix/deploy-688-round5 into dev. Tracked worktree and index are unchanged. No push, merge, deployment, issue closure, production contact or evidence commit was performed by this job.Final findings, reviewer probe source and validation evidence.
The original unmodified gate exited 124 after its 2400-second watchdog, with 1488 injection positions explicitly confirmed. Its output below is verbatim and its saved hash is unchanged. It is not a full gate pass. The separately labelled resumed run executed the remaining 1682 positions through the original injection function and three-worker limit, completing all 3170 combined cases, then passed relay/build/dry-run checks. The Rust HTTP health test passed with the reviewed full SHA supplied at compile time.
The required real staging rehearsal remains blocked: initial hostname attempts failed DNS lookup; final attempts to the hostname and current peer VPN IPv4/IPv6 addresses timed out. No remote command or mutation was confirmed. Real user-systemd fixture probes were run locally and reproduce two transport findings; they do not establish application behavior on staging.
Original gate output verbatim (exit 124):
Resumed validation output verbatim (exit 0; includes explicitly confirmed earlier positions):
Targeted Rust health test:
Starting deploy-fix-6 on staging 8by64-compute01. Base
d2ade1fb93, fetched fix/deploy-688-round5. Working branch fix/deploy-688-round6: the job's hard rule requires a new branch and PR because this forge ignores pushes to an open PR branch. I will link the replacement here and keep #748 open.Scope: scripted bootstrap with all image/unit writes and free-space checks before stop; missing first predecessor; worker umask; FIFO producer race. Gates: bash -n, shellcheck, all deploy tests, real staging bootstrap/full-disk/deploy/same-SHA/rollback rehearsal. No production contact, merge, or production deploy. Evidence stays untracked and in issue comments.
Finding → fix → test, round 6 in progress:
P1 bootstrap: README steps 6–8 stop before tagging; the review's persistent ENOSPC model strands recovery. Replacing the manual procedure with
deploy.sh bootstrap, routed through the existing detached lock, free-space admission, runtime reserves, scratch generator, prepared unit/pointer files and recovery. The running legacy digest is explicitly recorded before stop; only that digest permits status-only health, without fabricating a revision. New images still require the dev SHA. README becomes a dry-run and one command.P2 first update: switch_prepare required
:rollbackeven when a healthy labelled baseline had no predecessor. The image record now represents an absent predecessor as-; commit still records the old baseline as the real predecessor. Regression paths exercise the first update and its failures.P2 worker permissions: the parent umask does not cross systemd-run. Worker now sets umask 077 itself. Real user-systemd tests under UMask=0002 pass and verify receipt 0600 and enclosing directory 0700.
P2 FIFO race: shell
>recreates the path after an early receiver exit. Producer now opens without O_CREAT, refuses symlinks and verifies the opened descriptor is a FIFO. A handshake forces receiver completion before producer open; mocks and real user systemd both pass with no retained artifact from a 262144-byte payload. Existing disconnect, abandoned upload, partial receipt, failure result and lock tests pass as well.Evidence: .job-evidence/round6/transport.log (not committed). Full fault injection and real app rehearsals remain to run.
Real rehearsal progress on staging only: installed next normal deploy, same-SHA redeploy (same MainPID), and rollback pass. Disposable real Podman/systemd bootstrap from an unlabelled image passes; failed candidate health recovers the legacy image and original unit bytes. Both full-disk bootstrap admission and exhaustion after admission retain the exact old MainPID at zero available blocks.
The extra legacy rollback run correctly refused free-space admission in the 1 GiB fixture: legacy image size 115693123 bytes requires about 523 MiB headroom while about 461 MiB remained. This is a fixture sizing failure, not a service outage or bypass. The old candidate stayed healthy; cleanup completed. First-attempt logs are retained. Repeating the entire rehearsal with a dedicated 2 GiB filesystem, including explicit proof that the after-admission exhaustion hook completed before proceeding.
Round-six real staging rehearsal: PASS. All 12 rows below ran on 8by64-compute01 using real rootless Podman, real user systemd, the installed Quadlet generator and the real app. Existing A/B images and the original unlabelled image were reused; this script-only change requires no app rebuild. Normal deploy/rollback used the installed next service. Bootstrap and ENOSPC used a separate disposable real app and a bounded dedicated 2 GiB ext4 image store; no shared filesystem was filled.
Both full-disk rows verified zero available blocks with the original MainPID continuously alive. The after-admission hook separately recorded zero blocks before image-load/tag/preparation could complete. Failed candidate health restored the exact unlabelled digest and original unit bytes. Explicit legacy rollback verified status and digest; normal deploy subsequently removed the legacy exception. Staging finishes at A with rollback B and byte-identical original installed unit; mount roots match entry. Disposable service/store/data were cleaned. Per-run logs and rehearsal source are retained untracked and will be attached with the final gate output.
Full gate now running through one local build slot, with three injection workers. No production contact, merge, or production deploy.
Additional finding during this fix's verification: the first absence-handling implementation conflated an image-inspection error with an absent rollback tag. A targeted injection at the rollback image inspection, followed by candidate health failure, reproduced old-image recovery with the older predecessor erased to
-. Evidence: .job-evidence/round6/predecessor-red.log. This did not affect the earlier real staging rehearsal, which had no runtime inspection errors.I stopped only this job's recorded timeout PID 1452960 after verifying its exact command and owner. The partial sweep is retained with exit 143; it is not a full gate pass. No other process was signalled.
Fix: distinguish Podman's explicit image-exists result 1 (absent) from result 0 (present, inspection must succeed) and runtime errors (abort before switch). This keeps real absence supported while preserving an existing predecessor on errors. Targeted before/after injections and explicit runtime/inspection error tests are being run, then the full real rehearsal and gate will be repeated from the start.
After correcting the predecessor inspection/absence edge case, the entire real staging rehearsal was repeated from the start and all 12 rows passed again. Actual Podman image-exists results were explicitly checked: present image = 0, absent image = 1. Both zero-block bootstrap cases keep the old MainPID unchanged; failed bootstrap health recovers the exact unlabelled image; legacy rollback, normal deploy, same-SHA and normal rollback all pass. Staging again returns to A with rollback B, original installed unit bytes and unchanged mount identities. Disposable resources are removed.
Targeted predecessor existence and inspection failures before/after each command now preserve the older rollback identity through failed health. A fresh full gate is running from the beginning. The prior interrupted partial sweep remains recorded as exit 143 and is not claimed as a pass. Final evidence will include both the reproduced edge case and its passing tests, plus every rehearsal run log.
Resuming after host reboot on fix/deploy-688-round5, base/head
768dbacbb6. The coordinator already pushed the WIP to PR #748; this task explicitly requires updates only to that branch and PR, overriding the earlier generic new-branch instruction. Rechecking staging, runner and fixtures; rerunning the interrupted full gate and real staging rehearsals. No production access or merge. Evidence remains untracked.Post-reboot evidence on 8by64-compute01: boot ID 3e68dc6c-c253-49bd-a643-7dc507439df6. calternal-next.service active under user systemd; container running image sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb. Health revision
8dc52337a5, status ok. Saved pointer current matches that image; rollback sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108. Both forgejo-runner.service and forgejo-runner-web.service active since 06:43:49 CET with NRestarts=0. No leftover disposable round6/round6-extra stores. A/B archives and bundle fixtures remain present. Root fixture access works. Previous gate stopped without gate.exit at bootstrap-health-fails after-command 80/106; not a completed gate. Additional real regression evidence directory contains fixture symlinks only; those rehearsals have not run. Rerunning all gates and real rehearsals through one build slot, without changing production or filling a shared filesystem.Fresh post-reboot bootstrap rehearsal: all 12 rows PASS, command exit 0. Real next deploy, same-SHA with same MainPID, rollback, unlabelled bootstrap, legacy rollback, failed bootstrap health recovery, normal post-bootstrap deploy/same-SHA/rollback and final restoration passed. Both full-disk bootstrap cases (before admission and after admission before image load) observed zero available blocks and the same original service MainPID. Staging returns to A/rollback B, byte-identical installed unit and unchanged mount roots. Per-run logs are retained untracked and will be attached with final gates. Additional real regression rehearsal is running, then the interrupted exhaustive gate will rerun from its start.
{"case": "real staging normal deploy", "outcome": "PASS", "current": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "rollback": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "revision": "
5b0dd8d009", "mount_roots": "unchanged"}{"case": "real staging same SHA, same MainPID", "outcome": "PASS", "current": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "rollback": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "revision": "
5b0dd8d009", "mount_roots": "unchanged"}{"case": "real staging rollback", "outcome": "PASS", "current": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "rollback": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "revision": "
8dc52337a5", "mount_roots": "unchanged"}real Podman image-exists present=0 absent=1: PASS
{"case": "bootstrap full filesystem admission", "outcome": "PASS", "current": "sha256:983a8a530cb4330dadeb8c3f125ea2cfec9b242ae21c571d4fc6e964b5a1e17d", "old_service": "same MainPID", "available_blocks": 0}
{"case": "bootstrap full filesystem after admission", "outcome": "PASS", "current": "sha256:983a8a530cb4330dadeb8c3f125ea2cfec9b242ae21c571d4fc6e964b5a1e17d", "old_service": "same MainPID", "available_blocks": 0}
{"case": "bootstrap candidate failure recovers unlabelled image", "outcome": "PASS", "current": "sha256:983a8a530cb4330dadeb8c3f125ea2cfec9b242ae21c571d4fc6e964b5a1e17d", "rollback": "-", "unit_bytes": "original"}
{"case": "bootstrap from unlabelled running image", "outcome": "PASS", "current": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "rollback": "sha256:983a8a530cb4330dadeb8c3f125ea2cfec9b242ae21c571d4fc6e964b5a1e17d", "revision": "
8dc52337a5"}{"case": "rollback to recorded legacy image", "outcome": "PASS", "current": "sha256:983a8a530cb4330dadeb8c3f125ea2cfec9b242ae21c571d4fc6e964b5a1e17d", "rollback": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "health": "status and exact digest"}
{"case": "normal deploy after bootstrap removes legacy exception", "outcome": "PASS", "current": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "rollback": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "revision": "
5b0dd8d009"}{"case": "same SHA after bootstrap", "outcome": "PASS", "current": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "rollback": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "old_service": "same MainPID"}
{"case": "normal rollback after bootstrap", "outcome": "PASS", "current": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "rollback": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "revision": "
8dc52337a5"}{"case": "final staging state", "outcome": "PASS", "current": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "rollback": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "revision": "
8dc52337a5", "mount_roots": "unchanged"}round six real staging rehearsal: PASS
Fresh additional real staging rehearsal: 13 rows PASS, command exit 0. Together with the bootstrap rehearsal, all 25 rows now pass after reboot. Persistent ENOSPC remained at zero blocks at prepared, candidate_started and recovery_health; the filler stayed until recovery was verified. Real invalid Quadlet output loss recovered via the precompiled static unit. Terminating only this job's recorded loopback SSH client after stop did not abort detached failed-health recovery; result 001 was persisted and the original A image/unit with rollback B restored. Fixtures cleaned. Full gate has restarted from the beginning in one local build slot. Evidence below; final archive and verbatim gates follow on completion.
{"case": "real staging deploy", "outcome": "PASS", "current": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "rollback": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "revision": "
5b0dd8d009", "mounts_and_data_roots": "unchanged"}{"case": "real staging same SHA", "outcome": "PASS", "current": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "rollback": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "revision": "
5b0dd8d009", "mounts_and_data_roots": "unchanged"}{"case": "real staging rollback", "outcome": "PASS", "current": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "rollback": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "revision": "
8dc52337a5", "mounts_and_data_roots": "unchanged"}{"case": "real full receipt filesystem cleans partial upload", "outcome": "PASS", "current": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "rollback": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "revision": "
8dc52337a5", "mounts_and_data_roots": "unchanged"}{"case": "real staging invalid generator input", "outcome": "PASS", "current": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "rollback": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "revision": "
8dc52337a5", "mounts_and_data_roots": "unchanged"}{"case": "real staging failed candidate recovery", "outcome": "PASS", "current": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "rollback": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "revision": "
8dc52337a5", "mounts_and_data_roots": "unchanged"}{"case": "isolated store paths verified", "outcome": "PASS", "graph_root": "/home/calternal/.deploy-round6-extra/graph", "run_root": "/run/user/30033/calternal-round6-extra-run"}
{"case": "isolated real Podman deploy and rollback", "outcome": "PASS", "current": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "rollback": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "store": "dedicated ext4"}
{"case": "persistent full image store through recovery", "outcome": "PASS", "current": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "rollback": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "zero_free_blocks_at": ["candidate_started", "prepared", "recovery_health"], "filler": "retained until recovery verified"}
{"case": "free-space margin refuses next deploy", "outcome": "PASS", "current": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "rollback": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "old_service": "same MainPID throughout", "available_bytes": 0}
{"case": "final real staging check", "outcome": "PASS", "current": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "rollback": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "revision": "
8dc52337a5", "mounts_and_data_roots": "unchanged"}real staging rehearsal: PASS
{"case": "real generator output loss and static fallback", "outcome": "PASS", "current": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "rollback": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "revision": "
8dc52337a5", "details": "Actual invalid Quadlet removed primary generator output; a precompiled static unit served A. Original unit and primary A restored under the deploy lock."}{"case": "real SSH disconnect mid-switch", "outcome": "PASS", "current": "sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb", "rollback": "sha256:26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108", "revision": "
8dc52337a5", "details": "Killed the recorded loopback SSH client after the old service stopped; released candidate into failed health. PID 1 completed A recovery and persisted result 001 with original unit bytes and rollback B."}real generator-loss and SSH-disconnect rehearsal: PASS
Resuming round six on fix/deploy-688-round5 at base/head
768dbacbb6. Previous saved real staging and extra regression rehearsals exited 0 after reboot; exhaustive gate has no exit marker and is incomplete. Rechecking services and fixtures, then rerunning gates from the beginning under one build slot. The explicit job instruction to push only this branch and rehearse staging takes precedence over the general new-branch/no-deploy boilerplate. No production access or merge.Current resumption health check: calternal-next.service is active and its running digest is 60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb. /healthz returns {"revision":"8dc52337a566158e86b0234bcfb082331d76d1cd","status":"ok","version":"0.1.0"}. The private mode-0600 image record agrees and preserves rollback 26ec8a7decf4c161acbabf8c99ebb2ba868075dcd4697fe08df356295aab8108. Both forgejo-runner.service and forgejo-runner-web.service are active since 06:43:49 CET with NRestarts=0. There are no failed system units or leftover round6 disposable stores.
The saved post-reboot real staging rehearsals have both exit markers 0 and all 25 rows PASS. Their results were already posted at comments 69160 and 69169, and remain valid on the unchanged
768dbacbhead and the same boot. No staging rerun is necessary. The interrupted exhaustive gate has no completed result; a fresh full run is now active in one build slot, with output kept separately from the interrupted log. No product edits are needed from this resumption's diff review so far.Evidence audit found the complete pre-reboot log still available on unchanged product/test source, with explicit PASS positions through bootstrap-health-fails after 80/106. Those results were not lost. To follow the instruction to resume completed work, stopped only the current resumption's recorded timeout PID 534890, verified by /proc command and owner. Its exit 143 is retained and is not a passing gate.
The remaining gate uses an untracked resume harness. It reads the original matrix list from the committed test's AST, runs each baseline again, checks retained step counts, and calls the original inject function on every position without an explicit retained PASS. It keeps the original three-worker maximum and one build slot. Syntax, shellcheck, Quadlet, transport, ordinary transaction, bootstrap, relay, artifact and dry-run gates run fresh. Final evidence will state retained and fresh counts, include both original logs and resume source, and will not describe this as an uninterrupted full rerun. All 25 real staging rows already passed on this boot and unchanged head.
Round-six verification checkpoint on unchanged head
768dbacbb6: 2802 injection positions are now fully accounted PASS from retained explicit results plus fresh missing positions. Bootstrap health-failure and persistent-full-disk sweeps are complete, as are failed deploy and failed rollback ordinary and persistent-full-disk sweeps. The original matrix continues through stale-primary recovery and post-start unit-rename recovery. No failed test or new product defect has appeared. This is still a running gate, not a final gate pass. All 25 real staging rehearsal rows remain recorded PASS after this boot. No product file changes, production access or merge.Resuming deploy-fix-8 on fix/deploy-688-round5, base/head
768dbacbb6. Work only in /home/agent/wt/deploy-fix. The explicit task-specific instruction to push this branch overrides the older generic new-branch instruction. No production contact, merges, or production deployment. Post-reboot staging calternal-next and both CI runners active with zero restarts. Health reports expected8dc52337a5; real running image 60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb. Both runners have active jobs; no failed system units. RAM available 58GiB; root free 124GiB; systemd-oomd active. Product/test source is unchanged from WIP. Retained explicit passing injection positions will be combined with fresh remaining positions; all other gates and real rehearsals will run on this boot, one build slot, maximum three workers. Evidence remains untracked.Verification checkpoint on unchanged head
768dbacbb6: 3498 injection positions are fully accounted PASS from retained explicit results plus fresh positions. Stale-primary rollback recovery now passes every ordinary before/after failure and persistent-full-disk before-command failure; persistent after-command sweep is running. Fresh syntax, shellcheck, Quadlet, transport, transaction and bootstrap checks passed. No new product defect or code change. This is still a running gate, not a final gate pass. Fresh real staging rehearsals follow in the same single-slot sequence.Regression gate completed on unchanged head
768dbacbb6, exit 0. Exhaustive failure injection: 4,354 cases = 3,314 retained explicit passes + 1,040 fresh passes; userdata unchanged. Fresh syntax, shellcheck, all other deploy probes, dry runs and real user-systemd checks pass. Fresh real staging rehearsal is running next in one build slot. Final completion comment will include verbatim gate output and all real rehearsal rows. No code changes, production access or merge.Completed deploy-fix-8 on
fix/deploy-688-round5, head768dbacbb6b25773af560b5e10f69a9fbd88f1da. The WIP commit already contained all four fixes; review and verification found no additional product changes necessary. No new or empty commits were made. Pushed only the requested branch. No production contact, merges, or issue closure.Finding → fix → test:
deploy.sh bootstrapreplaces manual stop-before-tag steps. Verified image, unit and pointer preparation, free-space admission, static recovery and independent runtime reserves finish before stop. Real unlabelled bootstrap, candidate failure, legacy rollback and full disk before/after admission passed. Both bootstrap full-disk cases observed zero available blocks and preserved the exact old MainPID.-; successful update records the old baseline. Runtime/inspection errors refuse before stop. Regression checks verify first update, failed first update, absent rollback refusal and preservation of older predecessors.Post-reboot health was verified before any rehearsal: both staging services (calternal-next and calternal-preview) active, zero restarts; next health and exact image/pointer identities matched expectations. Both CI runners active with zero restarts and running jobs. No failed system or calternal user units. CI slice MemoryMax=32 GiB and agent slice MemoryMax=24 GiB, with oomd enabled. All fresh heavy work used one build slot and no more than three workers. No build was needed.
Verification accounting: unchanged committed product/test source, with SHA-256 manifest attached. Explicit passing injection positions retained from the two interrupted runs were combined with freshly executed missing positions using the original injection function and matrix. Every baseline was rerun and boundary counts checked. This is a completed combined sweep, not an uninterrupted full rerun. Other gates and all 25 real rehearsal rows ran fresh on this boot. Evidence is untracked; no artifacts were committed.
Real staging rehearsal (fresh this boot):
Rehearsals restored original staging unit bytes, image pointers and mount identities. Full disk cases used bounded dedicated filesystems; shared filesystems were never filled. Disposable fixtures were cleaned.
Complete evidence, retained output, resume source, source hashes and per-run logs.
Final health output, verbatim:
Gate output, verbatim:
Gate exit: 0. Real bootstrap/staging rehearsal exit: 0. Additional real regression and SSH-disconnect rehearsal exit: 0. Remote branch and PR head both verified as
768dbacbb6b25773af560b5e10f69a9fbd88f1da.Independent round-seven verification started at
768dbacbb6in fresh worktree /home/agent/wt/review-748g, tests branch review/748g-tests. I will test bootstrap, space failures, interruption/rerun, health recovery, concurrency and documentation independently. Required prior brief ~/codex-jobs/review-748/prompt.md is absent on this host; available round-six fix briefs and PR/issue history were read. No production contact or merge.Round-seven staging access limitation: the public staging health endpoint at http://10.70.3.14:8080/healthz reports status ok, revision
8dc52337a5. This review session runs as agent (uid 1002), not root. sudo -n true and sudo -n -l require a password. NetBird SSH to the configured root@8by64-compute01.vpn.kayg.org endpoint fails with connection refused at 10.69.69.147:22; loopback SSH also refuses. The private credential request timed out. Therefore installed calternal-user services and a real root-mounted limited image store cannot currently be exercised. Isolated review fixtures will use this host's actual rootless Podman/user-systemd where possible, clearly distinguished from the installed service. Production remains untouched.Independent review round 7 — PR #748
Head:
768dbacbb6. Worktree: /home/agent/wt/review-748g.Required previous brief /home/agent/codex-jobs/review-748/prompt.md is absent. Read available deploy-fix-6/7/8 briefs and fetched PR #748 plus issues #688/#733 and comments. No production access.
P1 — SIGKILL after stop strands the service; rerun cannot recover it
Locations: deploy/next/switch.sh:140–153, deploy/next/remote.sh:188–210; README explicitly excludes this at its SIGKILL/manual-recovery paragraph.
Reproducer: run
deploy/tests/staging-review-probe.pyas root on 8by64-compute01 with the existing A/B artifact directory and original unlabelled image ID. Itskilled-after-stopcase adds onlykill -KILL $$immediately after the successfulstop_oldcall in the private transaction copy. It then invokes the unchanged transaction again, against the same service, unit and pointer files. No production connection is used.Evidence: live/results.jsonl records
active_before_rerun=inactive,active_after_rerun=inactive,refused_precheck=true. live/rerun-after-kill.log reportspre-deploy health failed; service was not changedandtransaction failed before switch; old service unchanged. A real systemd detached worker does not rescue its killed transaction child. The prepared recovery files and reservations are stranded; a rerun performs the normal precheck instead of reconciling the interrupted switch. The independent 200-point seeded SIGKILL/rerun sweep is still running and will be appended separately. This violates the requested abrupt-stop/rerun and abort availability contract. The test manually repaired and removed its own disposable service; existing staging remained healthy at revision8dc52337a5.P2 — Exact data rollback is not provided (explicit contract mismatch)
Locations: deploy/next/switch.sh:155–214; deploy/next/README.md code-only rollback sections.
Reproducer: the same real staging test's
data-mutationcase creates a synthetic mounted-data sentinel containingbefore. The candidate unit writesafterin ExecStartPre and listens on the wrong internal HTTP port. Candidate health fails, and the transaction automatically restores the exact prior image, unit bytes and pointer. It does not restore the sentinel. This models a startup migration or other candidate write before health verification; it does not claim the current application performs that specific migration.Evidence: live/results.jsonl records
image_recovered=true,data_restored=false,sentinel=after. live/data-mutation.log confirms automatic old-image recovery. Earlier PR history intentionally chose code-only rollback to avoid replacing live userdata, and the README accurately disclaims data restoration. Under this review's explicit requirement that rollback restore prior image and data exactly, that is an unresolved contract gap, not a claim that the script itself deletes data. Resolve the contract or provide an upgrade/data strategy that meets it; blindly restoring live data would conflict with the earlier safety rationale.P2 — Free-space admission runs after receiving and duplicating the artifact (target.sh:43–47, remote.sh:196–209). Independent
python3 deploy/tests/preflight-review-probe.pyobserves the actual bootstrap at its first disk_preflight call. Five files already exist: bundle.tar=10240 bytes, image.tar=27, image.id=72, image.sha256=76, revision=41; the receiver wrote a separate input copy first. Abort preserves the running process in checked cases, but artifact writes can exhaust the target filesystem before admission. This violates the explicit “free-space preflight runs first” contract. The regression uses real files and the existing isolated service adapters; no production contact.P1 — Actual inode exhaustion strands both primary and fallback recovery. In the independent real Podman/systemd/ext4 rehearsal, after normal preparation and the successful old-service stop, the test creates zero-length files until f_favail reaches zero, then stops creating files. Data blocks remain available. The unchanged switch and EXIT recovery cannot start either service: primary_state=failed, fallback_state=failed, and the transaction reports
recovery failed; inspect service locally; private recovery files retained beside the quadlet. Locations: space.sh:19–36 and switch.sh:133–185. The runtime reserve allocates bytes in two files per root, but releasing them does not secure enough inodes for Podman's metadata. This is one-time ENOSPC after admission, with no continuous writer, SIGKILL or read-only filesystem. Reviewer test: deploy/tests/staging-review-probe.py, inode-exhaustion case; evidence: ~/codex-jobs/review-748g/live3/{inode-events.jsonl,inode-exhaustion.log,results.jsonl}. The disposable service was repaired after removing its own filler; installed staging remained healthy. No production contact.Independent real staging checks completed using this account's existing Docker-group access and a disposable host helper, without changing SSH or sudo configuration. The fixture used actual Podman, user systemd, Quadlet, a 2 GiB ext4 filesystem, saved unlabelled staging image 983a8a530cb4330dadeb8c3f125ea2cfec9b242ae21c571d4fc6e964b5a1e17d, and the existing verified A/B artifacts. The current production image digest was not queried, because production contact is forbidden.
Passed: scripted bootstrap from the unlabelled image; no-predecessor baseline; same-SHA deploy; rollback to exact unlabelled image; real zero-block admission with the same old MainPID; automatic old-image/unit/pointer recovery after candidate health failure; real full-block filesystem retained through recovery; concurrent complete deploy invocations serialized; actual loopback SSH client killed after old-service stop with detached automatic recovery.
Confirmed defects: SIGKILL of the transaction after old-service stop leaves it inactive and normal rerun refuses; one-time inode exhaustion leaves primary and fallback failed despite 1,286,885,376 available bytes; candidate startup data write persists after successful image recovery (the exact-data contract gap). First-preflight ordering is separately reproduced with real files and isolated command adapters.
Disposable units, container, limited disk mount/image, synthetic data, runtime, generated SSH keys and listener were removed. Existing staging health is still ok at
8dc52337a5. No production contact, merge or product-code change. The unmodified deploy gate and seeded 200-point interruption sweep remain running; this is not yet a gate-pass claim.Reviewer tests are pushed only to review/748g-tests, head
0ab7098b2416dd4b4dd62cd8953742af14983238. The author branch remains768dbacbb6b25773af560b5e10f69a9fbd88f1da. Three Python reproducer/rehearsal scripts were committed; no evidence or product changes.The real Podman/systemd sweep completed 200 distinct seeded (path, Bash command line) stop points, each with a confirmed SIGKILL exit 137 and a normal rerun before manual fixture repair. 16 cases left the primary service non-active; 85 reruns returned failure. The separate adapter sweep confirmed 200 SIGKILLs, with 13 stopped after rerun and 91 refused reruns. These are separate sweeps, not reused author results.
The final source rehearsal exited 0 after reproducing both P1s again. Actual inode exhaustion left primary and fallback failed with 1,286,844,416 bytes still free. Same-SHA deploy preserved MainPID, unit and pointer bytes; real concurrent invocations serialized; an actual SSH disconnect after stop recovered the old image. Synthetic mounted data remained changed after failed-candidate image rollback, as recorded in the exact-data contract finding.
Disposable resources and owned controls are removed. Installed staging is still healthy on exact image
sha256:60f680ae778350c332b127e477c3db3590927d4040007f0d9446c4d5d07c57eb, revision8dc52337a566158e86b0234bcfb082331d76d1cd; CI runner active. No production contact or merge.The original, unmodified
bash deploy/tests/gate.shremains running through the rollback/failed-health matrices. This is not a full gate-pass claim. I will post its exit status and complete output verbatim when it finishes. Verdict remains fix first, with two P1s and two P2 contract/order findings already recorded above and in~/codex-jobs/review-748g/findings.md.Verdict: fix first. Independent round-seven review of
768dbacbb6b25773af560b5e10f69a9fbd88f1da(PR #748).staging-review-probe.py,inode-exhaustioncase. Evidence: final rehearsal inode-events.jsonl, inode-exhaustion.log and results.jsonl.stop_old: the service stays inactive, and a normal rerun refuses at pre-health instead of reconciling prepared recovery. Reproducer: staging probe's killed-after-stop/rerun-after-kill cases and its seeded real-runtime interruption sweep. The separate adapter sweep completed 200 confirmed SIGKILLs: 13 remained stopped after rerun; 91 reruns refused. The independent real Podman/systemd sweep also completed 200 distinct seeded stop points, all confirmed SIGKILL exit 137: 16/200 left the primary non-active after rerun; 85/200 reruns returned nonzero.python3 deploy/tests/preflight-review-probe.pyobserves the real file writes before admission (10240-byte bundle plus extracted image/metadata for its small fixture). This fails the explicit preflight-first requirement, although checked early aborts keep the process serving.The README bootstrap invocation matches the script. Its SIGKILL/manual-repair and code-only-data limits match implementation; those are explicit differences from the requested contract.
Real rehearsal: saved unlabelled staging image → labelled A; exact legacy rollback; same-SHA no-op; full-block admission and exhaustion immediately after admission with unchanged old MainPID; failed candidate health; full-block image-store exhaustion retained through recovery; concurrent complete deploy invocations; and an actual SSH client killed after old-service stop. Real Podman, Quadlet and systemd were used on 8by64-compute01, with a unique disposable service and private 2 GiB ext4 storage. The synthetic unit uses a one-second stop timeout and Restart=no to bound repeated trials; the existing next service was not switched. Shared filesystems were not filled. The current production digest was not queried. Exhaustive per-command full-disk/failure coverage below is the repository's adapter matrix; physical exhaustion checks cover the explicitly listed boundaries, including the newly discovered inode failure.
Reviewer tests only:
review/748g-tests, head0ab7098b2416dd4b4dd62cd8953742af14983238. No product changes, production contact, merge or issue closure. Detailed reproducers and evidence are infindings.mdinside the attached archive. Evidence archive; SHA-256fddaf3fb013428e1f19fb34d659d6bbfde9ccf56a60d3ec3b162055991f66531. Test sources.Cleanup: disposable service, fallback, container, mounted disk, synthetic data, runtime, owned controls and generated loopback SSH credentials/listener removed. Existing staging healthy at
8dc52337a566158e86b0234bcfb082331d76d1cd; CI runner active.Gate command:
bash deploy/tests/gate.sh. Exit 0. Output verbatim:The simpler deploy design is ready in PR #765 — Plan: simpler production deploy (#684), into dev from docs/deploy-plan.
The three-page document covers the single commit point, recovery before pre-health, the failure matrix and all seven rounds of findings, unlabelled bootstrap, precise state rollback, operator commands, acceptance/rehearsal tests, deletion estimates and a five-risk second pass. Failed trials restore stopped app state; later rollback preserves acknowledged edits. It uses
768dbacband the reviewer sources as reference only.Documentation only. Whitespace/coverage checks and the three-page print check pass; no builds, deploys, staging mutation or production contact.
Closing per #931: deploy now uses the simple script in #786.
Pull request closed