[BE-0061] feat(roadmap): make BE-ID allocation collision-proof#175
Merged
Conversation
Two open PRs allocating in the same window could still take the same BE id — a BE-XXXX placeholder carries no digits, so ROADMAP_RESERVED_IDS can't see a not-yet-allocated number — and a number contested only between open PRs (none merged) had no arbiter, since repair's authority was origin/main alone. The three-way BE-0056 collision (#166/#169/#170) was exactly this failure. Prevention: allocation now claims each id atomically as a refs/be-claims/<NNNN> git ref via GitHub's create-ref API (a compare-and-set — 422 if the ref exists), so two branches cannot both take a number; the loser re-picks. Claims are released when a PR closes and swept daily (roadmap-claims-gc.yml). Backstop: allocate_roadmap_ids.py --repair generalizes its authority from "origin/main only" to "main first, else the lowest open-PR number" (ROADMAP_LOWER_PR_IDS, computed from open_pr_be_map.sh), and roadmap-id-repair runs on a daily schedule as well as on push-to-main. The Python script stays pure (no GitHub coupling); the create-ref calls and the retry loop live in the workflows. Filed born-implemented as BE-0060, a sibling to BE-0043. Adds scripts/be_claims.sh + scripts/open_pr_be_map.sh, extends the allocator tests (lower-PR collision detection + tiebreaker), and updates docs/ai-development.md (+ja). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR #173 (run-report-zip-export) also holds BE-0060 and is the lower PR number, so it is the authority and keeps it; this item (the higher PR, #175) moves to the next free id. BE-0061 is free on main and across open PRs. Renamed the directory and files, rewrote the self-references and the docs/ai-development.md (+ja) link, and regenerated the index. The BE-0060 left in tests/test_allocate_roadmap_ids.py is a synthetic next-free value in a fixture, not a reference to this item. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two open PRs allocating a BE id in the same window could still take the same number, and a number contested only between open PRs (none merged) had no arbiter — the three-way
BE-0056collision (#166 / #169 / #170) was exactly this failure. This adds two layers, filed born-implemented as BE-0061 (a sibling to BE-0043).Prevention — atomic reservation. Allocation claims each id as a
refs/be-claims/<NNNN>git ref through GitHub's create-ref API (a compare-and-set — HTTP 422 if the ref already exists), so two branches cannot both take a number; the loser re-picks. Claims are released when a PR closes and swept daily (roadmap-claims-gc.yml).Backstop — generalized auto-repair.
allocate_roadmap_ids.py --repairgeneralizes its authority from "origin/main only" to "main first, else the lowest open-PR number" (ROADMAP_LOWER_PR_IDS, computed fromopen_pr_be_map.sh), androadmap-id-repairruns on a daily schedule as well as on push-to-main. The Python script stays pure (no GitHub coupling); the create-ref calls and retry loop live in the workflows.No tool behavior, runtime, or scenario semantics change; the deterministic gate is untouched.
Changes
scripts/allocate_roadmap_ids.py— repair authority generalized (lower_pr_idsvia env), docstrings;tests/test_allocate_roadmap_ids.py+5 (lower-PR detection, tiebreaker, e2e renumber, parsing).scripts/be_claims.sh(claims ledger: list/claim/release) andscripts/open_pr_be_map.sh(PR→id map for the tiebreaker)..github/workflows/:roadmap-id.yml(claim + retry loop),roadmap-id-repair.yml(daily schedule + lower-PR tiebreaker + claim new ids), newroadmap-claims-gc.yml(release on PR close + daily orphan sweep).BE-0061item (en + ja),docs/ai-development.md(en + ja), Makefile (shellcheck list + repair comment), regenerated index.Test plan
make checkgreen locally: 1020 passed, coverage 89.81%; shellcheck + actionlint clean; no index drift.Notes
SHELL_SCRIPTSline in the Makefile (and [BE-0067] ci: harden the code-quality gate (CI fidelity, security lint, supply-chain) #170 also addsscriptstotypecheck). Union-resolve whichever merges second.🤖 Generated with Claude Code