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
§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. - 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. - The swap (
rm -rf docs && mv <stage> docs) MUST run under a remoteflockon~/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). - 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 shareddocs.newname on its first post-fix ship. - 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)
bash -n ship.shPASS; both workflows YAML-parse post-edit. PASS- Two-racer simulation (exact remote command shape, tiny payloads, shared console dir): both exit 0, final
docs= last swap's content, no cannot-stat. PASS (pre-fix design loses one racer by construction - observed live in #127) - Sweep semantics: pre-seeded stale
docs.new(3h) swept; fresh foreigndocs.new.other.0survived. PASS flockpresent in util-linux (verified in the ubuntu sandbox; the VPS is ubuntu - if ever absent, the step fails loud at "command not found", not silently).- Testing pass 2026-07-13 (post gate-1 "approve all"): two-racer simulation re-run green (both exit 0, last swap wins, sweep semantics intact); ship.sh bash -n + both workflows YAML re-verified.
§9
- The docs-absent window during the swap (sub-second, now serialized) is accepted, as it was pre-fix. A symlink-flip scheme (docs -> release dir, atomic
ln -sfn) would remove it entirely; adopt only if a monitoring blip ever attributes to it. - deploy.yml's docs job and release.yml's docs job still both exist by design (TASK-DOCS-003: tags refresh the site too); deduplicating the JOBS is out of scope - this task dedupes the shipper.
§10
| Failure | Detection | Recovery |
|---|---|---|
| two shippers race (the #127 event) | none needed - per-run staging + flock make it a supported case | last swap wins; both green |
| shipper killed mid-extract | orphan staging dir on the VPS | swept by the next ship's 2h sweep |
| flock missing on a future VPS image | step fails loud: command not found | install util-linux / adjust script |
| sweep window vs a >2h extract | impossible in practice (18M extracts in seconds); would surface as a swept-staging mv failure | raise -mmin threshold |
| release.yml tag ref predates ship.sh | docs 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.