@htekdev/actions-debugger 1.0.4 → 1.0.6
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/concurrency-timing/runner-group-not-found-race.yml +97 -0
- package/errors/runner-environment/checkout-safe-directory-container.yml +84 -0
- package/errors/runner-environment/dockerhub-pull-rate-limit.yml +125 -0
- package/errors/runner-environment/self-hosted-runner-version-deprecated.yml +99 -0
- package/errors/runner-environment/service-container-localhost-vs-hostname.yml +130 -0
- package/errors/runner-environment/ubuntu-latest-to-24-04-migration.yml +138 -0
- package/errors/silent-failures/cache-hit-restore-keys-misleading.yml +82 -0
- package/errors/silent-failures/github-output-multiline-truncated.yml +112 -0
- package/package.json +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
id: "concurrency-timing-008"
|
|
2
|
+
title: "Intermittent 'Required runner group not found' when ephemeral runner registers after job dispatch"
|
|
3
|
+
category: "concurrency-timing"
|
|
4
|
+
severity: "error"
|
|
5
|
+
tags:
|
|
6
|
+
- "runner-group"
|
|
7
|
+
- "self-hosted"
|
|
8
|
+
- "ephemeral"
|
|
9
|
+
- "race-condition"
|
|
10
|
+
- "dispatch"
|
|
11
|
+
- "organization"
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "Required runner group '.*' not found"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "runner_group_id.*null"
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Required runner group 'x' not found"
|
|
19
|
+
root_cause: |
|
|
20
|
+
In autoscaling self-hosted runner setups (EC2, GKE ephemeral runners, ARC), the runner
|
|
21
|
+
must register with GitHub BEFORE the job dispatcher resolves runner group membership.
|
|
22
|
+
|
|
23
|
+
The race condition occurs when:
|
|
24
|
+
1. A workflow is triggered and GitHub's broker immediately tries to assign the job
|
|
25
|
+
2. The ephemeral runner is still initializing (EC2 bootstrap, container pull, ~2:30 min)
|
|
26
|
+
3. The broker resolves runner group membership before the new runner completes registration
|
|
27
|
+
4. The broker reports "Required runner group 'X' not found" and fails the job
|
|
28
|
+
|
|
29
|
+
This is intermittent: matrix jobs expose it clearly because some cells get
|
|
30
|
+
already-running (pre-registered) runners while others need a fresh runner, triggering
|
|
31
|
+
the race. Inspecting the failed job via the Jobs API shows `runner_group_id: null`
|
|
32
|
+
and `runner_name: null` throughout the queue duration even though the runner group
|
|
33
|
+
exists and has the correct repository access.
|
|
34
|
+
|
|
35
|
+
A separate but related pattern occurs with org-level runner group repository access
|
|
36
|
+
grants not propagating to the broker V2 protocol in time, causing identical symptoms
|
|
37
|
+
regardless of runner initialization speed.
|
|
38
|
+
|
|
39
|
+
Reported upstream: https://github.com/actions/runner/issues/4252
|
|
40
|
+
Related: https://github.com/actions/runner/issues/4429
|
|
41
|
+
fix: |
|
|
42
|
+
For ephemeral autoscaling runners:
|
|
43
|
+
Implement a registration wait loop that polls the GitHub Runners API before signaling
|
|
44
|
+
the runner as available. The runner should only become eligible for jobs after the
|
|
45
|
+
broker has acknowledged its registration.
|
|
46
|
+
|
|
47
|
+
For org-level runner group access issues:
|
|
48
|
+
Verify that the target repository is in the runner group's allowed repositories list
|
|
49
|
+
via API. If misconfigured, re-registering the runner at repository level instead of
|
|
50
|
+
org level is a reliable workaround.
|
|
51
|
+
|
|
52
|
+
General mitigation: Add timeout-minutes to all jobs on self-hosted runners so stuck
|
|
53
|
+
queued jobs fail fast rather than waiting until the 6-hour workflow timeout.
|
|
54
|
+
fix_code:
|
|
55
|
+
- language: yaml
|
|
56
|
+
label: "Add timeout-minutes to detect stuck queued jobs quickly"
|
|
57
|
+
code: |
|
|
58
|
+
jobs:
|
|
59
|
+
build:
|
|
60
|
+
runs-on:
|
|
61
|
+
group: my-runner-group
|
|
62
|
+
labels: [self-hosted, linux]
|
|
63
|
+
timeout-minutes: 10 # Fail fast if runner never picks up the job
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@v4
|
|
66
|
+
- run: echo "Runner assigned successfully"
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: "Verify runner group repository access via API"
|
|
69
|
+
code: |
|
|
70
|
+
jobs:
|
|
71
|
+
debug-runner-group:
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
steps:
|
|
74
|
+
- name: Check runner group repository access
|
|
75
|
+
env:
|
|
76
|
+
GH_TOKEN: ${{ secrets.ORG_RUNNER_READ_TOKEN }}
|
|
77
|
+
run: |
|
|
78
|
+
echo "Runner groups and their visibility:"
|
|
79
|
+
gh api /orgs/${{ github.repository_owner }}/actions/runner-groups \
|
|
80
|
+
--jq '.runner_groups[] | "\(.name) (id: \(.id)) — visibility: \(.visibility)"'
|
|
81
|
+
|
|
82
|
+
echo "Repositories allowed for runner group ID 1:"
|
|
83
|
+
gh api /orgs/${{ github.repository_owner }}/actions/runner-groups/1/repositories \
|
|
84
|
+
--jq '.repositories[].full_name'
|
|
85
|
+
prevention:
|
|
86
|
+
- "Add timeout-minutes to all jobs using self-hosted runners so stuck-queued jobs fail fast instead of waiting for the 6h workflow limit"
|
|
87
|
+
- "For ephemeral runners (EC2/ARC), implement a registration health check that polls the Runners API before the runner accepts jobs"
|
|
88
|
+
- "For org-level runners, verify group repository access via API after any runner group configuration change"
|
|
89
|
+
- "For matrix jobs with ephemeral runners, keep N idle pre-registered runners to avoid cold-start races"
|
|
90
|
+
- "Monitor runner_group_id via the Jobs API to detect dispatch failures early in autoscaling pipelines"
|
|
91
|
+
docs:
|
|
92
|
+
- url: "https://github.com/actions/runner/issues/4252"
|
|
93
|
+
label: "actions/runner #4252 — Intermittent runner group not found"
|
|
94
|
+
- url: "https://github.com/actions/runner/issues/4429"
|
|
95
|
+
label: "actions/runner #4429 — Org-level runner never dispatched"
|
|
96
|
+
- url: "https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/managing-access-to-self-hosted-runners-using-groups"
|
|
97
|
+
label: "GitHub Docs — Managing runner group access"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
id: "runner-environment-022"
|
|
2
|
+
title: "actions/checkout set-safe-directory only runs in post step — container jobs get dubious ownership errors"
|
|
3
|
+
category: "runner-environment"
|
|
4
|
+
severity: "error"
|
|
5
|
+
tags:
|
|
6
|
+
- "actions/checkout"
|
|
7
|
+
- "safe-directory"
|
|
8
|
+
- "container"
|
|
9
|
+
- "dubious-ownership"
|
|
10
|
+
- "CVE-2022-24765"
|
|
11
|
+
- "post-step"
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "fatal: detected dubious ownership in repository"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "safe\\.directory.*not.*owned"
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "fatal: detected dubious ownership in repository at '/github/workspace'"
|
|
19
|
+
- "hint: git config --global --add safe.directory /github/workspace"
|
|
20
|
+
root_cause: |
|
|
21
|
+
The `actions/checkout` action configures `safe.directory` to allow git operations in
|
|
22
|
+
the workspace. However, this configuration only runs in the **post step** (cleanup
|
|
23
|
+
phase), not during the main execution step.
|
|
24
|
+
|
|
25
|
+
In container jobs, the workspace is mounted from the host and may be owned by a
|
|
26
|
+
different UID than the user running inside the container. Git's safe.directory
|
|
27
|
+
protection (introduced in Git 2.35.2 for CVE-2022-24765) blocks access when the
|
|
28
|
+
directory owner differs from the running user.
|
|
29
|
+
|
|
30
|
+
Because safe.directory is only written during the post step — after all workflow
|
|
31
|
+
steps have already run — any subsequent git operations in the job's main steps
|
|
32
|
+
fail with "fatal: detected dubious ownership". This includes third-party actions
|
|
33
|
+
that internally invoke git (reviewdog, gitops tools, semantic-release, etc.).
|
|
34
|
+
|
|
35
|
+
Reported upstream: https://github.com/actions/checkout/issues/2031
|
|
36
|
+
fix: |
|
|
37
|
+
Add an explicit safe.directory configuration step immediately after `actions/checkout`
|
|
38
|
+
in any container job that performs git operations. This ensures the directory is
|
|
39
|
+
trusted before any subsequent steps run.
|
|
40
|
+
fix_code:
|
|
41
|
+
- language: yaml
|
|
42
|
+
label: "Add safe.directory config step after checkout in container jobs"
|
|
43
|
+
code: |
|
|
44
|
+
jobs:
|
|
45
|
+
build:
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
container: node:20-bookworm
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
|
|
51
|
+
# Workaround: post step safe.directory config doesn't help in container jobs
|
|
52
|
+
- name: Mark workspace as safe for git
|
|
53
|
+
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
|
54
|
+
|
|
55
|
+
- name: Run git-dependent steps
|
|
56
|
+
run: git log --oneline -5
|
|
57
|
+
- language: yaml
|
|
58
|
+
label: "Use wildcard to mark all directories safe in container workflows"
|
|
59
|
+
code: |
|
|
60
|
+
jobs:
|
|
61
|
+
build:
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
container: python:3.12-slim
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@v4
|
|
66
|
+
|
|
67
|
+
- name: Configure git safe directories
|
|
68
|
+
run: |
|
|
69
|
+
git config --global --add safe.directory '*'
|
|
70
|
+
|
|
71
|
+
- name: Lint with pre-commit
|
|
72
|
+
run: pre-commit run --all-files
|
|
73
|
+
prevention:
|
|
74
|
+
- "Always add a safe.directory config step after checkout when using container jobs"
|
|
75
|
+
- "Audit third-party actions in container jobs — reviewdog, semantic-release, and gitops tools invoke git internally"
|
|
76
|
+
- "Consider running without a container and using docker run explicitly if git safety is complex to manage"
|
|
77
|
+
- "Track https://github.com/actions/checkout/issues/2031 for an official fix from the actions team"
|
|
78
|
+
docs:
|
|
79
|
+
- url: "https://github.com/actions/checkout/issues/2031"
|
|
80
|
+
label: "actions/checkout #2031 — safe.directory only set in post step"
|
|
81
|
+
- url: "https://github.blog/2022-04-12-git-security-vulnerability-announced/"
|
|
82
|
+
label: "Git CVE-2022-24765 — safe.directory background"
|
|
83
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-where-your-workflow-runs/running-jobs-in-a-container"
|
|
84
|
+
label: "GitHub Docs — Running jobs in a container"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
id: runner-environment-024
|
|
2
|
+
title: "Docker Hub Pull Rate Limit — toomanyrequests in GitHub Actions"
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- docker
|
|
7
|
+
- docker-hub
|
|
8
|
+
- rate-limit
|
|
9
|
+
- container
|
|
10
|
+
- pull
|
|
11
|
+
- authentication
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "toomanyrequests: You have reached your pull rate limit"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "You have reached your unauthenticated pull rate limit"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "pull rate limit reached"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "ERROR: toomanyrequests"
|
|
20
|
+
flags: "i"
|
|
21
|
+
- regex: "429 Too Many Requests.*docker"
|
|
22
|
+
flags: "i"
|
|
23
|
+
error_messages:
|
|
24
|
+
- "toomanyrequests: You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit"
|
|
25
|
+
- "Error response from daemon: toomanyrequests: You have reached your unauthenticated pull rate limit."
|
|
26
|
+
- "pull access denied, repository does not exist or may require 'docker login'"
|
|
27
|
+
root_cause: |
|
|
28
|
+
Docker Hub enforces anonymous pull rate limits on all IP addresses. GitHub-hosted
|
|
29
|
+
runners share a pool of egress IPs — multiple concurrent workflow runs from different
|
|
30
|
+
repositories hit Docker Hub from the same IP address, exhausting the shared quota
|
|
31
|
+
rapidly.
|
|
32
|
+
|
|
33
|
+
**Rate limit tiers (as of 2024):**
|
|
34
|
+
- **Unauthenticated (anonymous):** 100 pulls per 6 hours per IP
|
|
35
|
+
- **Authenticated free account:** 200 pulls per 6 hours per account
|
|
36
|
+
- **Docker Hub Pro/Team/Business:** unlimited
|
|
37
|
+
|
|
38
|
+
Because GitHub-hosted runners use a small set of shared egress IPs, popular runners
|
|
39
|
+
can hit the anonymous 100-pull limit within minutes during peak hours. The error
|
|
40
|
+
surfaces as a hard failure when the runner attempts `docker pull` for job containers,
|
|
41
|
+
service containers, container actions, or explicit `docker pull` commands in steps.
|
|
42
|
+
|
|
43
|
+
Even after adding `docker/login-action`, workflows can still hit the limit if they use
|
|
44
|
+
actions that internally pull images (e.g., `docker/build-push-action`, `docker/bake-action`)
|
|
45
|
+
before the login step has executed.
|
|
46
|
+
fix: |
|
|
47
|
+
Add `docker/login-action` as the **first step** in any job that pulls from Docker Hub.
|
|
48
|
+
The action authenticates with Docker Hub, switching from anonymous to authenticated rate
|
|
49
|
+
limits (200 pulls/6 hours per account) or unlimited for Pro/Team accounts.
|
|
50
|
+
|
|
51
|
+
**Recommended approach:**
|
|
52
|
+
1. Create a Docker Hub account (free tier) or use an existing one.
|
|
53
|
+
2. Store credentials as repository secrets: `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN`
|
|
54
|
+
(use an Access Token, not your password — generate at hub.docker.com > Account Settings > Security).
|
|
55
|
+
3. Add the login step before any image-pulling step.
|
|
56
|
+
|
|
57
|
+
**For self-hosted runners:** Configure a Docker Hub mirror or use GitHub Packages /
|
|
58
|
+
Amazon ECR / Google Artifact Registry to cache images and avoid Docker Hub entirely.
|
|
59
|
+
fix_code:
|
|
60
|
+
- language: yaml
|
|
61
|
+
label: "Authenticate with Docker Hub before pulling images"
|
|
62
|
+
code: |
|
|
63
|
+
jobs:
|
|
64
|
+
build:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
# Login FIRST — before any step that pulls from Docker Hub
|
|
68
|
+
- name: Log in to Docker Hub
|
|
69
|
+
uses: docker/login-action@v3
|
|
70
|
+
with:
|
|
71
|
+
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
72
|
+
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
73
|
+
|
|
74
|
+
- uses: actions/checkout@v4
|
|
75
|
+
|
|
76
|
+
- name: Build and push
|
|
77
|
+
uses: docker/build-push-action@v5
|
|
78
|
+
with:
|
|
79
|
+
push: true
|
|
80
|
+
tags: myorg/myimage:latest
|
|
81
|
+
- language: yaml
|
|
82
|
+
label: "Authenticate for jobs using a container image"
|
|
83
|
+
code: |
|
|
84
|
+
jobs:
|
|
85
|
+
test:
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
container:
|
|
88
|
+
image: python:3.12
|
|
89
|
+
credentials:
|
|
90
|
+
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
91
|
+
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
92
|
+
steps:
|
|
93
|
+
- uses: actions/checkout@v4
|
|
94
|
+
- run: python -m pytest
|
|
95
|
+
- language: yaml
|
|
96
|
+
label: "Authenticate for service containers pulling from Docker Hub"
|
|
97
|
+
code: |
|
|
98
|
+
jobs:
|
|
99
|
+
test:
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
services:
|
|
102
|
+
postgres:
|
|
103
|
+
image: postgres:16
|
|
104
|
+
credentials:
|
|
105
|
+
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
106
|
+
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
107
|
+
env:
|
|
108
|
+
POSTGRES_PASSWORD: postgres
|
|
109
|
+
steps:
|
|
110
|
+
- uses: actions/checkout@v4
|
|
111
|
+
prevention:
|
|
112
|
+
- "Store a Docker Hub access token (not password) in secrets and use docker/login-action@v3 in all jobs that pull images."
|
|
113
|
+
- "Place the docker/login-action step first in the job — before any action that internally pulls Docker Hub images."
|
|
114
|
+
- "Consider mirroring frequently-used base images to GitHub Container Registry (ghcr.io) or your cloud provider's registry to eliminate Docker Hub dependency."
|
|
115
|
+
- "Use Docker Hub's Automated Builds or a scheduled workflow to keep mirror images up to date."
|
|
116
|
+
- "For service containers and job containers, always provide `credentials:` block alongside `image:` when pulling from Docker Hub."
|
|
117
|
+
docs:
|
|
118
|
+
- url: "https://docs.docker.com/docker-hub/download-rate-limit/"
|
|
119
|
+
label: "Docker Hub: Download rate limit documentation"
|
|
120
|
+
- url: "https://github.com/docker/login-action"
|
|
121
|
+
label: "docker/login-action — GitHub Marketplace"
|
|
122
|
+
- url: "https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-docker-images"
|
|
123
|
+
label: "GitHub Docs: Publishing Docker images"
|
|
124
|
+
- url: "https://github.com/actions/runner-images/issues/1445"
|
|
125
|
+
label: "actions/runner-images #1445 — Did Docker Hub rate limit affect GitHub Action?"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
id: "runner-environment-023"
|
|
2
|
+
title: "Self-hosted runner on deprecated version stops receiving jobs"
|
|
3
|
+
category: "runner-environment"
|
|
4
|
+
severity: "error"
|
|
5
|
+
tags:
|
|
6
|
+
- "self-hosted"
|
|
7
|
+
- "runner"
|
|
8
|
+
- "deprecated"
|
|
9
|
+
- "version"
|
|
10
|
+
- "cannot-receive-messages"
|
|
11
|
+
- "maintenance"
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "Runner version v\\d+\\.\\d+\\.\\d+ is deprecated and cannot receive messages"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "WRITE ERROR.*runner.*deprecated"
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Runner version v2.332.0 is deprecated and cannot receive messages."
|
|
19
|
+
- "WRITE ERROR: An error occured: Runner version v2.XXX.X is deprecated and cannot receive messages."
|
|
20
|
+
root_cause: |
|
|
21
|
+
GitHub periodically deprecates older self-hosted runner versions. Once a runner version
|
|
22
|
+
is past its deprecation deadline, the runner agent can no longer communicate with the
|
|
23
|
+
GitHub Actions broker service.
|
|
24
|
+
|
|
25
|
+
The runner process stays alive, appears online in the GitHub UI (Settings → Actions →
|
|
26
|
+
Runners), and is listed as "Active" — but it can no longer receive job assignments.
|
|
27
|
+
Jobs queued for a runner group containing only deprecated-version runners will either
|
|
28
|
+
stay "Queued" indefinitely or time out without a clear error in the workflow UI.
|
|
29
|
+
|
|
30
|
+
This is a silent failure mode: the runner shows as online, no workflow error is
|
|
31
|
+
surfaced, but jobs never start. The deprecation schedule is published in the GitHub
|
|
32
|
+
Changelog and actions/runner releases but teams often miss it without automated
|
|
33
|
+
update pipelines.
|
|
34
|
+
|
|
35
|
+
As of 2026, GitHub requires runners to be within approximately the last 6 months of
|
|
36
|
+
releases. Related issues: actions/runner #4305, actions/runner #4442
|
|
37
|
+
fix: |
|
|
38
|
+
Update the runner binary to a currently supported version.
|
|
39
|
+
|
|
40
|
+
For manually managed runners:
|
|
41
|
+
1. SSH to the runner host
|
|
42
|
+
2. Stop the runner service: ./svc.sh stop
|
|
43
|
+
3. Download the latest runner from https://github.com/actions/runner/releases/latest
|
|
44
|
+
4. Extract to the runner directory (configuration is preserved via .runner file)
|
|
45
|
+
5. Restart: ./svc.sh start
|
|
46
|
+
|
|
47
|
+
For ARC (Actions Runner Controller) or autoscaling solutions: bump the runner image
|
|
48
|
+
version tag in your HelmRelease or Deployment manifest and redeploy.
|
|
49
|
+
fix_code:
|
|
50
|
+
- language: yaml
|
|
51
|
+
label: "Scheduled workflow to alert on outdated runner versions"
|
|
52
|
+
code: |
|
|
53
|
+
name: Check self-hosted runner versions
|
|
54
|
+
on:
|
|
55
|
+
schedule:
|
|
56
|
+
- cron: '0 9 * * 1' # Weekly Monday 9 AM
|
|
57
|
+
|
|
58
|
+
jobs:
|
|
59
|
+
check-runners:
|
|
60
|
+
runs-on: ubuntu-latest # GitHub-hosted runner for this diagnostic
|
|
61
|
+
steps:
|
|
62
|
+
- name: List runner versions via API
|
|
63
|
+
env:
|
|
64
|
+
GH_TOKEN: ${{ secrets.RUNNER_READ_TOKEN }}
|
|
65
|
+
run: |
|
|
66
|
+
echo "=== Org runners ==="
|
|
67
|
+
gh api /orgs/${{ github.repository_owner }}/actions/runners \
|
|
68
|
+
--jq '.runners[] | "\(.name): v\(.version) (\(.status))"'
|
|
69
|
+
|
|
70
|
+
- name: Check latest available version
|
|
71
|
+
run: |
|
|
72
|
+
LATEST=$(curl -sf https://api.github.com/repos/actions/runner/releases/latest | jq -r .tag_name)
|
|
73
|
+
echo "Latest runner version: $LATEST"
|
|
74
|
+
echo "Compare against your registered runners above"
|
|
75
|
+
- language: yaml
|
|
76
|
+
label: "ARC — bump runner version in HelmRelease"
|
|
77
|
+
code: |
|
|
78
|
+
# In your ARC HelmRelease or values.yaml
|
|
79
|
+
githubConfigUrl: "https://github.com/myorg"
|
|
80
|
+
template:
|
|
81
|
+
spec:
|
|
82
|
+
containers:
|
|
83
|
+
- name: runner
|
|
84
|
+
image: ghcr.io/actions/actions-runner:2.335.0 # Bump this regularly
|
|
85
|
+
prevention:
|
|
86
|
+
- "Subscribe to the GitHub Changelog (https://github.blog/changelog/) or watch actions/runner releases for deprecation notices"
|
|
87
|
+
- "Use Actions Runner Controller (ARC) or an autoscaling solution to automate runner lifecycle management"
|
|
88
|
+
- "Schedule a weekly cron workflow that checks registered runner versions via the Runners API and alerts if any are outdated"
|
|
89
|
+
- "Pin runner version in IaC (Terraform, Ansible) and include a runner version bump in your monthly maintenance checklist"
|
|
90
|
+
- "Set up Dependabot or Renovate to auto-update runner image tags in Docker/ARC manifests"
|
|
91
|
+
docs:
|
|
92
|
+
- url: "https://github.com/actions/runner/releases"
|
|
93
|
+
label: "actions/runner releases — version history and changelogs"
|
|
94
|
+
- url: "https://github.com/actions/runner/issues/4305"
|
|
95
|
+
label: "actions/runner #4305 — runner deprecated cannot receive messages"
|
|
96
|
+
- url: "https://github.com/actions/runner/issues/4442"
|
|
97
|
+
label: "actions/runner #4442 — version deprecation notice"
|
|
98
|
+
- url: "https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners"
|
|
99
|
+
label: "GitHub Docs — About self-hosted runners"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
id: runner-environment-025
|
|
2
|
+
title: "Service Container Network: localhost Works on Runner, Service Name Required in Container Jobs"
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- service-containers
|
|
7
|
+
- networking
|
|
8
|
+
- localhost
|
|
9
|
+
- postgres
|
|
10
|
+
- mysql
|
|
11
|
+
- redis
|
|
12
|
+
- container-jobs
|
|
13
|
+
- docker-networking
|
|
14
|
+
patterns:
|
|
15
|
+
- regex: "Connection refused.*(?:localhost|127\\.0\\.0\\.1).*(?:5432|3306|6379|27017)"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "could not connect to server.*Connection refused.*localhost"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "ECONNREFUSED.*(?:5432|3306|6379|27017)"
|
|
20
|
+
flags: "i"
|
|
21
|
+
- regex: "dial tcp 127\\.0\\.0\\.1:\\d+: connect: connection refused"
|
|
22
|
+
flags: "i"
|
|
23
|
+
error_messages:
|
|
24
|
+
- "Error: connect ECONNREFUSED 127.0.0.1:5432"
|
|
25
|
+
- "could not connect to server: Connection refused (Is the server running on host \"localhost\" (127.0.0.1) and accepting TCP/IP connections on port 5432?)"
|
|
26
|
+
- "OperationalError: connection to server at \"localhost\" (127.0.0.1), port 5432 failed: Connection refused"
|
|
27
|
+
- "Error: getaddrinfo ENOTFOUND postgres"
|
|
28
|
+
root_cause: |
|
|
29
|
+
GitHub Actions service containers run as Docker containers on the runner machine.
|
|
30
|
+
The correct hostname to reach them depends on **how the job itself runs**:
|
|
31
|
+
|
|
32
|
+
**Scenario A — Job runs directly on the runner (no `container:` key):**
|
|
33
|
+
The runner process runs natively on the VM. Docker maps service container ports to the
|
|
34
|
+
runner's loopback interface. Services are reachable at `localhost:<mapped_port>`.
|
|
35
|
+
Using the service name (e.g. `postgres`) as a hostname will **fail** with ENOTFOUND.
|
|
36
|
+
|
|
37
|
+
**Scenario B — Job runs inside a container (`container:` key present):**
|
|
38
|
+
Both the job container and service containers are on the same Docker user-defined network.
|
|
39
|
+
Docker's built-in DNS resolves container names. Services are reachable by their
|
|
40
|
+
**service name** (e.g. `postgres`, `redis`) as the hostname, NOT `localhost`.
|
|
41
|
+
Using `localhost` here will produce "Connection refused" because the service port is
|
|
42
|
+
not bound to the job container's loopback interface.
|
|
43
|
+
|
|
44
|
+
This asymmetry is the root of most service container connectivity bugs:
|
|
45
|
+
developers copy a working non-containerized workflow config into a containerized job
|
|
46
|
+
(or vice versa) without updating the hostname.
|
|
47
|
+
|
|
48
|
+
Additionally, when using `ports:` mapping in service definitions, the runner maps the
|
|
49
|
+
container port to a randomly assigned host port (unless pinned with `host:container`
|
|
50
|
+
syntax). The `${{ job.services.postgres.ports['5432'] }}` expression returns the
|
|
51
|
+
actual host-side port, which may differ from 5432.
|
|
52
|
+
fix: |
|
|
53
|
+
**For jobs running directly on the runner (no `container:` key):**
|
|
54
|
+
Connect to services via `localhost` (or `127.0.0.1`) and the mapped port.
|
|
55
|
+
Use `${{ job.services.<service>.ports['<container_port>'] }}` to get the dynamic host port
|
|
56
|
+
when not using explicit port pinning.
|
|
57
|
+
|
|
58
|
+
**For jobs running in a container (`container:` key):**
|
|
59
|
+
Connect to services via the **service name** as the hostname and the container's
|
|
60
|
+
internal port directly. No port mapping is required — both containers share the same
|
|
61
|
+
Docker network and communicate directly.
|
|
62
|
+
|
|
63
|
+
Use the `options: --add-host` trick only if you need the same workflow YAML to work
|
|
64
|
+
in both contexts (rare — avoid this complexity).
|
|
65
|
+
fix_code:
|
|
66
|
+
- language: yaml
|
|
67
|
+
label: "Correct connectivity for a runner-native job (no container:)"
|
|
68
|
+
code: |
|
|
69
|
+
jobs:
|
|
70
|
+
test:
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
# No 'container:' key — job runs directly on the runner VM
|
|
73
|
+
services:
|
|
74
|
+
postgres:
|
|
75
|
+
image: postgres:16
|
|
76
|
+
env:
|
|
77
|
+
POSTGRES_PASSWORD: postgres
|
|
78
|
+
ports:
|
|
79
|
+
- 5432:5432 # pin host:container port for simplicity
|
|
80
|
+
options: >-
|
|
81
|
+
--health-cmd pg_isready
|
|
82
|
+
--health-interval 10s
|
|
83
|
+
--health-timeout 5s
|
|
84
|
+
--health-retries 5
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
- name: Run tests
|
|
88
|
+
env:
|
|
89
|
+
# Use localhost — runner-native job maps service ports to loopback
|
|
90
|
+
DATABASE_URL: postgres://postgres:postgres@localhost:5432/testdb
|
|
91
|
+
run: npm test
|
|
92
|
+
- language: yaml
|
|
93
|
+
label: "Correct connectivity for a containerized job (container: key present)"
|
|
94
|
+
code: |
|
|
95
|
+
jobs:
|
|
96
|
+
test:
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
container:
|
|
99
|
+
image: node:20 # Job itself runs inside a container
|
|
100
|
+
services:
|
|
101
|
+
postgres:
|
|
102
|
+
image: postgres:16
|
|
103
|
+
env:
|
|
104
|
+
POSTGRES_PASSWORD: postgres
|
|
105
|
+
# NO ports: mapping needed — containers share the Docker network
|
|
106
|
+
options: >-
|
|
107
|
+
--health-cmd pg_isready
|
|
108
|
+
--health-interval 10s
|
|
109
|
+
--health-timeout 5s
|
|
110
|
+
--health-retries 5
|
|
111
|
+
steps:
|
|
112
|
+
- uses: actions/checkout@v4
|
|
113
|
+
- name: Run tests
|
|
114
|
+
env:
|
|
115
|
+
# Use service NAME "postgres" — Docker DNS resolves it within the network
|
|
116
|
+
DATABASE_URL: postgres://postgres:postgres@postgres:5432/testdb
|
|
117
|
+
run: npm test
|
|
118
|
+
prevention:
|
|
119
|
+
- "When your job uses `container:`, always connect to services via the service name (e.g. `postgres`, `redis`), NOT `localhost`."
|
|
120
|
+
- "When your job runs natively (no `container:`), always connect to services via `localhost` and the mapped host port."
|
|
121
|
+
- "Use `options: --health-cmd / --health-interval / --health-retries` on service containers to ensure they are ready before steps execute."
|
|
122
|
+
- "Avoid using `localhost` as the database hostname in connection strings — parameterize it so the same code works in both runner-native and containerized jobs."
|
|
123
|
+
- "If you move a job from runner-native to containerized (or vice versa), always audit all service hostnames in env variables and connection strings."
|
|
124
|
+
docs:
|
|
125
|
+
- url: "https://docs.github.com/en/actions/use-cases-and-examples/using-containerized-services/about-service-containers"
|
|
126
|
+
label: "GitHub Docs: About service containers"
|
|
127
|
+
- url: "https://docs.github.com/en/actions/use-cases-and-examples/using-containerized-services/creating-postgresql-service-containers"
|
|
128
|
+
label: "GitHub Docs: Creating PostgreSQL service containers"
|
|
129
|
+
- url: "https://docs.github.com/en/actions/use-cases-and-examples/using-containerized-services/creating-redis-service-containers"
|
|
130
|
+
label: "GitHub Docs: Creating Redis service containers"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
id: runner-environment-026
|
|
2
|
+
title: "ubuntu-latest Switched to Ubuntu 24.04 — Breaks Python 3.8/3.9, System Packages, OpenSSL"
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- ubuntu
|
|
7
|
+
- ubuntu-24.04
|
|
8
|
+
- runner-image
|
|
9
|
+
- python
|
|
10
|
+
- openssl
|
|
11
|
+
- apt
|
|
12
|
+
- migration
|
|
13
|
+
- breaking-change
|
|
14
|
+
patterns:
|
|
15
|
+
- regex: "Unable to locate package python3\\.[89]"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "python3\\.[89].*not found.*ubuntu.*24"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "E: Package 'python3\\.[89]' has no installation candidate"
|
|
20
|
+
flags: "i"
|
|
21
|
+
- regex: "error.*openssl.*libssl1\\.1.*not available"
|
|
22
|
+
flags: "i"
|
|
23
|
+
- regex: "deadsnakes.*ppa.*focal.*not available.*noble"
|
|
24
|
+
flags: "i"
|
|
25
|
+
- regex: "ubuntu-latest.*ubuntu 24"
|
|
26
|
+
flags: "i"
|
|
27
|
+
error_messages:
|
|
28
|
+
- "E: Package 'python3.8' has no installation candidate"
|
|
29
|
+
- "E: Package 'python3.9' has no installation candidate"
|
|
30
|
+
- "E: Package 'libssl1.1' has no installation candidate"
|
|
31
|
+
- "Error: The version '3.8' with architecture 'x64' was not found for Ubuntu 24.04"
|
|
32
|
+
- "Error: deadsnakes/ppa does not provide Python 3.8 packages for Ubuntu Noble (24.04)"
|
|
33
|
+
root_cause: |
|
|
34
|
+
GitHub began rolling out Ubuntu 24.04 as the new target for `ubuntu-latest` on
|
|
35
|
+
**December 5, 2024**, completing the transition on **January 17, 2025** (tracked in
|
|
36
|
+
actions/runner-images #10636, 266 👍 reactions). Any workflow using `runs-on: ubuntu-latest`
|
|
37
|
+
without explicit version pinning now runs on Ubuntu 24.04 ("Noble Numbat") instead of 22.04.
|
|
38
|
+
|
|
39
|
+
**Key breaking changes in Ubuntu 24.04:**
|
|
40
|
+
|
|
41
|
+
1. **Python 3.8 and 3.9 are end-of-life and not available** — not via `apt` and not via
|
|
42
|
+
the `deadsnakes/ppa` PPA (which only backports for supported Ubuntu releases). Workflows
|
|
43
|
+
that `apt-get install python3.8` or use `setup-python` with version `3.8` or `3.9` fail.
|
|
44
|
+
|
|
45
|
+
2. **OpenSSL 1.1 removed** — Ubuntu 24.04 ships only OpenSSL 3.x. Packages and Python
|
|
46
|
+
versions that were compiled against OpenSSL 1.1 (`libssl1.1`) will fail to install or
|
|
47
|
+
run. This affects older Ruby gems, some Go binaries, and legacy pip packages.
|
|
48
|
+
|
|
49
|
+
3. **System Python is 3.12** — the `python3` system binary points to Python 3.12, which
|
|
50
|
+
may break scripts that relied on the 3.10 default in 22.04.
|
|
51
|
+
|
|
52
|
+
4. **`apt` package name changes** — some packages were renamed or split between 22.04 and
|
|
53
|
+
24.04 (e.g., `python3-distutils` is gone; functionality merged into `python3`).
|
|
54
|
+
|
|
55
|
+
5. **`/usr/bin/python` symlink absent by default** — workflows calling bare `python`
|
|
56
|
+
(not `python3`) fail with "python: command not found".
|
|
57
|
+
fix: |
|
|
58
|
+
**Short-term — pin to ubuntu-22.04:**
|
|
59
|
+
Replace `runs-on: ubuntu-latest` with `runs-on: ubuntu-22.04` to freeze the runner image
|
|
60
|
+
while you migrate. Note: ubuntu-22.04 will eventually reach end-of-support (~April 2027).
|
|
61
|
+
|
|
62
|
+
**Long-term — migrate to supported Python/package versions:**
|
|
63
|
+
- Replace Python 3.8/3.9 with Python 3.10, 3.11, or 3.12 using `actions/setup-python`.
|
|
64
|
+
`actions/setup-python` downloads a pre-built binary from the tool cache — it works
|
|
65
|
+
regardless of the runner's system Python.
|
|
66
|
+
- Replace `libssl1.1` dependencies by upgrading the affected package to an OpenSSL-3
|
|
67
|
+
compatible version, or adding a compatibility shim if available.
|
|
68
|
+
- Replace bare `python` calls with `python3`.
|
|
69
|
+
|
|
70
|
+
**For `deadsnakes` PPA users:** `deadsnakes/ppa` only provides Python 3.8/3.9 for
|
|
71
|
+
Ubuntu 20.04 (Focal) and 22.04 (Jammy) — not 24.04 (Noble). There is no supported
|
|
72
|
+
workaround; upgrade to a supported Python version.
|
|
73
|
+
fix_code:
|
|
74
|
+
- language: yaml
|
|
75
|
+
label: "Pin to ubuntu-22.04 as a short-term fix while migrating"
|
|
76
|
+
code: |
|
|
77
|
+
jobs:
|
|
78
|
+
test:
|
|
79
|
+
# Pin explicitly until Python 3.8/3.9 dependencies are upgraded
|
|
80
|
+
runs-on: ubuntu-22.04
|
|
81
|
+
steps:
|
|
82
|
+
- uses: actions/checkout@v4
|
|
83
|
+
- run: python3.9 -m pytest
|
|
84
|
+
- language: yaml
|
|
85
|
+
label: "Use actions/setup-python to install any Python version on ubuntu-latest"
|
|
86
|
+
code: |
|
|
87
|
+
jobs:
|
|
88
|
+
test:
|
|
89
|
+
runs-on: ubuntu-latest # Now 24.04
|
|
90
|
+
steps:
|
|
91
|
+
- uses: actions/checkout@v4
|
|
92
|
+
- name: Set up Python
|
|
93
|
+
uses: actions/setup-python@v5
|
|
94
|
+
with:
|
|
95
|
+
# setup-python downloads pre-built binaries — works on any runner OS
|
|
96
|
+
python-version: '3.9'
|
|
97
|
+
- run: python --version # 3.9.x from setup-python's tool cache
|
|
98
|
+
- run: pip install -r requirements.txt
|
|
99
|
+
- language: yaml
|
|
100
|
+
label: "Test matrix against multiple Python versions to find breakages early"
|
|
101
|
+
code: |
|
|
102
|
+
jobs:
|
|
103
|
+
test:
|
|
104
|
+
runs-on: ubuntu-latest
|
|
105
|
+
strategy:
|
|
106
|
+
matrix:
|
|
107
|
+
python-version: ['3.9', '3.10', '3.11', '3.12']
|
|
108
|
+
steps:
|
|
109
|
+
- uses: actions/checkout@v4
|
|
110
|
+
- uses: actions/setup-python@v5
|
|
111
|
+
with:
|
|
112
|
+
python-version: ${{ matrix.python-version }}
|
|
113
|
+
- run: pip install -r requirements.txt && pytest
|
|
114
|
+
- language: yaml
|
|
115
|
+
label: "Fix bare 'python' command not found on Ubuntu 24.04"
|
|
116
|
+
code: |
|
|
117
|
+
steps:
|
|
118
|
+
- name: Ensure python symlink exists
|
|
119
|
+
run: |
|
|
120
|
+
# Ubuntu 24.04 no longer creates /usr/bin/python by default
|
|
121
|
+
sudo ln -sf /usr/bin/python3 /usr/bin/python
|
|
122
|
+
# Or: use actions/setup-python which sets up the PATH correctly
|
|
123
|
+
prevention:
|
|
124
|
+
- "Pin `runs-on: ubuntu-22.04` explicitly if your workflow depends on Python 3.8/3.9, libssl1.1, or other 22.04-specific packages."
|
|
125
|
+
- "Use `actions/setup-python@v5` to install a specific Python version instead of relying on `apt-get install python3.x` — it works across all Ubuntu versions."
|
|
126
|
+
- "When you upgrade Ubuntu runner versions, scan `apt-get install` lines in run steps for packages that may not exist in the new Ubuntu release."
|
|
127
|
+
- "Replace bare `python` invocations with `python3` or use `actions/setup-python` which adds an alias."
|
|
128
|
+
- "Test your workflows against `ubuntu-24.04` explicitly before `ubuntu-latest` changes, by running on both runner images in a matrix."
|
|
129
|
+
- "Subscribe to https://github.com/actions/runner-images/issues for announcements about upcoming `ubuntu-latest` image transitions."
|
|
130
|
+
docs:
|
|
131
|
+
- url: "https://github.com/actions/runner-images/issues/10636"
|
|
132
|
+
label: "actions/runner-images #10636 — Ubuntu-latest workflows will use Ubuntu-24.04 image"
|
|
133
|
+
- url: "https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md"
|
|
134
|
+
label: "Ubuntu 24.04 runner image — installed software reference"
|
|
135
|
+
- url: "https://github.com/actions/setup-python"
|
|
136
|
+
label: "actions/setup-python — install any Python version on any runner"
|
|
137
|
+
- url: "https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources"
|
|
138
|
+
label: "GitHub Docs: Supported runners and hardware resources"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
id: "silent-failures-010"
|
|
2
|
+
title: "cache-hit output is 'true' on restore-keys partial match, not just exact key"
|
|
3
|
+
category: "silent-failures"
|
|
4
|
+
severity: "silent-failure"
|
|
5
|
+
tags:
|
|
6
|
+
- "actions/cache"
|
|
7
|
+
- "cache-hit"
|
|
8
|
+
- "restore-keys"
|
|
9
|
+
- "partial-match"
|
|
10
|
+
- "output"
|
|
11
|
+
- "exact-match"
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "cache-hit.*true"
|
|
14
|
+
flags: "i"
|
|
15
|
+
error_messages:
|
|
16
|
+
- "cache-hit: true"
|
|
17
|
+
root_cause: |
|
|
18
|
+
The `cache-hit` output of `actions/cache` is documented to return `true` for an exact
|
|
19
|
+
cache key match. In practice, `cache-hit` also returns `true` when the key matched via
|
|
20
|
+
`restore-keys` (a partial/fallback match), not only for exact key hits.
|
|
21
|
+
|
|
22
|
+
Workflows that gate post-build steps on `steps.cache.outputs.cache-hit == 'true'` to
|
|
23
|
+
skip dependency installs assume an exact match. When a restore-keys match occurs,
|
|
24
|
+
`cache-hit` is `true` even though the cache may be stale or from a different branch.
|
|
25
|
+
The correct exact-match indicator is the `exact-match` output (available in
|
|
26
|
+
actions/cache v4+) or comparing `cache-matched-key` against the computed key.
|
|
27
|
+
|
|
28
|
+
Reported upstream: https://github.com/actions/cache/issues/1675
|
|
29
|
+
fix: |
|
|
30
|
+
Use the `exact-match` output (actions/cache v4+) to determine if the restore was an
|
|
31
|
+
exact key match. `cache-hit` alone does not distinguish between exact and partial
|
|
32
|
+
(restore-keys) matches.
|
|
33
|
+
|
|
34
|
+
Alternatively, compare the `cache-matched-key` output against the expected key to
|
|
35
|
+
determine if the restore was exact.
|
|
36
|
+
fix_code:
|
|
37
|
+
- language: yaml
|
|
38
|
+
label: "Use exact-match output (actions/cache v4+)"
|
|
39
|
+
code: |
|
|
40
|
+
- name: Restore cache
|
|
41
|
+
id: cache
|
|
42
|
+
uses: actions/cache@v4
|
|
43
|
+
with:
|
|
44
|
+
path: ~/.npm
|
|
45
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
46
|
+
restore-keys: |
|
|
47
|
+
${{ runner.os }}-node-
|
|
48
|
+
|
|
49
|
+
# Only skip install on EXACT key match, not partial restore-keys hit
|
|
50
|
+
- name: Install dependencies
|
|
51
|
+
if: steps.cache.outputs.exact-match != 'true'
|
|
52
|
+
run: npm ci
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: "Compare cache-matched-key to verify exact hit"
|
|
55
|
+
code: |
|
|
56
|
+
- name: Restore cache
|
|
57
|
+
id: cache
|
|
58
|
+
uses: actions/cache@v4
|
|
59
|
+
with:
|
|
60
|
+
path: ~/.npm
|
|
61
|
+
key: npm-${{ hashFiles('**/package-lock.json') }}
|
|
62
|
+
restore-keys: npm-
|
|
63
|
+
|
|
64
|
+
- name: Install or skip dependencies
|
|
65
|
+
run: |
|
|
66
|
+
EXPECTED_KEY="npm-${{ hashFiles('**/package-lock.json') }}"
|
|
67
|
+
if [ "${{ steps.cache.outputs.cache-matched-key }}" = "$EXPECTED_KEY" ]; then
|
|
68
|
+
echo "Exact cache hit — skipping npm ci"
|
|
69
|
+
else
|
|
70
|
+
echo "Partial/stale restore-keys hit — running npm ci"
|
|
71
|
+
npm ci
|
|
72
|
+
fi
|
|
73
|
+
prevention:
|
|
74
|
+
- "Never rely on cache-hit == 'true' alone to skip dependency installs; it fires on partial restore-keys matches too"
|
|
75
|
+
- "Use the exact-match output (actions/cache v4+) when you need to distinguish exact vs partial cache hits"
|
|
76
|
+
- "Use cache-matched-key output to log or compare the actual key that was restored"
|
|
77
|
+
- "If using restore-keys, always validate that your skip-install condition handles partial matches correctly"
|
|
78
|
+
docs:
|
|
79
|
+
- url: "https://github.com/actions/cache/issues/1675"
|
|
80
|
+
label: "actions/cache #1675 — cache-hit true on restore-keys match"
|
|
81
|
+
- url: "https://github.com/actions/cache#outputs"
|
|
82
|
+
label: "actions/cache outputs documentation"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
id: silent-failures-011
|
|
2
|
+
title: "GITHUB_OUTPUT Multiline Values Silently Truncated — Heredoc EOF Required"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- github-output
|
|
7
|
+
- multiline
|
|
8
|
+
- output
|
|
9
|
+
- heredoc
|
|
10
|
+
- truncation
|
|
11
|
+
- environment-files
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "echo\\s+['\"]?\\w+=.*\\$\\([^)]+\\).*>>\\s+\\$GITHUB_OUTPUT"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "invalid value.*GITHUB_OUTPUT.*contains newline"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "Error: Unable to process file command 'output'"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "GITHUB_OUTPUT.*multiline"
|
|
20
|
+
flags: "i"
|
|
21
|
+
error_messages:
|
|
22
|
+
- "Error: Unable to process file command 'output' successfully."
|
|
23
|
+
- "Invalid format 'key=line1\\nline2' - value contains newline, use heredoc syntax"
|
|
24
|
+
- "Warning: Unexpected input(s) 'output', valid inputs are"
|
|
25
|
+
root_cause: |
|
|
26
|
+
The `GITHUB_OUTPUT` environment file protocol (introduced November 2022 as a replacement
|
|
27
|
+
for the deprecated `::set-output` command) uses a line-based key=value format that does
|
|
28
|
+
NOT support literal newline characters in values when using simple `echo "key=value"` syntax.
|
|
29
|
+
|
|
30
|
+
When a step sets a multiline output using the naive approach:
|
|
31
|
+
```bash
|
|
32
|
+
echo "REPORT=$(cat report.txt)" >> $GITHUB_OUTPUT
|
|
33
|
+
```
|
|
34
|
+
the shell expands `$(cat report.txt)` into a multiline string, which is then written as:
|
|
35
|
+
```
|
|
36
|
+
REPORT=line1
|
|
37
|
+
line2
|
|
38
|
+
line3
|
|
39
|
+
```
|
|
40
|
+
The runner reads the first newline as end-of-value for `REPORT`, then tries to parse `line2`
|
|
41
|
+
as a new key=value entry (which fails silently or errors). The downstream step reading
|
|
42
|
+
`${{ steps.id.outputs.REPORT }}` receives only `line1` — or an empty string if the parser
|
|
43
|
+
rejects the entry entirely.
|
|
44
|
+
|
|
45
|
+
This is a **silent failure** because the workflow step itself does not fail — the `echo`
|
|
46
|
+
command exits successfully. The broken output only surfaces downstream, often as an empty
|
|
47
|
+
variable or a truncated value that's hard to trace back to the output-setting step.
|
|
48
|
+
|
|
49
|
+
The same issue affects `$GITHUB_ENV` for multiline environment variable values.
|
|
50
|
+
fix: |
|
|
51
|
+
Use the **heredoc (multi-line delimiter) syntax** documented by GitHub for multiline values.
|
|
52
|
+
The format is:
|
|
53
|
+
```
|
|
54
|
+
{name}<<{delimiter}
|
|
55
|
+
{value}
|
|
56
|
+
{delimiter}
|
|
57
|
+
```
|
|
58
|
+
where `{delimiter}` is any string that does NOT appear in the value itself. `EOF` is the
|
|
59
|
+
conventional choice, but using a random string (e.g. `$(uuidgen)`) prevents injection
|
|
60
|
+
attacks if the value is untrusted.
|
|
61
|
+
|
|
62
|
+
**For `$GITHUB_ENV` multiline values:** use the identical heredoc pattern with `$GITHUB_ENV`.
|
|
63
|
+
|
|
64
|
+
**For JavaScript/TypeScript actions:** use `core.setOutput('name', value)` from
|
|
65
|
+
`@actions/core` — it handles serialization automatically and is not subject to this limitation.
|
|
66
|
+
fix_code:
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: "Correct multiline GITHUB_OUTPUT using heredoc EOF syntax"
|
|
69
|
+
code: |
|
|
70
|
+
- name: Set multiline output
|
|
71
|
+
id: report
|
|
72
|
+
run: |
|
|
73
|
+
# WRONG: echo "REPORT=$(cat report.txt)" >> $GITHUB_OUTPUT
|
|
74
|
+
# Correct: heredoc syntax
|
|
75
|
+
echo "REPORT<<EOF" >> $GITHUB_OUTPUT
|
|
76
|
+
cat report.txt >> $GITHUB_OUTPUT
|
|
77
|
+
echo "EOF" >> $GITHUB_OUTPUT
|
|
78
|
+
|
|
79
|
+
- name: Use multiline output
|
|
80
|
+
run: |
|
|
81
|
+
echo "${{ steps.report.outputs.REPORT }}"
|
|
82
|
+
- language: yaml
|
|
83
|
+
label: "Use random delimiter to prevent injection from untrusted content"
|
|
84
|
+
code: |
|
|
85
|
+
- name: Set multiline output safely
|
|
86
|
+
id: data
|
|
87
|
+
run: |
|
|
88
|
+
DELIMITER=$(openssl rand -hex 16)
|
|
89
|
+
echo "JSON_OUTPUT<<${DELIMITER}" >> $GITHUB_OUTPUT
|
|
90
|
+
curl -s https://api.example.com/data >> $GITHUB_OUTPUT
|
|
91
|
+
echo "${DELIMITER}" >> $GITHUB_OUTPUT
|
|
92
|
+
- language: yaml
|
|
93
|
+
label: "Multiline GITHUB_ENV using heredoc (same pattern)"
|
|
94
|
+
code: |
|
|
95
|
+
- name: Set multiline environment variable
|
|
96
|
+
run: |
|
|
97
|
+
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
|
|
98
|
+
git log --oneline -10 >> $GITHUB_ENV
|
|
99
|
+
echo "EOF" >> $GITHUB_ENV
|
|
100
|
+
prevention:
|
|
101
|
+
- "Never use `echo \"KEY=$(command)\" >> $GITHUB_OUTPUT` when the command output may span multiple lines."
|
|
102
|
+
- "Always use the heredoc EOF syntax for any output value that could contain newlines: `echo \"KEY<<EOF\" >> $GITHUB_OUTPUT && <value> >> $GITHUB_OUTPUT && echo \"EOF\" >> $GITHUB_OUTPUT`."
|
|
103
|
+
- "Use a random delimiter (`$(openssl rand -hex 16)`) instead of `EOF` when the value originates from untrusted input to prevent delimiter injection."
|
|
104
|
+
- "In JavaScript/TypeScript actions, use `@actions/core`'s `core.setOutput()` which handles serialization automatically."
|
|
105
|
+
- "Test multiline outputs locally with `act` or by printing `cat $GITHUB_OUTPUT` in a subsequent step to verify the full value was captured."
|
|
106
|
+
docs:
|
|
107
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#multiline-strings"
|
|
108
|
+
label: "GitHub Docs: Workflow commands — Multiline strings"
|
|
109
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs"
|
|
110
|
+
label: "GitHub Docs: Passing information between jobs"
|
|
111
|
+
- url: "https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/"
|
|
112
|
+
label: "GitHub Changelog: Deprecating save-state and set-output commands"
|
package/package.json
CHANGED