-
Notifications
You must be signed in to change notification settings - Fork 1
PREQ-7781 Claim build numbers atomically via Git refs instead of verify-and-retry #336
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,21 +4,63 @@ | |
| set -euo pipefail | ||
|
|
||
| : "${GITHUB_REPOSITORY:?}" | ||
| : "${GITHUB_SHA:?}" | ||
| GH_API_VERSION_HEADER="X-GitHub-Api-Version: 2022-11-28" | ||
| BUILD_NUMBER_FILE="${BUILD_NUMBER_FILE:-.build_number.txt}" | ||
| PROPERTIES_API_URL="repos/${GITHUB_REPOSITORY}/properties/values" | ||
| REFS_API_URL="repos/${GITHUB_REPOSITORY}/git/refs" | ||
| # The custom-properties API has no conditional/atomic update (no If-Match support), so a plain | ||
| # GET-increment-PATCH on build_number races under concurrent runs (e.g. GitHub Stacked PRs) and can | ||
| # hand out the same number twice. Git ref creation IS atomic (it fails if the ref already exists), so | ||
| # it's used here as a compare-and-swap: exclusively claim refs/build-locks/<N> before trusting N. | ||
| # See PREQ-7781. | ||
| 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 | ||
| } | ||
|
|
||
| echo "Fetching build number from repository properties..." | ||
| PROPERTIES_API_URL="repos/${GITHUB_REPOSITORY}/properties/values" | ||
| BUILD_NUMBER=$(gh api -H "$GH_API_VERSION_HEADER" "$PROPERTIES_API_URL" --jq '.[] | select(.property_name == "build_number") | .value') | ||
| echo "Current build number from repo: ${BUILD_NUMBER:=0}" | ||
| 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 | ||
|
|
||
| BUILD_NUMBER=$((BUILD_NUMBER + 1)) | ||
| # BUILD_NUMBER above is only a starting hint for where to search; it may be stale (e.g. a concurrent | ||
| # run claimed further ahead and hasn't updated the property yet). Correctness never depends on it | ||
| # being accurate, only on the ref-creation compare-and-swap below. | ||
| 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 | ||
|
|
||
|
Comment on lines
+34
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Edge Case: Stale build_number hint can spuriously exhaust MAX_ATTEMPTSThe 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 👍 / 👎 |
||
| if (( attempt >= MAX_ATTEMPTS )); then | ||
| echo "::error title=Build number race::Could not claim a build number after ${MAX_ATTEMPTS} attempts (concurrent claims)." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "Build number ${CANDIDATE} already claimed; trying $((CANDIDATE + 1)) (attempt $((attempt + 1))/${MAX_ATTEMPTS})..." | ||
| CANDIDATE=$((CANDIDATE + 1)) | ||
| attempt=$((attempt + 1)) | ||
| done | ||
|
|
||
| # Best-effort: keep the custom property as a hint for the next run's starting point. Correctness | ||
| # never depends on this succeeding or being accurate. | ||
| gh api --method PATCH -H "$GH_API_VERSION_HEADER" "$PROPERTIES_API_URL" \ | ||
| -f "properties[][property_name]=build_number" \ | ||
| -f "properties[][value]=${BUILD_NUMBER}" | ||
| echo "Incremented 'build_number' repository property to ${BUILD_NUMBER}" | ||
| echo "${BUILD_NUMBER}" > "$BUILD_NUMBER_FILE" | ||
| -f "properties[][value]=${CANDIDATE}" \ | ||
| || echo "::warning title=Build number hint not updated::Failed to update the build_number property; this does not affect correctness." | ||
|
|
||
| echo "${CANDIDATE}" > "$BUILD_NUMBER_FILE" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 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 apiexit and use--include/checking for 422) or on the stablestatus/codefield so detection doesn't hinge on prose.Was this helpful? React with 👍 / 👎