Task — engineering-spec@1

"Docs-ship race — shared staging dir between deploy.yml and release.yml docs jobs; fixed by a single ship script with per-run staging + flock'd swap"

doneTASK-IMP-079
module improvement · class product · priority p0 · created 2026-07-13 · shipped 2026-07-13
depends on none · blocks none

§1

  1. Both docs jobs MUST ship through one implementation, tools/docs-site/ship.sh - the duplicated inline snippet is how the shared-staging race was born, and TASK-IMP-074's rules-to-channels principle applies to the shippers themselves.
  2. Staging MUST be per-run unique (docs.new.${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}, local$$ fallback): no shipper can ever name, and therefore never delete, another's in-flight staging.
  3. The swap (rm -rf docs && mv <stage> docs) MUST run under a remote flock on ~/cyberos/apps/console/.docs-ship.lock: swaps serialize, the docs-absent window stays single-threaded and sub-second, last writer wins (all writers build the same main, so content is equivalent).
  4. Abandoned staging dirs MUST be swept without endangering live ones: the flock'd section removes docs.new* entries untouched for 2h+ only - an in-flight extract continuously refreshes its dir mtime and completes in seconds. This also retires the legacy shared docs.new name on its first post-fix ship.
  5. Transport invariants inherited unchanged: tar streamed over ssh (no runner tgz, no VPS /tmp - the scp era shipped a truncated archive), set -euo pipefail, size echo for the deploy log.

Lean profile: one script + two call-site swaps; the race, the fix, and the sweep semantics are all machine-verified in-session by a two-racer simulation of the exact remote command shape.

§5 (run 2026-07-13)

§9

§10

FailureDetectionRecovery
two shippers race (the #127 event)none needed - per-run staging + flock make it a supported caselast swap wins; both green
shipper killed mid-extractorphan staging dir on the VPSswept by the next ship's 2h sweep
flock missing on a future VPS imagestep fails loud: command not foundinstall util-linux / adjust script
sweep window vs a >2h extractimpossible in practice (18M extracts in seconds); would surface as a swept-staging mv failureraise -mmin threshold
release.yml tag ref predates ship.shdocs job fails loud at "no such file"re-tag current main (the standard sequence)

End of TASK-IMP-079.

Audit

  • ISS-001: draft blamed deploy.sh (the concurrent green job) without evidence - resolved: deploy.sh audited line by line (no git clean, no console-tree rm); the actual second writer is release.yml's docs job, confirmed at its line 442 with the identical staging path.
  • ISS-002: draft swept ALL foreign docs.new.* during the swap - reintroduces the race it fixes (a live concurrent extract would be deleted). Resolved: sweep gated on -mmin +120; simulation asserts a fresh foreign staging survives.
  • ISS-003: draft patched both inline snippets in place - resolved: single shared script per TASK-IMP-074's one-implementation principle; the duplication itself was the root enabler. Score = 10/10.