PREQ-7781 Detect and retry concurrent build_number increments - #335
PREQ-7781 Detect and retry concurrent build_number increments#335julien-carsique-sonarsource wants to merge 1 commit into
Conversation
get_build_number.sh did a non-atomic read-increment-write on the repo's build_number custom property. Concurrent workflow runs (e.g. GitHub Stacked PRs triggering builds in parallel) could read the same value, both increment it, and both write the same "unique" number back — promote@v1 then fails with "Multiple builds found". GitHub's custom-properties API has no documented conditional/atomic write support (no If-Match), so this can't be made fully atomic without switching to a different backing store (tracked separately under BUILD-11797). Instead, verify the write by reading the value back; if another run raced ahead of us, retry with jittered backoff (up to MAX_ATTEMPTS, default 10). This closes most of the race window at the cost of a small amount of extra latency on collision.
| NEXT_BUILD_NUMBER=$((BUILD_NUMBER + 1)) | ||
| gh api --method PATCH -H "$GH_API_VERSION_HEADER" "$PROPERTIES_API_URL" \ | ||
| -f "properties[][property_name]=build_number" \ | ||
| -f "properties[][value]=${NEXT_BUILD_NUMBER}" | ||
|
|
||
| CONFIRMED_BUILD_NUMBER=$(get_property_value) | ||
| if [[ "$CONFIRMED_BUILD_NUMBER" == "$NEXT_BUILD_NUMBER" ]]; then | ||
| echo "Incremented 'build_number' repository property to ${NEXT_BUILD_NUMBER}" | ||
| echo "${NEXT_BUILD_NUMBER}" > "$BUILD_NUMBER_FILE" | ||
| exit 0 | ||
| fi |
There was a problem hiding this comment.
⚠️ Bug: Verify-after-write misses symmetric same-value race
When two runs both read N and both PATCH N+1, each read-back returns N+1 and CONFIRMED == NEXT for both, so both pass verification and claim the same number — precisely the 'Multiple builds found' scenario the PR aims to fix. The check only catches a concurrent run that writes a different value between our PATCH and our read-back; it does not detect two runs writing the identical next value. This narrows but does not close the reported window. Consider adding random jitter before the initial read, or a post-write delay before verification so a competing writer's value can diverge, and document that this remains best-effort until the BUILD-11797 atomic-store migration.
Delay before verification so a same-value competing write is more likely to be overwritten and detected (still best-effort).:
NEXT_BUILD_NUMBER=$((BUILD_NUMBER + 1))
gh api --method PATCH -H "$GH_API_VERSION_HEADER" "$PROPERTIES_API_URL" \
-f "properties[][property_name]=build_number" \
-f "properties[][value]=${NEXT_BUILD_NUMBER}"
# Give a competing writer time to overwrite with a different value so the
# symmetric "both wrote N+1" collision becomes detectable on read-back.
sleep "0.$((RANDOM % 900 + 100))"
CONFIRMED_BUILD_NUMBER=$(get_property_value)
Was this helpful? React with 👍 / 👎
Code Review
|
| Auto-apply | Compact | Unblock |
|
|
|
Was this helpful? React with 👍 / 👎 | Gitar
|
There was a problem hiding this comment.
Pull request overview
This PR updates get-build-number to reduce build-number collisions across concurrent GitHub Actions runs by adding a write-verify-retry loop (with jittered backoff) when updating the repository’s build_number custom property.
Changes:
- Refactors
get_build_number.shto loop with a post-PATCH confirmation read and retry up toMAX_ATTEMPTS. - Adds ShellSpec coverage for detected concurrent updates (retry-then-succeed) and for exhausting retries under sustained contention.
- Updates existing specs to account for the new “read twice” behavior (initial read + confirmation read).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| get-build-number/get_build_number.sh | Adds a retry loop and confirmation read after PATCH to detect certain concurrent updates. |
| spec/get_build_number_spec.sh | Extends mocks/tests to simulate races and retry exhaustion for the updated script behavior. |
Suppressed comments (1)
spec/get_build_number_spec.sh:106
- This spec also triggers backoff retries and will run real
sleepcalls (up to ~1s per retry). Stubbingsleepto a no-op here keeps the test suite fast and avoids flakes due to timing.
It 'should fail after exhausting retries if the race never resolves'
export MAX_ATTEMPTS=3
export GH_GET_CALLS_FILE="${TEMP_DIR}/gh_get_calls_5.txt"
rm -f "$GH_GET_CALLS_FILE"
Mock gh
if [[ "$*" =~ "api --method PATCH" ]]; then
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if ! [[ "$BUILD_NUMBER" =~ ^[0-9]+$ ]]; then | ||
| echo "::error title=Invalid build number::Build number '${BUILD_NUMBER}' is not a valid positive integer." >&2 | ||
| exit 1 | ||
| fi |
| NEXT_BUILD_NUMBER=$((BUILD_NUMBER + 1)) | ||
| gh api --method PATCH -H "$GH_API_VERSION_HEADER" "$PROPERTIES_API_URL" \ | ||
| -f "properties[][property_name]=build_number" \ | ||
| -f "properties[][value]=${NEXT_BUILD_NUMBER}" | ||
|
|
||
| CONFIRMED_BUILD_NUMBER=$(get_property_value) | ||
| if [[ "$CONFIRMED_BUILD_NUMBER" == "$NEXT_BUILD_NUMBER" ]]; then | ||
| echo "Incremented 'build_number' repository property to ${NEXT_BUILD_NUMBER}" | ||
| echo "${NEXT_BUILD_NUMBER}" > "$BUILD_NUMBER_FILE" | ||
| exit 0 | ||
| fi |
| It 'should retry when a concurrent run wins the race, then succeed' | ||
| export GH_GET_CALLS_FILE="${TEMP_DIR}/gh_get_calls_4.txt" | ||
| rm -f "$GH_GET_CALLS_FILE" | ||
| Mock gh |
|
Not the correct approach |
|
Adding context on why: Replacing this with a real atomic claim via GitHub's Git References API (ref creation is a genuine compare-and-swap — it fails with 422 if the ref already exists, no sleeping or probability involved). Continuing on PREQ-7781, new PR incoming. |



Summary
get-build-number/get_build_number.shdid a non-atomic read→increment→write on the repo'sbuild_numbercustom property.promote@v1then fails withMultiple builds found for <repo> <number>.MAX_ATTEMPTS, default 10) instead of trusting an unverified write.Why not fully atomic?
GitHub's custom repository properties API has no documented conditional/atomic write support (no
If-Match/ETag-based CAS, unlike the Contents API). A truly atomic counter would need a different backing store — that's the larger redesign already tracked in BUILD-11797 (migrateget-build-numberfromactions/cachetogh-action_cache). This PR closes the practical race window (GET→PATCH→GET-verify is much narrower than the original GET...PATCH span across two independent runs) without that bigger migration.Root-caused and fixed for PREQ-7781 (https://sonarsource.atlassian.net/browse/PREQ-7781), reported by Ismail Cherri via stacked PRs on
sonarqube-unification.Test plan
get_build_number_spec.shexamples pass (shellspec spec/get_build_number_spec.sh --shell bash)get_build_number.sh(kcov)