diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e06020ea..878046022 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1499,9 +1499,24 @@ jobs: with: node-version: '24.21.0' # exact: bump deliberately (#1645) - - name: Setup Python + - name: Setup Python (attempt 1) + # setup-python downloads 3.12.10 on runners that don't cache it, + # which takes ~6 s. On 2026-10-28 (run 40771482790) the manifest + # fetch died on `getaddrinfo EAI_AGAIN raw.githubusercontent.com` + # and the step held the leg for 19 min before failing it. Cap the + # download and try once more; a second failure is not a blip. + id: setup-python if: matrix.suite == 'e2e_pypi' || matrix.uv != '' || matrix.poetry != '' || matrix.pdm != '' || matrix.hatch != '' uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + timeout-minutes: 4 + continue-on-error: true + with: + python-version: '3.12.10' # exact: last 3.12 with Windows+macOS builds; bump deliberately (#1645) + + - name: Setup Python (attempt 2) + if: (matrix.suite == 'e2e_pypi' || matrix.uv != '' || matrix.poetry != '' || matrix.pdm != '' || matrix.hatch != '') && steps.setup-python.outcome != 'success' + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + timeout-minutes: 4 with: python-version: '3.12.10' # exact: last 3.12 with Windows+macOS builds; bump deliberately (#1645) @@ -2790,9 +2805,20 @@ jobs: bun --version node --no-warnings "$js" --version - - name: Setup Python + uv + - name: Setup Python (attempt 1) + # Capped and retried once, as in the e2e job. + id: setup-python if: steps.gate.outputs.run == 'true' uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + timeout-minutes: 4 + continue-on-error: true + with: + python-version: '3.12.10' # exact: last 3.12 with Windows+macOS builds; bump deliberately (#1645) + + - name: Setup Python (attempt 2) + if: (steps.gate.outputs.run == 'true') && steps.setup-python.outcome != 'success' + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + timeout-minutes: 4 with: python-version: '3.12.10' # exact: last 3.12 with Windows+macOS builds; bump deliberately (#1645) diff --git a/scripts/tests/test_merge_queue_fail_fast.py b/scripts/tests/test_merge_queue_fail_fast.py index 1b7a6f64b..11d028ee0 100644 --- a/scripts/tests/test_merge_queue_fail_fast.py +++ b/scripts/tests/test_merge_queue_fail_fast.py @@ -116,16 +116,29 @@ def test_no_job_tolerates_failure(self): # A job-level continue-on-error would let ci-ok pass despite a failed # job, and this script would then cancel a run that could still land. # A step-level one hides that step's failure the same way, so only - # the capped cache restores may carry it: a miss there just means - # building cold. + # the capped cache restores may carry it (a miss there just means + # building cold), plus a capped first attempt that the very next + # step retries without tolerance. self.assertIsNone(re.search(r"^ continue-on-error:", self.text, re.M)) steps = re.split(r"(?m)^ - ", self.text) - tolerant = [s for s in steps if re.search(r"(?m)^\s*continue-on-error:", s)] + tolerant = [i for i, s in enumerate(steps) if re.search(r"(?m)^\s*continue-on-error:", s)] self.assertTrue(tolerant) - for step in tolerant: - self.assertRegex(step, r"(?m)^ uses: (Swatinem/rust-cache|actions/cache/restore)@", step[:80]) + for i in tolerant: + step = steps[i] self.assertRegex(step, r"(?m)^ timeout-minutes: \d+$", step[:80]) self.assertRegex(step, r"(?m)^ continue-on-error: true$", step[:80]) + if re.search(r"(?m)^ uses: (Swatinem/rust-cache|actions/cache/restore)@", step): + continue + self.assertRegex(step, r"^name: .* \(attempt 1\)\n", step[:80]) + sid = re.search(r"(?m)^ id: (\S+)$", step).group(1) + retry = steps[i + 1] + self.assertRegex(retry, r"^name: .* \(attempt 2\)\n", retry[:80]) + self.assertIn(f"steps.{sid}.outcome != 'success'", retry, retry[:80]) + self.assertEqual( + re.search(r"(?m)^ uses: .*$", step).group(0), + re.search(r"(?m)^ uses: .*$", retry).group(0), + ) + self.assertNotRegex(retry, r"(?m)^\s*continue-on-error:", retry[:80]) def test_every_cache_restore_is_capped(self): # An uncapped restore that hangs holds the queue head until the job