@htekdev/actions-debugger 1.0.116 → 1.0.118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/errors/caching-artifacts/cache-key-windows-path-separator-never-matches.yml +107 -0
  2. package/errors/caching-artifacts/caching-artifacts-069.yml +133 -0
  3. package/errors/concurrency-timing/rerun-failed-jobs-bypasses-concurrency-group.yml +89 -0
  4. package/errors/concurrency-timing/workflow-run-head-branch-null-schedule-dispatch-concurrency.yml +135 -0
  5. package/errors/known-unsolved/empty-matrix-fromjson-workflow-failure-no-conditional-skip.yml +108 -0
  6. package/errors/known-unsolved/node-action-post-step-wrong-inputs-nested-composite.yml +133 -0
  7. package/errors/known-unsolved/ubuntu-24-04-arm64-missing-binder-ashmem-kernel-modules.yml +149 -0
  8. package/errors/permissions-auth/permissions-auth-069.yml +161 -0
  9. package/errors/runner-environment/arc-autoscalinglistener-ephemeralrunnerset-stale-after-upgrade.yml +134 -0
  10. package/errors/runner-environment/broker-server-socket-exception-nat-timeout-linux.yml +114 -0
  11. package/errors/runner-environment/checkout-v603-hash-algorithm-api-rate-limiting.yml +100 -0
  12. package/errors/runner-environment/macos-self-hosted-listener-aad-ghost-busy-stall.yml +126 -0
  13. package/errors/runner-environment/runner-environment-210.yml +105 -0
  14. package/errors/runner-environment/runner-environment-213.yml +142 -0
  15. package/errors/runner-environment/setup-node-ebaddevengines-devengines-packagemanager.yml +103 -0
  16. package/errors/runner-environment/ubuntu-24-man-db-dpkg-trigger-apt-install-stall.yml +94 -0
  17. package/errors/runner-environment/ubuntu-26-04-missing-preinstalled-tools.yml +178 -0
  18. package/errors/runner-environment/upload-artifact-v6-proxy-headers-leak-strict-proxy-fail.yml +101 -0
  19. package/errors/silent-failures/silent-failures-108.yml +108 -0
  20. package/errors/triggers/pull-request-labeled-fires-all-labels-no-name-filter.yml +110 -0
  21. package/errors/yaml-syntax/duplicate-step-id-within-job-scope-validation-error.yml +130 -0
  22. package/package.json +1 -1
