Commit Graph
3 Commits
Author SHA1 Message Date
Nick b5c9a3c04e feat(release): in-critical-section convergence hook — the WINNER refreshes release history atomically with its promotion (SCAFFOLD-TRANCHE-0012 §12C)
CI Pipeline / best-practices (push) Successful in 11s
CI Pipeline / canary (push) Successful in 35s
CI Pipeline / lint (push) Successful in 47s
CI Pipeline / documentation (push) Successful in 5s
Auto-version and Release / release (push) Successful in 1m7s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / type-check (push) Successful in 32s
CI Pipeline / unit-tests (push) Successful in 1m51s
CI Pipeline / required-checks (push) Successful in 2s
deploy / deploy (push) Failing after 4m35s
§12C design insight from the 0.66.51 deploy (run 7721): the workflow-step
refresh fired (steps.promotion_truth gate worked) but the authority
readback disagreed — an overlapping deploy had superseded the candidate
between promotion-check and refresh-step. Correct refusal, but the WINNER
never converges because its own refresh can hit the same race.

DEPLOY_PROMOTION.PROMOTE now accepts on_promoted: invoked INSIDE the flock
critical section immediately after the symlink swap — only the promotion
WINNER converges, atomically with the swap. Failure degrades to STALE
(software valid, history lags, retry converges). deploy_stage passes a
convergence hook that builds evidence-derived notes and publishes to Gitea.

Tests: 4 new (hook fires on promotion, NOT fired for stale, failure
degrades not crashes, A→B race winner-converges-inside-lock); suite 1687
green; race tests all green.
2026-09-14 06:26:24 +01:00
Nick f765365d03 fix(release): ancestry check fetches canonical branch on divergent classification
CI Pipeline / best-practices (push) Successful in 7s
CI Pipeline / documentation (push) Successful in 4s
Auto-version and Release / release (push) Successful in 29s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / lint (push) Successful in 42s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 17s
CI Pipeline / unit-tests (push) Successful in 1m8s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 2m59s
Observed live on deploy 7449 (SHA 535c2909): a delayed older candidate's
checkout cannot contain commits newer than itself — its local object
store lacks the deployed commit entirely, so the ancestry classification
returned 'divergent' and fail-closed refused. Correct refusal, wrong
reason: the candidate IS an ancestor of the deployed build.

Repair: on divergent classification, fetch origin's canonical branch
(best-effort, credentials already configured in the workflow checkout)
and re-classify. Still divergent afterwards = genuine divergence, refusal
stands. Regression test reproduces the 7449 shape with a depth-1
checkout that genuinely lacks the deployed commit's objects.

Release-path invariant unchanged: unknown/unprovable ordering still
refuses; the fetch only lets the checker SEE the newer reality before
refusing.
2026-09-13 14:01:04 +01:00
Nick 600008482d fix(release): monotonic promotion — stale deploy candidates fenced at promotion time (DEPLOY-MONOTONIC-001)
CI Pipeline / best-practices (push) Successful in 8s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / canary (push) Successful in 32s
Auto-version and Release / release (push) Successful in 31s
CI Pipeline / lint (push) Successful in 43s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / gitea-offline (push) Successful in 17s
CI Pipeline / unit-tests (push) Successful in 1m7s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 2m58s
Root cause of the 2026-09-13 deploy-order race (deploys 7355/7359):
deploy_stage.py promoted unconditionally in job-finish order — no
comparison against the currently-deployed commit, no lock, unconditional
PROMOTED claim. Two overlapping deploys (tranche push + auto-bump push)
could finish out of order and move current BACKWARD.

Repair (governed through the Tranche 4 live Scaffolding path — decision
trace + contract DEPLOY-MONOTONIC-001 durably saved BEFORE edits):

- deploy_promotion.py: promotion authority with GIT ANCESTRY ordering
  (canonical release branch; Rune's flow is linear master history):
  same -> NOOP_ALREADY_CURRENT (idempotent), descendant -> PROMOTED,
  ancestor -> STALE_SUPERSEDED (truthful non-error refusal), divergent
  or unknown -> fail closed. flock'd critical section: the ancestry
  check against the commit current ACTUALLY points to, the last-good
  refresh, and the symlink swap happen atomically — no check/swap
  interleaving window. Symlink swaps are os.replace(2) over temp links:
  current is never absent, never half-updated (crash-safe).
- deploy_stage.py: promotion routed through the authority; truthful
  outcome reporting — STALE_SUPERSEDED prints no PROMOTED claim,
  mutates nothing, exits 0 (the newer build legitimately won);
  divergent/unknown exit 1 (fail closed).
- deploy.yml: promotion truthfulness check reads the deployment
  authority (current symlink) after staging; runtime verification only
  runs for real promotions — build/stage success != promotion success.

Test matrix (brief §20 A-M): 21 tests — the exact race shape with B
promoting then A fenced, forward order, same-candidate idempotence,
retry-after-promotion last-good integrity, stale mutation-freedom
(current AND last-good untouched), divergent orphan-branch fail-closed,
qualification gate regression, stage/promote distinction, promoted-
outcome proven from deployment authority, auto-bump overlap shape,
crash-safety coherence across mixed outcomes, red-candidate refusal
regression, green forward deploy, concurrent-thread serialization, and
the §21 LIVE two-process race: old process paused at its promotion
barrier, new promotes, old resumes and is fenced — current remains the
newer build, deterministic via file barrier (no sleeps).

Suite 1497 green; henxels rc=0; pyright clean.
2026-09-13 12:51:57 +01:00