From 040e91429a257682b336313b9a14c94c11b6ac46 Mon Sep 17 00:00:00 2001 From: Christopher Sellers <1@christophersellers.com> Date: Sat, 24 Oct 2026 23:33:21 -0700 Subject: [PATCH] ci(pr-gate): wait for slow Backend CI instead of timing out at 40 min Signed-off-by: Christopher Sellers <1@christophersellers.com> --- .github/workflows/pr-gate.yml | 41 +++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index 30eacf8c9..a3d9e1071 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -59,7 +59,7 @@ jobs: actions: read name: "PR Gate: Backend" runs-on: ubuntu-latest - timeout-minutes: 45 + timeout-minutes: 150 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 with: @@ -116,13 +116,15 @@ jobs: github-token: ${{ github.token }} # "Backend CI Status Gate" is the final aggregate job of # backend-ci.yml. It is not created until every other backend job - # finishes (~8 min on a green run), so we poll up to 40 minutes for it - # to appear and complete. The job's timeout-minutes: 45 remains the hard bound. + # finishes (up to 80 min under runner contention), so we poll up to + # 140 minutes for it to appear and complete. The job's 150-minute + # timeout remains the hard bound. script: | const { owner, repo } = context.repo; const sha = process.env.HEAD_SHA; const CHECK_NAME = process.env.CHECK_NAME; - const deadline = Date.now() + 40 * 60 * 1000; + const started = Date.now(); + const deadline = started + 140 * 60 * 1000; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); const callWithRetry = async (fn) => { @@ -190,6 +192,7 @@ jobs: }; let delay = 30000; + let completedWithoutGate = false; while (Date.now() < deadline) { const res = await callWithRetry(() => github.rest.checks.listForRef({ @@ -216,6 +219,36 @@ jobs: return; } + const workflowRes = await callWithRetry(() => + github.rest.actions.listWorkflowRuns({ + owner, + repo, + workflow_id: 'backend-ci.yml', + head_sha: sha, + event: 'pull_request', + per_page: 20, + }) + ); + if (!workflowRes) { + break; + } + const workflowRuns = workflowRes.data?.workflow_runs || []; + if (workflowRuns.length === 0 && Date.now() - started >= 10 * 60 * 1000) { + core.setFailed('Backend paths changed but Backend CI was not triggered within 10 minutes; path lists in pr-gate.yml and backend-ci.yml may have drifted.'); + return; + } + if (runs.length === 0 && workflowRuns.length > 0 && workflowRuns.every(r => r.status === 'completed')) { + if (completedWithoutGate) { + core.setFailed(`Backend CI completed without ${CHECK_NAME} check run. Run conclusion: ${workflowRuns.map(r => r.conclusion || 'unknown').join(', ')}`); + return; + } + completedWithoutGate = true; + core.info(`Backend CI completed (${workflowRuns.map(r => r.conclusion || 'unknown').join(', ')}); rechecking for ${CHECK_NAME}.`); + } else { + completedWithoutGate = false; + core.info(`Backend CI run status: ${workflowRuns.length ? workflowRuns.map(r => r.status).join(', ') : 'not triggered yet'}.`); + } + const remaining = Math.max(0, deadline - Date.now()); if (remaining === 0) { break;