@@ -0,0 +1,114 @@
1
+ id: runner-environment-209
2
+ title: 'Self-hosted runner BrokerServer TaskCanceledException / SocketException — runner stuck in Busy, jobs delayed'
3
+ category: runner-environment
4
+ severity: error
5
+ tags:
6
+ - self-hosted
7
+ - broker
8
+ - TaskCanceledException
9
+ - SocketException
10
+ - NAT
11
+ - kubernetes
12
+ - ARC
13
+ - busy-state
14
+ patterns:
15
+ - regex: 'BrokerServer.*TaskCanceledException'
16
+ flags: 'i'
17
+ - regex: 'SocketException \(125\): Operation canceled'
18
+ flags: 'i'
19
+ - regex: 'GET request to https://broker\.actions\.githubusercontent\.com.*has been cancelled'
20
+ flags: 'i'
21
+ error_messages:
22
+ - '[ERR BrokerServer] System.Threading.Tasks.TaskCanceledException: The operation was canceled.'
23
+ - '[ERR BrokerServer] System.IO.IOException: Unable to read data from the transport connection: Operation canceled.'
24
+ - '[ERR BrokerServer] System.Net.Sockets.SocketException (125): Operation canceled'
25
+ - '[WARN GitHubActionsService] GET request to https://broker.actions.githubusercontent.com/message?sessionId=...&status=Busy&runnerVersion=... has been cancelled.'
26
+ - '[WARN BrokerServer] Back off 6.934 seconds before next retry. 4 attempt left.'
27
+ root_cause: |
28
+ The GitHub Actions runner (on Linux, macOS, Kubernetes, and ARC) maintains a
29
+ persistent long-poll HTTPS connection to `broker.actions.githubusercontent.com`
30
+ to receive job dispatch messages. This connection is kept open by a blocking
31
+ GET request that the server holds for up to 90 seconds before responding.
32
+
33
+ When the runner operates behind a **NAT gateway, stateful firewall, or cloud
34
+ provider network** (common in Kubernetes/ARC deployments on EKS, GKE, AKS, or
35
+ on-premise k8s), the network layer's connection tracking table can expire the
36
+ idle TLS socket before the server responds. Most cloud NAT tables have a
37
+ default idle timeout of 30–60 seconds — shorter than the runner's 90-second
38
+ poll interval.
39
+
40
+ When the NAT table entry expires:
41
+ 1. The next packet the runner sends receives an RST from the network (or is
42
+ silently dropped), causing the underlying `SslStream.ReadAsyncInternal`
43
+ to throw `SocketException (125): Operation canceled`
44
+ 2. The exception propagates as `TaskCanceledException` through the
45
+ `BrokerHttpClient.GetRunnerMessageAsync` call chain
46
+ 3. The runner logs `ERR BrokerServer` and backs off exponentially (6–60 s)
47
+ 4. During the back-off, the runner remains in **Busy** status from the broker's
48
+ perspective, preventing new jobs from being dispatched
49
+
50
+ The back-off recovers automatically but delays job pickup by minutes. Under
51
+ high-frequency job dispatch (CI matrix builds), this causes jobs to queue
52
+ while the runner is technically idle.
53
+
54
+ **Distinct from re-199** (Windows V2 broker listener stops polling after the
55
+ first job — Windows-specific software bug): this issue affects Linux/macOS/K8s
56
+ and recovers automatically; re-199 causes a permanent stall requiring service
57
+ restart.
58
+ fix: |
59
+ **1. Enable TCP keepalive on the runner host (most effective):**
60
+
61
+ Configure the OS to send TCP keepalive probes before the NAT table expires:
62
+ ```bash
63
+ # Linux — reduce keepalive idle time from default 7200s to 30s
64
+ sudo sysctl -w net.ipv4.tcp_keepalive_time=30
65
+ sudo sysctl -w net.ipv4.tcp_keepalive_intvl=10
66
+ sudo sysctl -w net.ipv4.tcp_keepalive_probes=3
67
+ # Make persistent:
68
+ echo "net.ipv4.tcp_keepalive_time=30" | sudo tee -a /etc/sysctl.conf
69
+ ```
70
+
71
+ **2. Increase NAT idle timeout (infrastructure change):**
72
+
73
+ - **AWS EKS:** Set `--conntrack-tcp-timeout-established=300` on kube-proxy,
74
+ or add a NAT gateway connection tracking timeout of 350 s
75
+ - **GKE:** Use Cloud NAT with `--nat-tcp-established-idle-timeout=350`
76
+ - **Azure AKS:** Set `--load-balancer-idle-timeout-in-minutes=10` (default is 4 min)
77
+ - **On-premise k8s:** Increase `conntrack` timeout or set up a keepalive proxy
78
+
79
+ **3. Use a runner proxy with keepalive support:**
80
+
81
+ Route runner outbound traffic through an application-level proxy that
82
+ maintains the connection, preventing the NAT table from expiring the socket.
83
+
84
+ **4. Upgrade runner version:**
85
+
86
+ Runner v2.326.0+ includes improved broker reconnect logic that reduces the
87
+ window where the runner stays in Busy state after a socket reset.
88
+ fix_code:
89
+ - language: yaml
90
+ label: 'Runner DaemonSet init container — set TCP keepalive before runner starts'
91
+ code: |
92
+ # In your ARC runner DaemonSet / Pod spec
93
+ initContainers:
94
+ - name: set-sysctl
95
+ image: busybox
96
+ securityContext:
97
+ privileged: true
98
+ command:
99
+ - sh
100
+ - -c
101
+ - |
102
+ sysctl -w net.ipv4.tcp_keepalive_time=30
103
+ sysctl -w net.ipv4.tcp_keepalive_intvl=10
104
+ sysctl -w net.ipv4.tcp_keepalive_probes=3
105
+ prevention:
106
+ - 'Set tcp_keepalive_time to 30 s on all Linux self-hosted runner hosts, especially those in Kubernetes'
107
+ - 'For ARC scale sets in EKS/GKE/AKS, explicitly configure NAT idle timeout to at least 350 seconds'
108
+ - 'Monitor runner diagnostic logs (Runner_<date>-utc.log) for repeated BrokerServer ERR lines — they indicate this issue'
109
+ - 'Upgrade runner to v2.326.0+ which has improved back-off and reconnect behavior'
110
+ docs:
111
+ - url: 'https://github.com/actions/runner/issues/3904'
112
+ label: 'actions/runner#3904 — Runner fails to connect to broker, TaskCanceledException / SocketException (17 reactions)'
113
+ - url: 'https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#communication-between-self-hosted-runners-and-github'
114
+ label: 'GitHub Docs — Self-hosted runner communication requirements'
@@ -0,0 +1,100 @@
1
+ id: runner-environment-206
2
+ title: 'actions/checkout v6.0.3 regression — new /hash-algorithm API call on every checkout exhausts rate limits in high-volume orgs'
3
+ category: runner-environment
4
+ severity: error
5
+ tags:
6
+ - checkout
7
+ - rate-limit
8
+ - api-rate-limit
9
+ - v6
10
+ - PAT
11
+ - regression
12
+ - hash-algorithm
13
+ patterns:
14
+ - regex: 'API rate limit exceeded'
15
+ flags: 'i'
16
+ - regex: 'You have exceeded a secondary rate limit'
17
+ flags: 'i'
18
+ - regex: 'HttpError.*API rate limit exceeded'
19
+ flags: 'i'
20
+ - regex: 'Rate limit.*exceeded.*403'
21
+ flags: 'i'
22
+ error_messages:
23
+ - 'Error: HttpError: API rate limit exceeded for user ID'
24
+ - 'You have exceeded a secondary rate limit and have been temporarily blocked from content creation'
25
+ - 'remote: Repository not found.'
26
+ - 'fatal: repository ''https://github.com/owner/repo/'' not found'
27
+ root_cause: |
28
+ actions/checkout v6.0.3 (released June 2, 2026, commit 1cce339) introduced a new
29
+ REST API call to GET /repos/{owner}/{repo}/hash-algorithm on every checkout operation
30
+ to determine the repository's object hashing algorithm.
31
+
32
+ In organizations with high-concurrency workflows, this additional API call multiplies
33
+ rate-limit consumption significantly. A matrix build with 30 parallel jobs each running
34
+ checkout makes 30 additional API calls per push event. At the per-user rate limit of
35
+ 5,000 requests/hour, large orgs using PATs (Personal Access Tokens) for cross-repo
36
+ checkout quickly exhaust their quota across many concurrent pipelines.
37
+
38
+ When the /hash-algorithm endpoint returns HTTP 403 (rate limited), the checkout action
39
+ may interpret the 403 as a resource-not-found condition, producing misleading errors
40
+ such as "remote: Repository not found" that mask the true rate-limit cause.
41
+
42
+ GITHUB_TOKEN is not affected in the same way because it carries per-repository rate
43
+ limits (15,000 requests/hour for Actions) rather than per-user limits.
44
+ Source: actions/checkout#2450.
45
+ fix: |
46
+ Immediate fix — pin to actions/checkout@v6.0.2 until the upstream regression is
47
+ addressed:
48
+ uses: actions/checkout@v6.0.2
49
+
50
+ Use GITHUB_TOKEN instead of PAT where possible:
51
+ GITHUB_TOKEN rate limits (15,000 req/hr for GitHub Actions) are scoped per
52
+ repository and do not aggregate across your organization's other workflows.
53
+ Reserve PATs for cross-repo or cross-org checkouts only.
54
+
55
+ Reduce parallel checkout volume:
56
+ Add fetch-depth: 1 and/or sparse-checkout to minimize the API surface area
57
+ per checkout call, reducing the number of API requests triggered per job.
58
+
59
+ Monitor actions/checkout#2450 for the upstream fix (caching the hash-algorithm
60
+ result within a workflow run, or making the call conditional).
61
+ fix_code:
62
+ - language: yaml
63
+ label: 'Pin to v6.0.2 to avoid the regression until upstream fix is released'
64
+ code: |
65
+ - uses: actions/checkout@v6.0.2
66
+ with:
67
+ # Prefer GITHUB_TOKEN over a PAT to use per-repo rate limits
68
+ token: ${{ secrets.GITHUB_TOKEN }}
69
+ fetch-depth: 1 # Shallow fetch reduces ancillary API calls
70
+ - language: yaml
71
+ label: 'Cross-repo checkout — use dedicated PAT only where necessary, pin version'
72
+ code: |
73
+ - uses: actions/checkout@v6.0.2
74
+ with:
75
+ repository: org/other-repo
76
+ token: ${{ secrets.CROSS_REPO_PAT }} # PAT required here; rate-limited per user
77
+ fetch-depth: 1
78
+ - language: yaml
79
+ label: 'Monitor rate limit headers in a preflight step (diagnostic aid)'
80
+ code: |
81
+ - name: Check remaining GitHub API rate limit
82
+ run: |
83
+ remaining=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
84
+ https://api.github.com/rate_limit | jq '.rate.remaining')
85
+ echo "Remaining API calls: $remaining"
86
+ if (( remaining < 500 )); then
87
+ echo "::warning::Low API rate limit — $remaining calls remaining"
88
+ fi
89
+ prevention:
90
+ - 'Pin actions/checkout to a specific patch version (e.g., @v6.0.2) in high-volume orgs — patch updates can introduce API regressions like this one'
91
+ - 'Prefer GITHUB_TOKEN over org-wide PATs for checkout; GITHUB_TOKEN rate limits are per-repository and isolated from other workflows'
92
+ - 'Monitor your org REST API usage under Settings > Insights > API Requests to detect unexpected call spikes from action updates before they hit production'
93
+ - 'Add a rate-limit check step to long-running or high-parallelism pipelines to catch exhaustion before it causes misleading errors'
94
+ docs:
95
+ - url: 'https://github.com/actions/checkout/issues/2450'
96
+ label: 'actions/checkout#2450: New /hash-algorithm API call causing rate limiting failures in v6.0.3'
97
+ - url: 'https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api'
98
+ label: 'GitHub Docs: Rate limits for the REST API'
99
+ - url: 'https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token'
100
+ label: 'GitHub Docs: GITHUB_TOKEN permissions and rate limits'
@@ -0,0 +1,126 @@
1
+ id: runner-environment-205
2
+ title: 'macOS self-hosted Runner.Listener silently stalls after AAD credential-refresh — ghost-busy state blocks queue'
3
+ category: runner-environment
4
+ severity: silent-failure
5
+ tags:
6
+ - self-hosted
7
+ - macos
8
+ - apple-silicon
9
+ - listener
10
+ - aad
11
+ - ghost-busy
12
+ - broker-reconnect
13
+ - credential-refresh
14
+ patterns:
15
+ - regex: 'AAD Correlation ID for this token request:\s*Unknown'
16
+ flags: 'i'
17
+ - regex: 'RSAFileKeyManager.*Loading RSA key parameters from file.*credentials_rsaparams'
18
+ flags: 'i'
19
+ - regex: 'GitHubActionsService.*AAD Correlation ID.*Unknown'
20
+ flags: 'i'
21
+ error_messages:
22
+ - '[INFO RSAFileKeyManager] Loading RSA key parameters from file .../.credentials_rsaparams'
23
+ - '[INFO GitHubActionsService] AAD Correlation ID for this token request: Unknown'
24
+ root_cause: |
25
+ On long-lived self-hosted macOS runners (v2.334.0+, Apple Silicon), the
26
+ Runner.Listener process can permanently stall after an AAD (Azure Active Directory)
27
+ credential-refresh event coincides with a broker session disconnect.
28
+
29
+ Normal broker long-poll timeouts produce "SocketException (89): Operation canceled"
30
+ entries and the listener successfully reconnects. However, when a broker disconnect
31
+ occurs at the same time as an AAD credential refresh, the listener logs its final
32
+ diagnostic sequence and then goes permanently silent:
33
+ [INFO RSAFileKeyManager] Loading RSA key parameters from file .../.credentials_rsaparams
34
+ [INFO GitHubActionsService] AAD Correlation ID for this token request: Unknown
35
+
36
+ After these lines, the main thread parks in pthread_cond_wait with no further diag
37
+ log output and no TCP ESTABLISHED connection to the broker. The OS process stays alive
38
+ (visible in ps/Activity Monitor/launchctl list), so launchd does not restart it. The
39
+ broker-side agent state continues to show the runner as "busy" from its last completed
40
+ job, stalling all subsequent queued jobs behind the phantom runner until an external
41
+ restart clears the state.
42
+
43
+ The trigger requires: runner lifetime longer than several hours (so a credential
44
+ refresh occurs), plus a broker disconnect at or immediately after the refresh boundary.
45
+ Observed simultaneously affecting all 4 of 4 macOS ARM64 runners on a single host
46
+ within a 32-minute window, causing a 4-hour queue stall. Source: actions/runner#4446.
47
+ fix: |
48
+ No platform-side fix available as of June 2026 (open issue).
49
+
50
+ Workaround — implement an out-of-band watchdog script/cron that:
51
+ 1. Confirms the Runner.Listener PID has an ESTABLISHED TCP socket to the broker
52
+ (check with `lsof -p <pid> -i TCP | grep ESTABLISHED`)
53
+ 2. Confirms the most recent entry in _diag/Runner_*.log is less than N minutes old
54
+ (e.g., 10 minutes)
55
+ 3. If both checks fail, restarts the runner service:
56
+ - macOS (launchd):
57
+ launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/actions.runner.<owner-repo>.<name>.plist
58
+ sleep 5
59
+ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/actions.runner.<owner-repo>.<name>.plist
60
+ - Linux (systemd):
61
+ systemctl --user restart actions.runner.<owner-repo>.<name>.service
62
+ 4. Optionally clear the broker-side ghost-busy state via REST API:
63
+ curl -X DELETE \
64
+ -H "Authorization: Bearer $GH_TOKEN" \
65
+ "https://api.github.com/repos/<owner>/<repo>/actions/runners/<runner_id>"
66
+ This forces re-registration and clears the stale busy state immediately.
67
+
68
+ Long-term: run macOS runners as ephemeral (--once) with a process supervisor
69
+ that restarts after each completed job, eliminating the multi-hour lifetime
70
+ that triggers the credential-refresh race.
71
+ fix_code:
72
+ - language: yaml
73
+ label: 'Watchdog workflow on separate runner — detect and restart stalled listeners'
74
+ code: |
75
+ # Separate monitoring workflow on a non-affected runner
76
+ # Runs every 15 minutes via cron
77
+ on:
78
+ schedule:
79
+ - cron: '*/15 * * * *'
80
+ jobs:
81
+ watchdog:
82
+ runs-on: ubuntu-latest # Use a separate hosted runner for the watchdog
83
+ steps:
84
+ - name: Check and restart stalled macOS listeners
85
+ env:
86
+ GH_TOKEN: ${{ secrets.RUNNER_MGMT_PAT }}
87
+ run: |
88
+ # List all self-hosted runners and check for stuck-busy ones
89
+ gh api /repos/${{ github.repository }}/actions/runners \
90
+ --jq '.runners[] | select(.status=="online" and .busy==true) | .id' \
91
+ | while read runner_id; do
92
+ echo "Runner $runner_id showing busy — may need investigation"
93
+ # Add custom liveness check here (SSH to host, check log freshness)
94
+ done
95
+ - language: bash
96
+ label: 'Shell watchdog — check listener log freshness and restart via launchctl'
97
+ code: |
98
+ #!/bin/bash
99
+ # Run on the macOS runner host via cron every 10 minutes
100
+ RUNNER_LABEL="owner-repo-runner-name"
101
+ PLIST="$HOME/Library/LaunchAgents/actions.runner.${RUNNER_LABEL}.plist"
102
+ DIAG_DIR="$HOME/actions-runner/_diag"
103
+ STALE_MINUTES=10
104
+
105
+ latest_log=$(ls -t "${DIAG_DIR}/Runner_"*.log 2>/dev/null | head -1)
106
+ if [[ -z "$latest_log" ]]; then exit 0; fi
107
+
108
+ age_minutes=$(( ($(date +%s) - $(stat -f %m "$latest_log")) / 60 ))
109
+ if (( age_minutes > STALE_MINUTES )); then
110
+ echo "Runner diag log stale for ${age_minutes}min — restarting..."
111
+ launchctl bootout "gui/$(id -u)" "$PLIST" 2>/dev/null
112
+ sleep 5
113
+ launchctl bootstrap "gui/$(id -u)" "$PLIST"
114
+ fi
115
+ prevention:
116
+ - 'Run macOS self-hosted runners as ephemeral (--once) with a process supervisor — this eliminates the multi-hour lifetime needed to trigger the credential-refresh race condition'
117
+ - 'Implement a log-freshness watchdog that monitors _diag/Runner_*.log modification time and restarts the launchd service if no new entries appear for > 10 minutes'
118
+ - 'Monitor GET /repos/{owner}/{repo}/actions/runners and alert on runners with busy=true for longer than your longest expected job duration'
119
+ - 'Limit runner lifetime with a cron-triggered scheduled restart between jobs (e.g., nightly) to reduce the window where credential refresh coincides with a broker disconnect'
120
+ docs:
121
+ - url: 'https://github.com/actions/runner/issues/4446'
122
+ label: 'actions/runner#4446: Listener silently exits broker-reconnect loop after AAD credential-refresh (ghost-busy)'
123
+ - url: 'https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/monitoring-and-troubleshooting-self-hosted-runners'
124
+ label: 'GitHub Docs: Monitoring and troubleshooting self-hosted runners'
125
+ - url: 'https://docs.github.com/en/rest/actions/self-hosted-runners'
126
+ label: 'GitHub REST API: Self-hosted runners'
@@ -0,0 +1,105 @@
1
+ id: runner-environment-210
2
+ title: 'Runner step-log and summary uploads silently stall behind egress-only firewall — .NET BlobClient ignores HTTPS_PROXY'
3
+ category: runner-environment
4
+ severity: silent-failure
5
+ tags:
6
+ - proxy
7
+ - https-proxy
8
+ - blob-storage
9
+ - self-hosted
10
+ - egress-firewall
11
+ - logs-missing
12
+ - azure-blob
13
+ patterns:
14
+ - regex: 'productionresultssa\d+\.blob\.core\.windows\.net'
15
+ flags: 'i'
16
+ - regex: 'ua=azsdk-net-Storage\.Blobs.*latency=[0-9]{2,3}\.'
17
+ flags: 'i'
18
+ - regex: 'step summary.*not.*available|summary.*upload.*failed|diagnostic.*log.*missing'
19
+ flags: 'i'
20
+ error_messages:
21
+ - 'Step logs not visible in Actions UI — log upload stalled silently'
22
+ - 'Job completed but step summary is blank or missing'
23
+ - 'latency=74.999634s ua=azsdk-net-Storage.Blobs/12.27.0 (.NET 8.0)'
24
+ - 'host=productionresultssa6.blob.core.windows.net:443 latency=74.99s'
25
+ root_cause: |
26
+ The GitHub Actions runner process (v2.333.1 and earlier) creates Azure SDK
27
+ `BlobClient` instances for uploading step logs, workflow summaries, and diagnostic
28
+ logs without configuring an `HttpClientTransport` that honours the `HTTPS_PROXY`
29
+ environment variable.
30
+
31
+ In `ResultsHttpClient.cs`, the `GetBlobClient()` and `GetAppendBlobClient()` methods
32
+ pass only retry/timeout options to `BlobClientOptions` — no `Transport`. The Azure SDK
33
+ therefore falls back to its internal default HTTP pipeline, which does **not** inherit
34
+ the runner's `RunnerWebProxy` configuration.
35
+
36
+ The impact differs depending on network topology:
37
+
38
+ - **Without an egress firewall**: The runner's BlobClient connects directly to
39
+ `*.blob.core.windows.net` and `*.actions.githubusercontent.com`, bypassing the
40
+ proxy entirely. Direct egress works, so no error is visible.
41
+
42
+ - **With a deny-all egress firewall (proxy-only)**: The BlobClient attempts a direct
43
+ connection that the firewall silently drops. Each attempt stalls for ~75 seconds
44
+ (the TCP connect timeout), then times out. Since log uploads are non-fatal, the job
45
+ eventually completes — but step logs are absent from the Actions UI and the job
46
+ takes 5–15 minutes longer than expected.
47
+
48
+ This is separate from the `upload-artifact@v6` proxy CONNECT-headers regression
49
+ (re-208), which affects the Node.js artifact upload path.
50
+ fix: |
51
+ Add the Azure Blob Storage and Actions results endpoints to the `NO_PROXY` (or
52
+ `no_proxy`) environment variable so the runner bypasses the proxy for those hosts
53
+ and connects to them directly.
54
+
55
+ This requires that the runner's egress firewall allows direct connections to
56
+ `*.blob.core.windows.net` and `results-receiver.actions.githubusercontent.com`.
57
+ If only proxy egress is available, the workaround is to configure the proxy to
58
+ pass through those hosts without TLS inspection.
59
+
60
+ A proper fix (runner-side, not yet released): the `BlobClientOptions.Transport` in
61
+ `ResultsHttpClient.cs` should be configured with an `HttpClientTransport` wrapping
62
+ the runner's `RunnerWebProxy` — tracked in actions/runner#4351.
63
+ fix_code:
64
+ - language: yaml
65
+ label: 'Self-hosted runner — set NO_PROXY to bypass proxy for Azure Blob endpoints'
66
+ code: |
67
+ # Set at the OS level or in the runner's .env file before starting the runner service.
68
+ # This allows the BlobClient to connect directly while other traffic goes through the proxy.
69
+ #
70
+ # On Linux/macOS (add to /etc/environment or runner startup script):
71
+ # NO_PROXY=.blob.core.windows.net,.actions.githubusercontent.com,results-receiver.actions.githubusercontent.com
72
+ #
73
+ # In a workflow (if runner is ephemeral and you can set env per job):
74
+ jobs:
75
+ build:
76
+ runs-on: self-hosted
77
+ env:
78
+ HTTPS_PROXY: http://proxy.corp.example.com:3128
79
+ NO_PROXY: '.blob.core.windows.net,.actions.githubusercontent.com,results-receiver.actions.githubusercontent.com'
80
+ steps:
81
+ - uses: actions/checkout@v4
82
+ - run: echo "Logs will upload correctly now"
83
+ - language: yaml
84
+ label: 'ARC / Kubernetes — set NO_PROXY in RunnerDeployment or RunnerScaleSet'
85
+ code: |
86
+ apiVersion: actions.summerwind.dev/v1alpha1
87
+ kind: RunnerDeployment
88
+ spec:
89
+ template:
90
+ spec:
91
+ env:
92
+ - name: HTTPS_PROXY
93
+ value: http://proxy.corp.example.com:3128
94
+ - name: NO_PROXY
95
+ value: '.blob.core.windows.net,.actions.githubusercontent.com,results-receiver.actions.githubusercontent.com'
96
+ prevention:
97
+ - 'When deploying self-hosted runners behind a forward proxy with deny-all egress, always set NO_PROXY to include Azure Blob Storage endpoints — the runner BlobClient does not inherit HTTPS_PROXY.'
98
+ - 'Monitor Actions step log visibility alongside job exit codes — missing logs with a successful exit often indicate a proxy or network configuration issue, not a code failure.'
99
+ - 'Run a proxy connectivity diagnostic on a new runner host: confirm .blob.core.windows.net is reachable (directly or via proxy) before routing real workloads to it.'
100
+ - 'Track actions/runner#4351 for a first-party fix that configures the BlobClient transport to use the runner proxy settings.'
101
+ docs:
102
+ - url: 'https://github.com/actions/runner/issues/4351'
103
+ label: 'actions/runner #4351 — BlobClient uploads stall through HTTPS proxy (Apr 2026)'
104
+ - url: 'https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/using-a-proxy-server-with-self-hosted-runners'
105
+ label: 'GitHub Docs: Using a proxy server with self-hosted runners'
@@ -0,0 +1,142 @@
1
+ id: runner-environment-213
2
+ title: 'Self-Hosted Runner Stuck in Active State Indefinitely When Job Process Hangs Without Exiting'
3
+ category: runner-environment
4
+ severity: error
5
+ tags:
6
+ - self-hosted
7
+ - runner
8
+ - active-state
9
+ - hung-process
10
+ - child-process
11
+ - stuck
12
+ - service-restart
13
+ - timeout-minutes
14
+ patterns:
15
+ - regex: 'Waiting for a runner to pick up this job'
16
+ flags: 'i'
17
+ - regex: 'Runner\.Worker.*hung|Worker.*process.*running|active.*runner.*blocking'
18
+ flags: 'i'
19
+ - regex: 'sudo systemctl restart actions\.runner'
20
+ flags: 'i'
21
+ error_messages:
22
+ - 'Waiting for a runner to pick up this job...'
23
+ - 'Runner shows as Active in GitHub UI; new jobs remain Queued indefinitely'
24
+ root_cause: |
25
+ On self-hosted runners, the Runner.Worker process tracks the lifecycle of the running
26
+ job. When a job step spawns a child process (e.g., a test runner like `vitest --coverage`,
27
+ a long network operation, or a background daemon) that does not exit cleanly, the
28
+ Runner.Worker stays alive waiting for the child to terminate.
29
+
30
+ While Runner.Worker is alive, the parent Runner.Listener considers the runner slot
31
+ occupied (busy=true) and does not accept new job messages from the broker. The runner
32
+ appears as "Active" in the GitHub UI and all queued jobs for that runner remain
33
+ in "Waiting for a runner to pick up this job..." state indefinitely.
34
+
35
+ This differs from:
36
+ - GitHub-hosted runners: these have a hard 6-hour job timeout enforced by the platform;
37
+ the job is cancelled and the slot freed automatically
38
+ - Self-hosted runners WITH timeout-minutes set: once timeout-minutes elapses the
39
+ job is cancelled and the runner sends SIGTERM to the worker — but if the child
40
+ process ignores SIGTERM (common with some test runners), the worker still hangs
41
+
42
+ Common triggers:
43
+ - vitest --coverage, jest --forceExit not used, pytest hanging due to unclosed resources
44
+ - npm/yarn scripts that spawn background processes not tied to the shell session
45
+ - Docker commands (docker run without --rm) that keep running after the step exits
46
+ - Network calls blocked by firewall with no connection timeout
47
+ - Interactive prompts waiting for stdin input in a CI non-interactive context
48
+
49
+ Automatic recovery does NOT occur. The runner stays Active until either:
50
+ 1. The hung child process eventually exits on its own
51
+ 2. An operator manually restarts the runner service
52
+ 3. A watchdog script kills the orphaned worker process
53
+ fix: |
54
+ Immediate recovery: restart the runner service to free the stuck slot.
55
+ sudo systemctl restart actions.runner.<scope>.<name>.service # Linux systemd
56
+ launchctl unload ~/Library/LaunchAgents/actions.runner.*.plist # macOS
57
+ .\svc.sh stop && .\svc.sh start # Windows
58
+
59
+ Prevention (preferred):
60
+ 1. Add timeout-minutes to ALL jobs on self-hosted runners to cap maximum runtime.
61
+ Even if the worker hangs, the platform cancels the job and sends SIGTERM after
62
+ the timeout. Pair with process group kill to catch SIGTERM-resistant children.
63
+
64
+ 2. Ensure test commands force-exit when done:
65
+ - vitest: add --forceExit flag
66
+ - jest: use jest --forceExit or --detectOpenHandles to identify hanging handles
67
+ - pytest: add timeout fixtures via pytest-timeout plugin
68
+
69
+ 3. Use process groups (setsid / start new session) so SIGTERM cascades to children:
70
+ run: |
71
+ setsid bash -c 'npm test' &
72
+ CHILD_PID=$!
73
+ wait $CHILD_PID
74
+
75
+ 4. Deploy a runner watchdog that monitors Worker processes with no active child
76
+ CPU activity for > N minutes and kills them:
77
+ - Check elapsed time + zero CPU descendants
78
+ - SIGKILL stale Worker processes
79
+ - Trigger runner service restart via systemd or equivalent
80
+ fix_code:
81
+ - language: yaml
82
+ label: 'Add timeout-minutes to prevent indefinite runner lock'
83
+ code: |
84
+ jobs:
85
+ test:
86
+ runs-on: self-hosted
87
+ timeout-minutes: 30 # Always set on self-hosted runners
88
+ steps:
89
+ - uses: actions/checkout@v4
90
+ - name: Run tests
91
+ run: npm test
92
+ - language: yaml
93
+ label: 'Force-exit test runner so worker process completes cleanly'
94
+ code: |
95
+ jobs:
96
+ test:
97
+ runs-on: self-hosted
98
+ timeout-minutes: 30
99
+ steps:
100
+ - name: Run Vitest tests
101
+ run: npx vitest run --forceExit
102
+
103
+ - name: Run Jest tests
104
+ run: npx jest --forceExit
105
+
106
+ - name: Run pytest with timeout
107
+ run: pytest --timeout=300
108
+ - language: yaml
109
+ label: 'Watchdog step — kill orphaned background processes after main step'
110
+ code: |
111
+ jobs:
112
+ test:
113
+ runs-on: self-hosted
114
+ timeout-minutes: 30
115
+ steps:
116
+ - name: Run tests
117
+ run: npm test
118
+ continue-on-error: true
119
+ - name: Kill orphaned processes
120
+ if: always()
121
+ run: |
122
+ # Kill any remaining node processes owned by this runner user
123
+ pkill -u "$(whoami)" -f "vitest|jest|mocha" || true
124
+ prevention:
125
+ - 'Always set timeout-minutes on self-hosted runner jobs — without it there is no
126
+ platform-enforced maximum and a hung process can block the runner indefinitely'
127
+ - 'Use --forceExit with Jest/Vitest; use --timeout with pytest; audit any test suite
128
+ that takes longer than expected for open handles (jest --detectOpenHandles)'
129
+ - 'Avoid spawning background daemons in run: steps without explicit cleanup in an
130
+ if: always() cleanup step'
131
+ - 'Consider running self-hosted runners as ephemeral (ephemeral: true with ARC or
132
+ JIT tokens) — an ephemeral runner terminates after one job, so a hung runner
133
+ does not affect other jobs (a new runner pod is provisioned for each job)'
134
+ - 'Monitor runner Active state duration via GitHub REST API (GET /repos/{owner}/{repo}/actions/runners)
135
+ and alert when busy: true persists beyond expected max job duration'
136
+ docs:
137
+ - url: 'https://github.com/actions/runner/issues/4312'
138
+ label: 'actions/runner#4312 — Self-hosted runner gets stuck in active state, blocking queued jobs'
139
+ - url: 'https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/monitoring-and-troubleshooting-self-hosted-runners'
140
+ label: 'GitHub Docs — Monitoring and troubleshooting self-hosted runners'
141
+ - url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes'
142
+ label: 'GitHub Docs — timeout-minutes syntax reference'