PREQ-7781 Claim build numbers atomically via Git refs instead of verify-and-retry - #336
PREQ-7781 Claim build numbers atomically via Git refs instead of verify-and-retry#336julien-carsique-sonarsource wants to merge 1 commit into
Conversation
Replaces the verify-after-write + retry approach (#335, closed) with a genuine atomic claim: creating refs/build-locks/<N> fails if the ref already exists, so it acts as a compare-and-swap instead of a probabilistic race-window narrowing.
| MAX_ATTEMPTS="${MAX_ATTEMPTS:-50}" | ||
|
|
||
| claim_build_number() { | ||
| gh api --method POST -H "$GH_API_VERSION_HEADER" "$REFS_API_URL" -f "ref=refs/build-locks/$1" -f "sha=${GITHUB_SHA}" 2>&1 |
There was a problem hiding this comment.
💡 Bug: Collision detection relies on fragile English error string
Collision handling keys off the substring "Reference already exists" (get_build_number.sh:44). This depends on gh/GitHub's exact human-readable 422 message; if the wording, casing, or localization ever changes, every collision falls through to the fatal error branch (line 45) and the atomic-claim mechanism breaks entirely under normal concurrency rather than retrying. Consider matching on the HTTP status (e.g. capture gh api exit and use --include/checking for 422) or on the stable status/code field so detection doesn't hinge on prose.
Was this helpful? React with 👍 / 👎
| attempt=1 | ||
| CANDIDATE=$((BUILD_NUMBER + 1)) | ||
| while true; do | ||
| RESPONSE=$(claim_build_number "$CANDIDATE") && CLAIM_STATUS=0 || CLAIM_STATUS=$? | ||
|
|
||
| if [[ "$CLAIM_STATUS" -eq 0 ]]; then | ||
| echo "Claimed build number ${CANDIDATE} (refs/build-locks/${CANDIDATE})" | ||
| break | ||
| fi | ||
|
|
||
| if [[ "$RESPONSE" != *"Reference already exists"* ]]; then | ||
| echo "::error title=Build number claim failed::${RESPONSE}" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
There was a problem hiding this comment.
💡 Edge Case: Stale build_number hint can spuriously exhaust MAX_ATTEMPTS
The claim loop scans linearly from BUILD_NUMBER+1 and gives up after MAX_ATTEMPTS (default 50) consecutive collisions (get_build_number.sh:35-57). Since refs/build-locks/* are never pruned and the property PATCH is best-effort, if the hint lags the true frontier by more than MAX_ATTEMPTS (e.g. after repeated PATCH failures or a burst of >50 concurrent claims), every candidate in the window collides and the run fails even though a free number exists just beyond the window. Consider seeding the starting candidate from the highest existing build-lock ref, or exponentially jumping the candidate on repeated collisions instead of a fixed +1 linear scan.
Was this helpful? React with 👍 / 👎
CI failed: 4 CI jobs failed across the test suites with a 403 Forbidden 'Resource not accessible by integration' error when attempting to claim build numbers via Git refs.OverviewAll 4 analyzed CI log failures point to the same root cause: the GitHub token lacks sufficient permissions ( FailuresGit Reference Creation Failure during Build Number Claim (confidence: high)
Summary
Code Review 👍 Approved with suggestions 0 resolved / 2 findingsReplaces the verify-and-retry build number allocation with atomic Git ref creation to eliminate race conditions, backed by comprehensive ShellSpec test coverage. Consider tightening the collision error string matching and addressing potential exhaustion of retry attempts from stale hints. 💡 Bug: Collision detection relies on fragile English error string📄 get-build-number/get_build_number.sh:20 📄 get-build-number/get_build_number.sh:44 Collision handling keys off the substring "Reference already exists" (get_build_number.sh:44). This depends on gh/GitHub's exact human-readable 422 message; if the wording, casing, or localization ever changes, every collision falls through to the fatal error branch (line 45) and the atomic-claim mechanism breaks entirely under normal concurrency rather than retrying. Consider matching on the HTTP status (e.g. capture 💡 Edge Case: Stale build_number hint can spuriously exhaust MAX_ATTEMPTS📄 get-build-number/get_build_number.sh:34-48 📄 get-build-number/get_build_number.sh:61-64 The claim loop scans linearly from BUILD_NUMBER+1 and gives up after MAX_ATTEMPTS (default 50) consecutive collisions (get_build_number.sh:35-57). Since refs/build-locks/* are never pruned and the property PATCH is best-effort, if the hint lags the true frontier by more than MAX_ATTEMPTS (e.g. after repeated PATCH failures or a burst of >50 concurrent claims), every candidate in the window collides and the run fails even though a free number exists just beyond the window. Consider seeding the starting candidate from the highest existing build-lock ref, or exponentially jumping the candidate on repeated collisions instead of a fixed +1 linear scan. 🤖 Prompt for agentsTip Comment OptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |
Summary
gitar-bot's review on PREQ-7781 Detect and retry concurrent build_number increments #335 correctly found verify-after-write doesn't close the race: two runs that both read N and both PATCH N+1 both pass verification and claim the same number.POST .../git/refsfails with 422 if the ref already exists).get_build_number.shnow claims a number by creatingrefs/build-locks/<N>— exclusively and deterministically.Design
build_numberproperty is read only as a starting hint; correctness never depends on it being accurate.refs/build-locks/<candidate>. Success = claimed. "Reference already exists" = collision, try the next candidate immediately. Any other API error is fatal (unchanged from before).refs/build-locks/*accumulate indefinitely; pruning is a follow-up, out of scope here (doesn't affect correctness).Blocking dependency
Needs the Vault-issued
build-numberbot token to gaincontents: write(to create refs) — it currently only hasrepository_custom_properties: write. That permission is defined once, shared by every repo using this pattern, so granting it widens that token's scope org-wide. See companion PR: SonarSource/re-terraform-aws-vault#9518, and PREQ-7781 for the tradeoff writeup.Do not merge before the Vault PR is reviewed/merged and Mate Molnar has weighed in — this PR is non-functional without it (ref creation will 403).
Test plan
spec/get_build_number_spec.sh: no-contention claim, single-collision retry, exhausted retries under permanent contention, non-collision API errors treated as fatal, hint-update failures don't fail the run.shellspec spec/get_build_number_spec.sh --shell bash)get_build_number.sh(kcov)shellcheckclean