diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c3d496d9..6de0c5c25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,14 +68,31 @@ jobs: exit 1 fi repository_name="${GITHUB_REPOSITORY#*/}" - live_base_sha="$( - gh api graphql \ - -f query='query($owner:String!,$name:String!,$qualifiedName:String!){repository(owner:$owner,name:$name){ref(qualifiedName:$qualifiedName){target{oid}}}}' \ - -F owner="$GITHUB_REPOSITORY_OWNER" \ - -F name="$repository_name" \ - -F qualifiedName="refs/heads/${NOEMA_PR_BASE_REF}" \ - --jq '.data.repository.ref.target.oid' - )" + resolve_live_base_sha() { + local output="" + for attempt in 1 2 3; do + if output="$( + gh api graphql \ + -f query='query($owner:String!,$name:String!,$qualifiedName:String!){repository(owner:$owner,name:$name){ref(qualifiedName:$qualifiedName){target{oid}}}}' \ + -F owner="$GITHUB_REPOSITORY_OWNER" \ + -F name="$repository_name" \ + -F qualifiedName="refs/heads/${NOEMA_PR_BASE_REF}" \ + --jq '.data.repository.ref.target.oid' \ + 2>&1 + )"; then + printf '%s\n' "$output" + return 0 + fi + if printf '%s\n' "$output" | grep -Eq '\(HTTP (502|503|504)\)$' && [ "$attempt" -lt 3 ]; then + sleep "$attempt" + continue + fi + printf '::error::Live pull-request base resolution failed after attempt %s.\n' "$attempt" >&2 + return 1 + done + return 1 + } + live_base_sha="$(resolve_live_base_sha)" if [[ ! "$live_base_sha" =~ ^[0-9a-f]{40}$ ]]; then printf '::error::Live pull-request base ref did not resolve to a full commit SHA.\n' exit 1 @@ -135,14 +152,31 @@ jobs: exit 1 fi repository_name="${GITHUB_REPOSITORY#*/}" - live_base_sha="$( - gh api graphql \ - -f query='query($owner:String!,$name:String!,$qualifiedName:String!){repository(owner:$owner,name:$name){ref(qualifiedName:$qualifiedName){target{oid}}}}' \ - -F owner="$GITHUB_REPOSITORY_OWNER" \ - -F name="$repository_name" \ - -F qualifiedName="refs/heads/${NOEMA_PR_BASE_REF}" \ - --jq '.data.repository.ref.target.oid' - )" + resolve_live_base_sha() { + local output="" + for attempt in 1 2 3; do + if output="$( + gh api graphql \ + -f query='query($owner:String!,$name:String!,$qualifiedName:String!){repository(owner:$owner,name:$name){ref(qualifiedName:$qualifiedName){target{oid}}}}' \ + -F owner="$GITHUB_REPOSITORY_OWNER" \ + -F name="$repository_name" \ + -F qualifiedName="refs/heads/${NOEMA_PR_BASE_REF}" \ + --jq '.data.repository.ref.target.oid' \ + 2>&1 + )"; then + printf '%s\n' "$output" + return 0 + fi + if printf '%s\n' "$output" | grep -Eq '\(HTTP (502|503|504)\)$' && [ "$attempt" -lt 3 ]; then + sleep "$attempt" + continue + fi + printf '::error::Live pull-request base resolution failed after attempt %s.\n' "$attempt" >&2 + return 1 + done + return 1 + } + live_base_sha="$(resolve_live_base_sha)" if [[ ! "$live_base_sha" =~ ^[0-9a-f]{40}$ ]]; then printf '::error::Live pull-request base ref did not resolve to a full commit SHA.\n' exit 1 diff --git a/test/workflow-readiness.test.ts b/test/workflow-readiness.test.ts index d6961f63a..f6258fd18 100644 --- a/test/workflow-readiness.test.ts +++ b/test/workflow-readiness.test.ts @@ -130,6 +130,15 @@ describe("deployment workflow readiness gates", () => { expect(workflow).toContain("if: always()"); }); + it("bounds live-base GitHub retries to transient availability failures", () => { + const workflow = readFileSync(".github/workflows/ci.yml", "utf8"); + + expect(workflow.match(/for attempt in 1 2 3; do/g)).toHaveLength(2); + expect(workflow.match(/grep -Eq '\\\(HTTP \(502\|503\|504\)\\\)\$'/g)).toHaveLength(2); + expect(workflow.match(/sleep "\$attempt"/g)).toHaveLength(2); + expect(workflow.match(/Live pull-request base resolution failed after attempt/g)).toHaveLength(2); + }); + it("runs the mandatory reviewer gate on every pull request", () => { const workflow = readFileSync(".github/workflows/reviewer-ci.yml", "utf8");