@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.
- package/errors/caching-artifacts/cache-key-windows-path-separator-never-matches.yml +107 -0
- package/errors/caching-artifacts/caching-artifacts-069.yml +133 -0
- package/errors/concurrency-timing/rerun-failed-jobs-bypasses-concurrency-group.yml +89 -0
- package/errors/concurrency-timing/workflow-run-head-branch-null-schedule-dispatch-concurrency.yml +135 -0
- package/errors/known-unsolved/empty-matrix-fromjson-workflow-failure-no-conditional-skip.yml +108 -0
- package/errors/known-unsolved/node-action-post-step-wrong-inputs-nested-composite.yml +133 -0
- package/errors/known-unsolved/ubuntu-24-04-arm64-missing-binder-ashmem-kernel-modules.yml +149 -0
- package/errors/permissions-auth/permissions-auth-069.yml +161 -0
- package/errors/runner-environment/arc-autoscalinglistener-ephemeralrunnerset-stale-after-upgrade.yml +134 -0
- package/errors/runner-environment/broker-server-socket-exception-nat-timeout-linux.yml +114 -0
- package/errors/runner-environment/checkout-v603-hash-algorithm-api-rate-limiting.yml +100 -0
- package/errors/runner-environment/macos-self-hosted-listener-aad-ghost-busy-stall.yml +126 -0
- package/errors/runner-environment/runner-environment-210.yml +105 -0
- package/errors/runner-environment/runner-environment-213.yml +142 -0
- package/errors/runner-environment/setup-node-ebaddevengines-devengines-packagemanager.yml +103 -0
- package/errors/runner-environment/ubuntu-24-man-db-dpkg-trigger-apt-install-stall.yml +94 -0
- package/errors/runner-environment/ubuntu-26-04-missing-preinstalled-tools.yml +178 -0
- package/errors/runner-environment/upload-artifact-v6-proxy-headers-leak-strict-proxy-fail.yml +101 -0
- package/errors/silent-failures/silent-failures-108.yml +108 -0
- package/errors/triggers/pull-request-labeled-fires-all-labels-no-name-filter.yml +110 -0
- package/errors/yaml-syntax/duplicate-step-id-within-job-scope-validation-error.yml +130 -0
- 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'
|