docs(concurrency): #585 ADR-0010 3-guard record + dogfood release.yml block #594
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!594
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/585-concurrency-guard-record"
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?
What + why
Discharges #585 (Phase 7 concurrency-guard). Framing-verify result: the tracker framed this as an open architecture call ("(A) flock vs (B) Forgejo concurrency block, decide the composition"). The substrate says otherwise — the concurrency architecture is already decided, documented, and implemented. The mechanisms are not competing alternatives for one class of overlap; they are three orthogonal guards for three different classes, all already present. The residual was a missing consolidated record + a dogfooding gap, not a decision.
The three orthogonal guards (each with a named VOID condition)
Per the "N independent layers, each sufficient alone → name where each is VOID" discipline:
concurrency: cancel-in-progress: false(caller)concurrency:release-decide.shorphan-detector (internal/decide)flock→ErrConcurrentCutrtprocesses, same workspacePREVENT and REENTRANCY don't overlap (workflow-level vs process-level); neither is redundant. RECOVER exists because PREVENT's VOID condition is real and unobservable from inside PREVENT.
What this PR does
release.yml(+14): add theconcurrency: {group: release-cut-${{ github.ref }}, cancel-in-progress: false}block the toolkit recommends to adopters (integration.md:350) but did not run on its own repo — closing the dogfooding gap (a doc-honesty-vs-substrate credibility issue). Additive-dormant: declares intent; the empirical proof that Forgejo honourscancel-in-progress: falseis deferred to #584 (needs the live runner). Coexists with the #139paths-ignore(trigger-time filter vs run-time serialiser — different sub-cases).Reachability call (the #562-shape constraint)
The PREVENT block must live in the caller (
release.yml), not theworkflow_callreusable — that's the documented adopter placement. The toolkit ships the recommendation + reusable; each consumer carries the block in their own caller.AC mapping
TestNewCutter_concurrentCut); CI-cancellation DEFERRED to #584 — whether Forgejo queues the second run is a live-runner property no unit/dry-run test can reach. Substrate-honest split.concurrencyis workflow-level, not the C3 inputs/outputs API surface.ErrConcurrentCutfires): DONE —TestNewCutter_concurrentCut+lock_test.go.What this PR does NOT do
Gate
release.ymlvalidated as YAML (concurrencyblock parses correctly). register-check clean on all changed files (0 drift hits). Full Go suite (19 pkgs)ok,go vet/golangci-lint(0 issues) — confirming the no-Go-delta change breaks nothing, incl. the C3internal/contractsworkflow-API test. No bash touched (shellcheck n/a); no changelog fragment (nothing the toolkit ships to adopters changed — consistent with #562).Refs #585 #584 #417 #554 #499
Framing-verify found the concurrency guard is NOT an open architecture call (as the tracker framed it) — it is already decided, documented, and implemented. The residual was a missing consolidated record + a dogfooding gap, not a decision. - ADR-0010: consolidate the concurrency story as three ORTHOGONAL guards, each with a named VOID condition (the "N independent layers — name where each is VOID" discipline): PREVENT (Forgejo concurrency block, VOID if the provider ignores it), RECOVER (release-decide.sh orphan-detector, the backstop for PREVENT being VOID), REENTRANCY (Cutter flock, VOID cross-workspace). Names the empirical status of each + the placement/reachability call (block lives in the caller, not the reusable). - release.yml: add the `concurrency: {group: release-cut-${{github.ref}}, cancel-in-progress: false}` block the toolkit recommends to adopters (integration.md) but did not run itself — closing the dogfooding gap. Additive-dormant: declares intent; empirical-that-Forgejo-honours-it is #584. Coexists with the #139 paths-ignore (trigger-time filter vs run-time serialiser — different sub-cases). - arc42 §6.8: cross-ref ADR-0010 + name the third (flock/reentrancy) guard the runtime view previously omitted. AC mapping: #1 already-decided (3 orthogonal guards, not a composition choice); #2 documented -> consolidated here; #3 flock empirical DONE (TestNewCutter_concurrentCut), CI-cancellation empirical DEFERRED to #584 (needs the live runner); #4 n/a (concurrency is workflow-level, not the C3 API surface); #5 flock DONE. No Go delta; no runtime behavior change. Refs #585 #584 #417 #554 #499Review — PR#594, concurrency-guard composition record (#585, Phase 7)
Independent verify at head
219ff07(basemain@ddb4fc9, in sync). Docs + dogfood, no Go delta: ADR-0010 (new), arelease.ymlconcurrency block, an arc42 §6.8 cross-ref. The headline is a framing-verify result — the tracker framed an open flock-vs-block decision, and the substrate shows it's already decided as three orthogonal guards. I verified every guard's VOID condition at source, the empirical split, the §6.7 self-catch fix, and the dogfood's validity + coexistence.No findings. The framing-verify is correct, and each layer's VOID condition is named at the point of use — the #242 lesson applied rather than repeated.
The 3-orthogonal-guards claim — each VOID condition verified at source
concurrencyblock on the callerconcurrency:internal/decide+ release-decide.sh)TestDecide_417OrphanFatalpresent (decide_test.go:665)flock→ErrConcurrentCutcutLockPath(lock.go:20-36) hashes the symlink-resolved absolute repo root intoos.TempDir()/rt-cut-<hash>.lock— different checkout ⇒ different hash ⇒ different lock (does not serialise); same root ⇒ same lock (serialises).TestNewCutter_concurrentCutassertserrors.Is(err, ErrConcurrentCut)(cutter_test.go:319-331).Orthogonality is genuine: PREVENT operates at workflow-instance level (VOID for same-workspace reentrancy); REENTRANCY operates at workspace level (VOID for cross-workflow-instance overlap — separate lock files). Neither covers the other's class, so neither is redundant, and RECOVER exists because PREVENT's VOID condition is real and unobservable from inside PREVENT. The claim holds at source.
AC#3 substrate-honest split — the deferral is a verified falsifiable record
The #585 AC "empirical verification: overlapping cut attempts correctly rejected" is split: flock is verified (
TestNewCutter_concurrentCut); the CI-cancellation half is deferred to #584. I confirmed that half is hermetically unreachable — whether Forgejo honourscancel-in-progress: falseis a live-orchestrator property; no unit test or dry-run oracle executesconcurrency:(it's interpreted by the CI provider, not by any script/binary in the repo). Same shape as #562's dry-run-unreachable-PATCH: the branch a hermetic test can reach is verified now; the one needing a live orchestrator is deferred to the tracker that owns that surface. The ADR restates the deferred half as an action AC pointing at #584 — the honest handling.§6.7 self-catch fix — correct
§6.7 is indeed "Two runtime axes that are NOT the α/γ split" (06-runtime-view.md:163) — it does not cover flock. The arc42 cross-ref now cites
internal/release/lock.gofor the flock layer, not §6.7. The fix points at the right substrate.The dogfood — valid, coexists, correctly placed
release.ymlis valid YAML; the block parses to{group: release-cut-${{ github.ref }}, cancel-in-progress: false}— matching the ADR, andgithub.refgroups both push:main and dispatch. The #139paths-ignore: ['.release-toolkit-manifest.json']is still present (a different top-level key: trigger-time filter vs run-time serialiser — they cannot conflict, exactly as the ADR claims).release.ymluses:the reusable (line 70), so it is the caller — the placement the ADR specifies (block belongs in the caller, not the reusable). ADR cross-ref links (0004, 0007) resolve.Register-check — clean, and I verified why
ADR-0010 line 45 contains
substrate-honest, a built-in register-check pattern — the kind of thing that could red the gate. Verified it's clean rather than assuming:rt register-check docs/adr/→ 0 hits with the allowlist active, because.register-allowlistlistsdocs/adr/by design (ADRs record decisions at the time of the decision; retroactive genericising is wrong). The arc42 file and release.yml are clean by absence. CI's two register-check contexts are green.Verdict
APPROVED, head-pinned at
219ff07. The framing-verify is correct (an already-decided composition, not an open call), each of the three guards' VOID conditions is verified at source and named at point of use, the flock cross-workspace crux holds incutLockPath, the CI-cancellation deferral to #584 is a sound falsifiable record (hermetically unreachable, #562-shape), the §6.7 self-catch fix cites the right substrate, and the dogfood is valid YAML that coexists with #139 and sits in the correct (caller) workflow. Full Go suite green (19 pkgs — no Go delta breaks nothing, C3 unaffected), register-check clean (ADR allowlisted by design), CI 8/8. The CI-image-Go skip is proportionate for a no-Go-delta change. A clean consolidation record. Yours to land.— Surveyor