From 4c71fcb157e71555622a9ec45c2c707439d601d4 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Thu, 10 Sep 2026 13:22:17 +0000
Subject: [PATCH] Budget the Windows leg's runtime in wait-msi-artifact, not the queue it sits in (#989)

---
 .github/workflows/build.yml |   59 +++++++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 43 insertions(+), 16 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c009b5c..9bb01cd 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -946,9 +946,12 @@
   # so both jobs would have failed a perfectly healthy build.
   wait-msi-artifact:
     runs-on: 'ubuntu-latest'
-    # Well past any queue seen so far, and only reached if the Windows leg neither publishes
-    # nor finishes; the usual exits are the artifact appearing or the leg failing.
-    timeout-minutes: 130
+    # The queue is not this run's to bound: it is a property of the account's runner
+    # capacity, which GitHub shares across every OS, and the leg has been seen sitting in it
+    # for three hours. So the wall clock here is only the six-hour ceiling GitHub puts on a
+    # job, less a margin; what gets a budget is the leg's own runtime, which is the part a
+    # change under test can actually break.
+    timeout-minutes: 350
     permissions:
       contents: read
       # Listing the run's artifacts and jobs, which the wait below polls.
@@ -959,24 +962,48 @@
         env:
           GH_TOKEN: ${{ github.token }}
         run: |
-          deadline=$(( $(date +%s) + 120 * 60 ))
+          # Four times the longest the leg has taken to publish, and counted only from the
+          # moment it has a runner. The budget this replaces ran from t=0 and so was spent on
+          # the queue instead: in run 34318105667 the leg waited 178 minutes for a Windows
+          # runner and published 14 minutes later, an hour after a 120-minute budget had
+          # failed a build that was entirely healthy.
+          budget=$(( 60 * 60 ))
+          deadline=''
           while true; do
-            if gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts" \
-                 --jq '.artifacts[].name' | grep -qx 'windows-latest-11'; then
+            # A transient API error is a reason to poll again, not to read "not published
+            # yet" out of a listing that never arrived: the leg may already have published,
+            # and the completion check below would then call a healthy build one that
+            # finished without publishing.
+            if ! artifacts=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts" \
+                 --jq '.artifacts[].name'); then
+              sleep 30
+              continue
+            fi
+            if grep -qx 'windows-latest-11' <<<"$artifacts"; then
               echo 'windows-latest-11 is available'
               exit 0
             fi
-            # Stop waiting the moment the leg that would publish it has finished without
-            # doing so, instead of sitting out the deadline.
-            # || true: a transient API error is a reason to poll again, not to fail the run.
-            conclusion=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs?per_page=100" \
-              --jq '.jobs[] | select(.name | startswith("build-maven (windows-latest, 11)")) | .conclusion' || true)
-            if [ -n "$conclusion" ] && [ "$conclusion" != "null" ]; then
-              echo "the Windows build leg finished as '$conclusion' without publishing windows-latest-11" >&2
-              exit 1
+            if ! leg=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs?per_page=100" \
+                 --jq '.jobs[] | select(.name | startswith("build-maven (windows-latest, 11)")) | "\(.status) \(.conclusion)"'); then
+              sleep 30
+              continue
             fi
-            if [ "$(date +%s)" -ge "$deadline" ]; then
-              echo 'windows-latest-11 was not published within 120 minutes' >&2
+            case "$leg" in
+              # Stop waiting the moment the leg that would publish it has finished without
+              # doing so, instead of sitting out the deadline.
+              completed*)
+                echo "the Windows build leg finished as '${leg#* }' without publishing windows-latest-11" >&2
+                exit 1
+                ;;
+              # It has a runner, so from here on the wait is on the build rather than on the
+              # queue. A queued leg is listed too, and with a started_at holding the time it
+              # was queued - which is why this reads the status and not that.
+              in_progress*)
+                [ -n "$deadline" ] || deadline=$(( $(date +%s) + budget ))
+                ;;
+            esac
+            if [ -n "$deadline" ] && [ "$(date +%s)" -ge "$deadline" ]; then
+              echo 'the Windows build leg has been running for 60 minutes without publishing windows-latest-11' >&2
               exit 1
             fi
             sleep 30

--
Gitblit v1.10.0