@htekdev/actions-debugger 1.0.29 → 1.0.31
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/known-unsolved/input-unset-vs-empty-string.yml +78 -0
- package/errors/permissions-auth/tj-actions-changed-files-supply-chain-cve-2025-30066.yml +99 -0
- package/errors/runner-environment/arc-task-cancelled-pod-eviction.yml +90 -0
- package/errors/runner-environment/bash-script-path-unquoted-spaces.yml +83 -0
- package/errors/runner-environment/immutable-actions-pkg-domain-not-allowlisted.yml +97 -0
- package/errors/runner-environment/self-hosted-runner-stuck-between-jobs.yml +109 -0
- package/errors/silent-failures/github-ref-full-path-comparison-always-false.yml +97 -0
- package/errors/silent-failures/head-commit-null-on-pull-request.yml +121 -0
- package/package.json +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
id: 'known-unsolved-030'
|
|
2
|
+
title: 'core.getInput cannot distinguish unset input from explicitly-empty input'
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: limitation
|
|
5
|
+
tags:
|
|
6
|
+
- toolkit
|
|
7
|
+
- core-getInput
|
|
8
|
+
- composite-action
|
|
9
|
+
- empty-string
|
|
10
|
+
- null-input
|
|
11
|
+
- fork-secrets
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'core\.getInput\('
|
|
14
|
+
flags: 'i'
|
|
15
|
+
error_messages:
|
|
16
|
+
- 'No way to detect if input was provided vs set to empty string'
|
|
17
|
+
root_cause: |
|
|
18
|
+
The `core.getInput(name)` function in `@actions/core` always returns an empty string ('')
|
|
19
|
+
when an input is either not provided by the caller or explicitly set to the empty string.
|
|
20
|
+
There is no `core.hasInput(name)` API or other mechanism to distinguish these two cases.
|
|
21
|
+
|
|
22
|
+
This creates several practical problems for action authors:
|
|
23
|
+
- A required input set to '' passes the required: true check in getInput but is semantically absent
|
|
24
|
+
- Fork pull requests inject secrets as empty strings (secrets are unavailable); an action cannot tell
|
|
25
|
+
if a secret input was omitted vs provided-as-empty-because-fork
|
|
26
|
+
- Composite action callers cannot express "I intentionally leave this blank" vs "I don't provide this at all"
|
|
27
|
+
- YAML null or ~ values (e.g., with: my_val: ~) are coerced to '' by the runner before the action sees them
|
|
28
|
+
|
|
29
|
+
Upstream GitHub toolkit issue #940 has been open since 2022 with 22 upvotes and no fix planned.
|
|
30
|
+
fix: |
|
|
31
|
+
No direct fix exists — there is no core.hasInput() API. Workarounds depend on the use case:
|
|
32
|
+
- For sentinel detection: document a convention like 'none' or '__unset__' as the explicit absent value
|
|
33
|
+
and check getInput('x') === 'none'
|
|
34
|
+
- For fork secret detection: check github.event.pull_request.head.repo.fork == true and gate on that
|
|
35
|
+
rather than on whether the secret is empty
|
|
36
|
+
- For optional inputs: provide a well-documented default value in action.yml so callers always get a
|
|
37
|
+
predictable non-empty string when they omit the input
|
|
38
|
+
- For composite actions: use ${{ inputs.my_input != '' }} in if: conditions, documenting that
|
|
39
|
+
callers must pass a non-empty string to opt in
|
|
40
|
+
fix_code:
|
|
41
|
+
- language: yaml
|
|
42
|
+
label: 'Use sentinel value convention to detect absent input'
|
|
43
|
+
code: |
|
|
44
|
+
# action.yml — declare sentinel default
|
|
45
|
+
inputs:
|
|
46
|
+
deploy_env:
|
|
47
|
+
description: 'Target environment (leave blank to skip deployment)'
|
|
48
|
+
required: false
|
|
49
|
+
default: '__unset__'
|
|
50
|
+
|
|
51
|
+
# In composite action steps
|
|
52
|
+
steps:
|
|
53
|
+
- name: Deploy
|
|
54
|
+
if: ${{ inputs.deploy_env != '__unset__' && inputs.deploy_env != '' }}
|
|
55
|
+
run: echo "Deploying to ${{ inputs.deploy_env }}"
|
|
56
|
+
- language: yaml
|
|
57
|
+
label: 'Detect fork PR to guard secret-gated steps instead of empty-check'
|
|
58
|
+
code: |
|
|
59
|
+
steps:
|
|
60
|
+
- name: Publish (skip on fork PRs)
|
|
61
|
+
if: >-
|
|
62
|
+
${{ github.event_name != 'pull_request' ||
|
|
63
|
+
github.event.pull_request.head.repo.full_name == github.repository }}
|
|
64
|
+
run: echo "$NPM_TOKEN" | npm login --registry https://registry.npmjs.org
|
|
65
|
+
env:
|
|
66
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
67
|
+
prevention:
|
|
68
|
+
- 'Document in action.yml that empty string and absent are treated identically by core.getInput'
|
|
69
|
+
- 'Use a non-empty sentinel default value (e.g. __unset__) instead of relying on empty-check logic'
|
|
70
|
+
- 'Never gate fork-secret logic on secret emptiness — use fork-detection via event context instead'
|
|
71
|
+
- 'For required inputs that must be non-empty, add an explicit validation step that fails with a helpful message'
|
|
72
|
+
docs:
|
|
73
|
+
- url: 'https://github.com/actions/toolkit/issues/940'
|
|
74
|
+
label: 'actions/toolkit#940: Impossible to detect unset inputs from inputs set as empty string'
|
|
75
|
+
- url: 'https://github.com/actions/toolkit/tree/main/packages/core'
|
|
76
|
+
label: 'actions/toolkit core package — getInput API'
|
|
77
|
+
- url: 'https://docs.github.com/en/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#inputs'
|
|
78
|
+
label: 'GitHub Docs: Action metadata — inputs'
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
id: 'permissions-auth-030'
|
|
2
|
+
title: 'tj-actions/changed-files supply chain attack exposed CI/CD secrets in workflow logs (CVE-2025-30066)'
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- supply-chain
|
|
7
|
+
- tj-actions
|
|
8
|
+
- changed-files
|
|
9
|
+
- cve-2025-30066
|
|
10
|
+
- secrets-exposed
|
|
11
|
+
- security
|
|
12
|
+
- third-party-action
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'tj-actions/changed-files@(?:v(?:1|35|44)\b|0e58ed8671d6b60d0890c21b07f8835ace038e67)'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: '(?:memdump\.py|gist\.githubusercontent\.com/nikitastupin)'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
error_messages:
|
|
19
|
+
- 'Unexpected base64-encoded output in tj-actions/changed-files step logs'
|
|
20
|
+
- 'Unauthorized outbound request to gist.githubusercontent.com detected in workflow run'
|
|
21
|
+
root_cause: |
|
|
22
|
+
Between March 14 and March 15, 2025, attackers compromised the tj-actions/changed-files
|
|
23
|
+
GitHub Action (CVE-2025-30066, GHSA-mw4p-6x4p-x5m5) by retroactively modifying multiple
|
|
24
|
+
version tags to reference a malicious commit SHA
|
|
25
|
+
(0e58ed8671d6b60d0890c21b07f8835ace038e67).
|
|
26
|
+
|
|
27
|
+
The malicious commit injected a Python script that:
|
|
28
|
+
1. Downloaded a memory-dump utility from a public GitHub Gist
|
|
29
|
+
2. Scanned the GitHub Actions Runner Worker process memory for secrets
|
|
30
|
+
3. Base64-encoded the extracted memory content
|
|
31
|
+
4. Printed the encoded secrets directly to the workflow log
|
|
32
|
+
|
|
33
|
+
For repositories with public workflow logs, these secrets were immediately publicly
|
|
34
|
+
accessible. Over 23,000 repositories were affected.
|
|
35
|
+
|
|
36
|
+
The attack exploited a fundamental trust model weakness: when workflows pin to a version
|
|
37
|
+
tag (e.g., @v35 or @v44.5.1) rather than a specific commit SHA, an attacker who gains
|
|
38
|
+
write access to the upstream repository can silently redirect any tag to malicious code.
|
|
39
|
+
The workflow continues to run without any error — it just also exfiltrates secrets.
|
|
40
|
+
|
|
41
|
+
The compromised tags included: v1.0.0, v35.7.7-sec, v44.5.1, and others.
|
|
42
|
+
The vulnerability window was March 14–15, 2025. Tags have since been updated to safe
|
|
43
|
+
code, but any secrets exposed during that window must be treated as compromised.
|
|
44
|
+
fix: |
|
|
45
|
+
Immediate actions if your workflows ran tj-actions/changed-files between March 14-15, 2025:
|
|
46
|
+
1. Inspect workflow logs for unexpected base64 output in the changed-files step
|
|
47
|
+
2. Decode any suspicious output: echo 'BLOB' | base64 -d | base64 -d
|
|
48
|
+
3. Rotate ALL secrets (API keys, tokens, SSH keys, cloud credentials) that were present
|
|
49
|
+
in any workflow that used the affected action during the vulnerability window
|
|
50
|
+
|
|
51
|
+
For all workflows (ongoing prevention):
|
|
52
|
+
- Pin third-party actions to specific commit SHAs instead of version tags
|
|
53
|
+
- Use tools like StepSecurity Harden-Runner to detect unexpected outbound network calls
|
|
54
|
+
- Use tools like pin-github-action or GitHub's allowed-actions list to enforce SHA pinning
|
|
55
|
+
fix_code:
|
|
56
|
+
- language: yaml
|
|
57
|
+
label: 'Pin third-party actions to a specific commit SHA instead of a version tag'
|
|
58
|
+
code: |
|
|
59
|
+
steps:
|
|
60
|
+
# Vulnerable: tag can be silently repointed to malicious code
|
|
61
|
+
# - uses: tj-actions/changed-files@v44
|
|
62
|
+
# - uses: tj-actions/changed-files@v44.5.1
|
|
63
|
+
|
|
64
|
+
# Safe: commit SHA is immutable — attacker cannot redirect it
|
|
65
|
+
- name: Get changed files
|
|
66
|
+
id: changed-files
|
|
67
|
+
uses: tj-actions/changed-files@823fcebdb31bb97eca5b8e3cd20bb12abfa9b68d
|
|
68
|
+
with:
|
|
69
|
+
files: |
|
|
70
|
+
src/**
|
|
71
|
+
- language: yaml
|
|
72
|
+
label: 'Use StepSecurity Harden-Runner to detect unauthorized outbound network calls'
|
|
73
|
+
code: |
|
|
74
|
+
jobs:
|
|
75
|
+
build:
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
steps:
|
|
78
|
+
- name: Harden runner
|
|
79
|
+
uses: step-security/harden-runner@v2
|
|
80
|
+
with:
|
|
81
|
+
egress-policy: audit # or 'block' to prevent unauthorized calls
|
|
82
|
+
- name: Get changed files
|
|
83
|
+
uses: tj-actions/changed-files@823fcebdb31bb97eca5b8e3cd20bb12abfa9b68d
|
|
84
|
+
prevention:
|
|
85
|
+
- 'Always pin third-party GitHub Actions to a specific commit SHA, not a version tag'
|
|
86
|
+
- 'Audit your workflows periodically for version-tagged (not SHA-pinned) third-party actions'
|
|
87
|
+
- 'Use StepSecurity Harden-Runner or similar tooling to detect anomalous outbound network requests in CI'
|
|
88
|
+
- 'Subscribe to security advisories for third-party actions your workflows depend on'
|
|
89
|
+
- 'Rotate secrets immediately if a supply chain compromise is announced for any action you use'
|
|
90
|
+
- 'Consider using a private action mirror with controlled updates rather than pulling from public repos'
|
|
91
|
+
docs:
|
|
92
|
+
- url: 'https://github.com/tj-actions/changed-files/security/advisories/GHSA-mw4p-6x4p-x5m5'
|
|
93
|
+
label: 'GHSA-mw4p-6x4p-x5m5: tj-actions/changed-files supply chain attack advisory'
|
|
94
|
+
- url: 'https://www.cve.org/CVERecord?id=CVE-2025-30066'
|
|
95
|
+
label: 'CVE-2025-30066: NVD entry'
|
|
96
|
+
- url: 'https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-third-party-actions'
|
|
97
|
+
label: 'GitHub Docs: Security hardening — using third-party actions'
|
|
98
|
+
- url: 'https://docs.stepsecurity.io/harden-runner/'
|
|
99
|
+
label: 'StepSecurity Harden-Runner documentation'
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
id: 'runner-environment-083'
|
|
2
|
+
title: 'Actions Runner Controller pods intermittently fail with "A task was cancelled" during large matrix jobs'
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- arc
|
|
7
|
+
- kubernetes
|
|
8
|
+
- matrix
|
|
9
|
+
- task-cancelled
|
|
10
|
+
- pod-eviction
|
|
11
|
+
- self-hosted
|
|
12
|
+
- keda
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'A task was cancell?ed\.'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'The operation was canceled\.'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
error_messages:
|
|
19
|
+
- 'Error: A task was cancelled.'
|
|
20
|
+
- 'Error: The operation was canceled.'
|
|
21
|
+
root_cause: |
|
|
22
|
+
When using Actions Runner Controller (ARC) to run GitHub Actions on Kubernetes, ephemeral
|
|
23
|
+
runner pods can be evicted or preempted mid-job by the Kubernetes scheduler, producing
|
|
24
|
+
a "A task was cancelled" or "The operation was canceled" error with no application-level
|
|
25
|
+
log output before the failure.
|
|
26
|
+
|
|
27
|
+
Common causes:
|
|
28
|
+
- Kubernetes resource pressure: if a node is under memory or CPU pressure, the kubelet
|
|
29
|
+
evicts lower-priority pods. ARC runner pods have no PriorityClass by default and are
|
|
30
|
+
among the first to be evicted
|
|
31
|
+
- Node autoscaling: Cluster Autoscaler draining nodes for scale-down triggers eviction of
|
|
32
|
+
runner pods that have been running longer than the scale-down grace period
|
|
33
|
+
- KEDA queue-length scaling: KEDA scaling down the runner Deployment while jobs are
|
|
34
|
+
in-flight terminates runner pods before jobs complete
|
|
35
|
+
- OOM kills: matrix jobs that each consume significant memory can saturate node memory,
|
|
36
|
+
causing the OOM killer to terminate runner pods
|
|
37
|
+
|
|
38
|
+
The issue is especially common with large matrix builds (10+ parallel jobs) because the
|
|
39
|
+
aggregate resource demand spike can trigger autoscaler or eviction behavior. Because ARC
|
|
40
|
+
runner pods are ephemeral and have no restart policy, the job is permanently failed when
|
|
41
|
+
the pod is evicted.
|
|
42
|
+
fix: |
|
|
43
|
+
Assign a high PriorityClass to ARC runner pods so the Kubernetes scheduler avoids evicting
|
|
44
|
+
them during resource pressure. Also set adequate resource requests/limits and configure
|
|
45
|
+
terminationGracePeriodSeconds to at least the expected maximum job duration.
|
|
46
|
+
fix_code:
|
|
47
|
+
- language: yaml
|
|
48
|
+
label: 'Create a high-priority PriorityClass for ARC runner pods'
|
|
49
|
+
code: |
|
|
50
|
+
apiVersion: scheduling.k8s.io/v1
|
|
51
|
+
kind: PriorityClass
|
|
52
|
+
metadata:
|
|
53
|
+
name: github-runner-high
|
|
54
|
+
value: 1000000
|
|
55
|
+
globalDefault: false
|
|
56
|
+
description: 'High priority for GitHub Actions runner pods to prevent eviction'
|
|
57
|
+
- language: yaml
|
|
58
|
+
label: 'Reference PriorityClass and set resource limits in ARC AutoscalingRunnerSet values'
|
|
59
|
+
code: |
|
|
60
|
+
# helm values for actions-runner-controller AutoscalingRunnerSet chart
|
|
61
|
+
template:
|
|
62
|
+
spec:
|
|
63
|
+
priorityClassName: github-runner-high
|
|
64
|
+
# Allow jobs up to 1 hour to finish before pod is force-terminated
|
|
65
|
+
terminationGracePeriodSeconds: 3600
|
|
66
|
+
containers:
|
|
67
|
+
- name: runner
|
|
68
|
+
resources:
|
|
69
|
+
requests:
|
|
70
|
+
memory: '2Gi'
|
|
71
|
+
cpu: '500m'
|
|
72
|
+
limits:
|
|
73
|
+
memory: '4Gi'
|
|
74
|
+
cpu: '2000m'
|
|
75
|
+
prevention:
|
|
76
|
+
- 'Assign a PriorityClass to ARC runner pods to prevent eviction under resource pressure'
|
|
77
|
+
- 'Set terminationGracePeriodSeconds to at least the expected maximum single-job duration'
|
|
78
|
+
- 'Set explicit resource requests and limits to avoid OOM kills during large matrix builds'
|
|
79
|
+
- 'Configure KEDA scale-down stabilization windows to prevent scaling down while jobs run'
|
|
80
|
+
- 'Monitor node resource utilization and right-size cluster nodes for peak matrix concurrency'
|
|
81
|
+
- 'Enable PodDisruptionBudgets for runner workloads to reduce involuntary evictions during node drains'
|
|
82
|
+
docs:
|
|
83
|
+
- url: 'https://github.com/actions/runner/issues/3819'
|
|
84
|
+
label: 'actions/runner#3819: A lot of random "A task was cancelled" errors'
|
|
85
|
+
- url: 'https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/'
|
|
86
|
+
label: 'Kubernetes: Pod Priority and Preemption'
|
|
87
|
+
- url: 'https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/autoscaling-with-self-hosted-runners'
|
|
88
|
+
label: 'GitHub Docs: Autoscaling with self-hosted runners'
|
|
89
|
+
- url: 'https://github.com/actions/actions-runner-controller'
|
|
90
|
+
label: 'Actions Runner Controller (ARC) GitHub repository'
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
id: 'runner-environment-084'
|
|
2
|
+
title: 'Bash/sh script handler does not quote script path — fails when path contains spaces'
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- bash
|
|
7
|
+
- shell
|
|
8
|
+
- path-spaces
|
|
9
|
+
- job-hooks
|
|
10
|
+
- self-hosted
|
|
11
|
+
- macos
|
|
12
|
+
- tart-vm
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'ACTIONS_RUNNER_HOOK_JOB_(?:STARTED|COMPLETED)'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'bash.*No such file or directory'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
error_messages:
|
|
19
|
+
- 'bash: /Volumes/My Shared Files/hook.sh: No such file or directory'
|
|
20
|
+
- 'sh: /path with spaces/script.sh: not found'
|
|
21
|
+
- '/path/with: not found'
|
|
22
|
+
root_cause: |
|
|
23
|
+
The GitHub Actions runner bash/sh script handler does not quote the script path
|
|
24
|
+
placeholder when building the shell invocation. The default bash arguments in
|
|
25
|
+
ScriptHandlerHelpers.cs are:
|
|
26
|
+
--noprofile --norc -e -o pipefail {0}
|
|
27
|
+
When {0} is replaced with a path containing spaces — e.g.,
|
|
28
|
+
/Volumes/My Shared Files/hook.sh
|
|
29
|
+
bash receives the path as two separate arguments due to word splitting:
|
|
30
|
+
bash --noprofile --norc -e -o pipefail /Volumes/My Shared Files/hook.sh
|
|
31
|
+
This causes a "No such file or directory" error for the first word-split token.
|
|
32
|
+
|
|
33
|
+
By contrast, the PowerShell and cmd handlers correctly quote the path:
|
|
34
|
+
pwsh: -command "& '{0}'"
|
|
35
|
+
powershell: -command ". '{0}'"
|
|
36
|
+
cmd: /D /E:ON /V:OFF /S /C "CALL "{0}""
|
|
37
|
+
Only bash and sh are affected (runner#4404, unresolved as of June 2026).
|
|
38
|
+
|
|
39
|
+
Practical impact:
|
|
40
|
+
- Job hooks (ACTIONS_RUNNER_HOOK_JOB_STARTED, ACTIONS_RUNNER_HOOK_JOB_COMPLETED) placed in
|
|
41
|
+
shared directories with spaces — common on macOS Tart VMs mounted at /Volumes/My Shared Files/
|
|
42
|
+
- Self-hosted runner workspaces on paths containing spaces (less common but possible)
|
|
43
|
+
- Any run: step using a working-directory with spaces in the resolved path
|
|
44
|
+
fix: |
|
|
45
|
+
Ensure hook script paths and runner working directories never contain spaces.
|
|
46
|
+
On macOS Tart VMs, place hook scripts under a path without spaces (e.g., /Users/runner/hooks/).
|
|
47
|
+
|
|
48
|
+
Use a wrapper script at a space-free path that exec-delegates to the actual script if
|
|
49
|
+
it must reside under a shared mount with spaces in its path.
|
|
50
|
+
|
|
51
|
+
Monitor actions/runner#4404 for the upstream fix and upgrade when a patched runner ships.
|
|
52
|
+
fix_code:
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: 'Configure job hook at a space-free path (environment variable)'
|
|
55
|
+
code: |
|
|
56
|
+
# In the runner .env file (e.g., /home/runner/actions-runner/.env):
|
|
57
|
+
# Point hook variables to a path WITHOUT spaces
|
|
58
|
+
ACTIONS_RUNNER_HOOK_JOB_STARTED=/Users/runner/hooks/job-started.sh
|
|
59
|
+
ACTIONS_RUNNER_HOOK_JOB_COMPLETED=/Users/runner/hooks/job-completed.sh
|
|
60
|
+
#
|
|
61
|
+
# Avoid paths like:
|
|
62
|
+
# /Volumes/My Shared Files/hooks/ ← spaces cause bash word-splitting error
|
|
63
|
+
- language: yaml
|
|
64
|
+
label: 'Wrapper script at space-free path delegates to actual hook in shared mount'
|
|
65
|
+
code: |
|
|
66
|
+
#!/bin/bash
|
|
67
|
+
# /Users/runner/hooks/job-started.sh (space-free path — registered as the hook)
|
|
68
|
+
#
|
|
69
|
+
# Exec-delegates to the actual hook that lives under a shared volume with spaces.
|
|
70
|
+
# Using exec preserves exit codes and avoids a subprocess layer.
|
|
71
|
+
exec "/Volumes/My Shared Files/hooks/actual-job-started.sh" "$@"
|
|
72
|
+
prevention:
|
|
73
|
+
- 'Never place runner hooks, workspace paths, or working-directories in paths containing spaces'
|
|
74
|
+
- 'On macOS Tart VMs, configure shared mounts to use space-free mount points (e.g., /Volumes/SharedFiles)'
|
|
75
|
+
- 'Test runner hook invocations explicitly on macOS or Windows deployments with shared mounts'
|
|
76
|
+
- 'Watch actions/runner#4404 for the upstream fix; upgrade the runner version when it ships'
|
|
77
|
+
docs:
|
|
78
|
+
- url: 'https://github.com/actions/runner/issues/4404'
|
|
79
|
+
label: 'actions/runner#4404: Bash script handler does not quote script path — breaks with spaces'
|
|
80
|
+
- url: 'https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/running-scripts-before-or-after-a-job'
|
|
81
|
+
label: 'GitHub Docs: Running scripts before or after a job (hooks)'
|
|
82
|
+
- url: 'https://github.com/actions/runner/blob/main/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs'
|
|
83
|
+
label: 'Runner source: ScriptHandlerHelpers.cs (unquoted bash path template)'
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
id: 'runner-environment-085'
|
|
2
|
+
title: 'Self-hosted runner fails to download immutable actions — `pkg.actions.githubusercontent.com` not in network allowlist'
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- self-hosted-runner
|
|
7
|
+
- immutable-actions
|
|
8
|
+
- network
|
|
9
|
+
- allowlist
|
|
10
|
+
- firewall
|
|
11
|
+
- pkg.actions.githubusercontent.com
|
|
12
|
+
- action-download
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'Failed to download action.*pkg\.actions\.githubusercontent\.com'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'Unable to locate executable.*pkg\.actions\.githubusercontent\.com'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
- regex: 'Error: An error occurred while loading the action.*pkg\.actions'
|
|
19
|
+
flags: 'i'
|
|
20
|
+
- regex: 'connect(?:ion)? (?:refused|timed? ?out).*pkg\.actions\.githubusercontent\.com'
|
|
21
|
+
flags: 'i'
|
|
22
|
+
error_messages:
|
|
23
|
+
- "Error: Failed to download action 'actions/checkout@v4'. Actions requiring immutable action downloads from pkg.actions.githubusercontent.com are blocked."
|
|
24
|
+
- "Error: An error occurred while loading the action. Connection refused: pkg.actions.githubusercontent.com"
|
|
25
|
+
root_cause: |
|
|
26
|
+
Starting February 2025, GitHub began migrating GitHub-hosted runners to use
|
|
27
|
+
"Immutable Actions" — a system where action code is downloaded from a new dedicated
|
|
28
|
+
CDN host (`pkg.actions.githubusercontent.com`) rather than being cloned directly from
|
|
29
|
+
the action's GitHub repository.
|
|
30
|
+
|
|
31
|
+
Self-hosted runners behind corporate firewalls or with strict network egress allowlists
|
|
32
|
+
fail to download actions when this new domain is not permitted, causing the job to fail
|
|
33
|
+
immediately during the action download phase before any user step runs.
|
|
34
|
+
|
|
35
|
+
The failure is distinct from a runtime error: the runner itself cannot fetch the action
|
|
36
|
+
code, so no build output is produced. Logs show a network-level failure during the
|
|
37
|
+
"Getting action download info" or "Download action repository" phase.
|
|
38
|
+
|
|
39
|
+
Organizations that previously configured their allowlist with `*.github.com`,
|
|
40
|
+
`codeload.github.com`, or `github.com` for action downloads must now also add
|
|
41
|
+
`pkg.actions.githubusercontent.com`.
|
|
42
|
+
|
|
43
|
+
Note: If your allowlist already includes `*.actions.githubusercontent.com`, no change
|
|
44
|
+
is required — the wildcard matches the new subdomain.
|
|
45
|
+
fix: |
|
|
46
|
+
Add `pkg.actions.githubusercontent.com` to your self-hosted runner's outbound network
|
|
47
|
+
allowlist. Runners that already allow `*.actions.githubusercontent.com` via wildcard
|
|
48
|
+
do not require any change.
|
|
49
|
+
|
|
50
|
+
For Azure private networking, also update your NSG template with the additional IP
|
|
51
|
+
ranges published by GitHub in the February 2025 changelog.
|
|
52
|
+
|
|
53
|
+
For GitHub Enterprise Server customers using GitHub Connect: update your self-hosted
|
|
54
|
+
runner allowlists to include the new domain before enabling immutable actions.
|
|
55
|
+
fix_code:
|
|
56
|
+
- language: yaml
|
|
57
|
+
label: 'Required network allowlist domains for self-hosted runners (as of Feb 2025)'
|
|
58
|
+
code: |
|
|
59
|
+
# Ensure these domains/IPs are allowed for outbound HTTPS (443) from your runners:
|
|
60
|
+
#
|
|
61
|
+
# Existing required domains (unchanged):
|
|
62
|
+
# github.com
|
|
63
|
+
# api.github.com
|
|
64
|
+
# *.actions.githubusercontent.com <- wildcard covers new subdomain too
|
|
65
|
+
# codeload.github.com
|
|
66
|
+
# results-receiver.actions.githubusercontent.com
|
|
67
|
+
#
|
|
68
|
+
# New domain required for immutable actions (Feb 2025):
|
|
69
|
+
# pkg.actions.githubusercontent.com
|
|
70
|
+
#
|
|
71
|
+
# If you use domain-specific (not wildcard) allowlists, add:
|
|
72
|
+
# pkg.actions.githubusercontent.com
|
|
73
|
+
- language: yaml
|
|
74
|
+
label: 'Verify immutable actions network access with a diagnostic step'
|
|
75
|
+
code: |
|
|
76
|
+
jobs:
|
|
77
|
+
network-check:
|
|
78
|
+
runs-on: [self-hosted]
|
|
79
|
+
steps:
|
|
80
|
+
- name: Check immutable actions domain reachability
|
|
81
|
+
run: |
|
|
82
|
+
curl -sSf --max-time 10 \
|
|
83
|
+
https://pkg.actions.githubusercontent.com \
|
|
84
|
+
-o /dev/null -w "HTTP %{http_code}\n" \
|
|
85
|
+
|| echo "BLOCKED: pkg.actions.githubusercontent.com unreachable"
|
|
86
|
+
prevention:
|
|
87
|
+
- 'Use wildcard domain entries (`*.actions.githubusercontent.com`) in your network allowlist rather than enumerating specific subdomains'
|
|
88
|
+
- 'Subscribe to GitHub Changelog (github.blog/changelog) to catch new domain requirements before they cause outages'
|
|
89
|
+
- 'Periodically run network reachability checks against all required GitHub Actions domains from your runner environment'
|
|
90
|
+
- 'Document your runner network requirements in runbooks and tie allowlist updates to a change management process'
|
|
91
|
+
docs:
|
|
92
|
+
- url: 'https://github.blog/changelog/2025-02-12-notice-of-upcoming-deprecations-and-breaking-changes-for-github-actions/'
|
|
93
|
+
label: 'GitHub Changelog 2025-02-12: Immutable Actions migration and network allowlist updates'
|
|
94
|
+
- 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'
|
|
95
|
+
label: 'GitHub Docs: Self-hosted runner network communication requirements'
|
|
96
|
+
- url: 'https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#hardening-for-self-hosted-runners'
|
|
97
|
+
label: 'GitHub Docs: Security hardening for self-hosted runners'
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
id: 'runner-environment-082'
|
|
2
|
+
title: 'Self-hosted runner gets stuck "Waiting for a runner to pick up this job" between jobs in the same workflow'
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- self-hosted
|
|
7
|
+
- runner
|
|
8
|
+
- multi-job
|
|
9
|
+
- queued
|
|
10
|
+
- stuck
|
|
11
|
+
- windows
|
|
12
|
+
- auto-update
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'Waiting for a runner to pick up this job'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
error_messages:
|
|
17
|
+
- 'Waiting for a runner to pick up this job...'
|
|
18
|
+
root_cause: |
|
|
19
|
+
After completing the first job in a multi-job workflow, a self-hosted runner sometimes
|
|
20
|
+
fails to pick up subsequent jobs in the same workflow run. The subsequent jobs remain
|
|
21
|
+
in queued status indefinitely, with no timeout and no automatic retry.
|
|
22
|
+
|
|
23
|
+
Common root causes:
|
|
24
|
+
- Runner auto-update race condition: when the runner auto-updates between jobs, the post-job
|
|
25
|
+
cleanup of the first job can leave the runner in a state where it reports idle to the
|
|
26
|
+
broker but cannot accept new job messages
|
|
27
|
+
- Windows service restart latency: on Windows hosts, if the runner was auto-updated or
|
|
28
|
+
the service restarted between jobs, the new process may not have fully re-registered
|
|
29
|
+
with the GitHub Actions broker before the second job is dispatched
|
|
30
|
+
- JIT token expiry: in ephemeral/JIT runner setups, the registration token can expire
|
|
31
|
+
between jobs if the first job runs for a long time, and the runner cannot re-register
|
|
32
|
+
- Broker disconnect: a transient network interruption between jobs severs the long-poll
|
|
33
|
+
connection; the runner reconnects but the already-dispatched job message is missed
|
|
34
|
+
|
|
35
|
+
Manually cancelling and re-running the workflow, or restarting the runner service,
|
|
36
|
+
resolves the issue immediately, confirming the runner is functional but lost broker contact.
|
|
37
|
+
fix: |
|
|
38
|
+
Immediate workaround: cancel the stuck workflow run and re-trigger it, or restart the
|
|
39
|
+
runner service:
|
|
40
|
+
Windows: Restart-Service "actions.runner.*"
|
|
41
|
+
Linux: sudo systemctl restart actions.runner.*.<name>.service
|
|
42
|
+
|
|
43
|
+
Long-term fixes:
|
|
44
|
+
- Disable runner auto-update during active workflows by setting RUNNER_ALLOW_RUNASROOT
|
|
45
|
+
environment variable and pinning a specific runner version
|
|
46
|
+
- Use ephemeral runners (--ephemeral flag) so each job dispatches a fresh runner that
|
|
47
|
+
registers anew with the broker, eliminating the between-job reconnect window
|
|
48
|
+
- Split long workflows into separate workflow files triggered via workflow_run or
|
|
49
|
+
repository_dispatch so each workflow gets an independent runner session
|
|
50
|
+
- On Windows: ensure the runner service account has the "Log on as a service" right
|
|
51
|
+
and that antivirus is not blocking runner binary updates
|
|
52
|
+
fix_code:
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: 'Use ephemeral runners to avoid stuck-between-jobs on self-hosted'
|
|
55
|
+
code: |
|
|
56
|
+
# When configuring the runner, use the --ephemeral flag:
|
|
57
|
+
# ./config.sh --url https://github.com/OWNER/REPO --token TOKEN --ephemeral
|
|
58
|
+
#
|
|
59
|
+
# For ARC (Actions Runner Controller), set runnerScaleSetSettings:
|
|
60
|
+
# spec:
|
|
61
|
+
# template:
|
|
62
|
+
# metadata:
|
|
63
|
+
# labels:
|
|
64
|
+
# ephemeral: 'true'
|
|
65
|
+
#
|
|
66
|
+
# Each job gets a freshly-registered runner; no between-job broker reconnect issues.
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: 'Split multi-job workflow into two workflows triggered by workflow_run'
|
|
69
|
+
code: |
|
|
70
|
+
# phase1.yml
|
|
71
|
+
on:
|
|
72
|
+
push:
|
|
73
|
+
jobs:
|
|
74
|
+
build:
|
|
75
|
+
runs-on: [self-hosted, linux]
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/checkout@v4
|
|
78
|
+
- run: make build
|
|
79
|
+
- uses: actions/upload-artifact@v4
|
|
80
|
+
with:
|
|
81
|
+
name: build-output
|
|
82
|
+
path: dist/
|
|
83
|
+
|
|
84
|
+
# phase2.yml (fresh runner registration — no stuck-between-jobs risk)
|
|
85
|
+
on:
|
|
86
|
+
workflow_run:
|
|
87
|
+
workflows: [phase1.yml]
|
|
88
|
+
types: [completed]
|
|
89
|
+
jobs:
|
|
90
|
+
test:
|
|
91
|
+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
92
|
+
runs-on: [self-hosted, linux]
|
|
93
|
+
steps:
|
|
94
|
+
- uses: actions/download-artifact@v4
|
|
95
|
+
with:
|
|
96
|
+
name: build-output
|
|
97
|
+
- run: make test
|
|
98
|
+
prevention:
|
|
99
|
+
- 'Use ephemeral runners (--ephemeral) to ensure each job gets a fresh broker registration'
|
|
100
|
+
- 'Configure the runner service with Restart=on-failure to auto-recover from crashes between jobs'
|
|
101
|
+
- 'Pin runner versions and suppress auto-updates in production to prevent mid-workflow upgrades'
|
|
102
|
+
- 'Monitor for stuck runs via the GitHub Actions API and alert or auto-cancel them'
|
|
103
|
+
docs:
|
|
104
|
+
- url: 'https://github.com/actions/runner/issues/3609'
|
|
105
|
+
label: 'actions/runner#3609: Self-hosted runner stuck on "Waiting for a runner to pick up this job"'
|
|
106
|
+
- url: 'https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners'
|
|
107
|
+
label: 'GitHub Docs: About self-hosted runners'
|
|
108
|
+
- url: 'https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/autoscaling-with-self-hosted-runners'
|
|
109
|
+
label: 'GitHub Docs: Autoscaling with self-hosted runners'
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
id: 'silent-failures-036'
|
|
2
|
+
title: '`if: github.ref == ''main''` silently never matches — ref contains full `refs/heads/main` path'
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- github-ref
|
|
7
|
+
- ref-name
|
|
8
|
+
- branch-comparison
|
|
9
|
+
- if-condition
|
|
10
|
+
- refs/heads
|
|
11
|
+
- silent-skip
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'github\.ref\s*[!=]=\s*[''"](?:main|master|develop|release|staging)[''"]'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'github\.ref\s*[!=]=\s*[''"][a-z][a-z0-9_/-]*[^/][''"](?!\s*#.*refs/heads)'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Step was skipped because the condition was false: github.ref == 'main'"
|
|
19
|
+
- "Job skipped: if: github.ref == 'master'"
|
|
20
|
+
root_cause: |
|
|
21
|
+
`github.ref` always returns the full Git reference path, never just the branch name:
|
|
22
|
+
- Push to `main` branch: `refs/heads/main`
|
|
23
|
+
- Pull request: `refs/pull/123/merge`
|
|
24
|
+
- Tag push: `refs/tags/v1.0.0`
|
|
25
|
+
|
|
26
|
+
When a developer writes `if: github.ref == 'main'`, the condition always evaluates
|
|
27
|
+
to false because `'main'` != `'refs/heads/main'`. The step or job is silently skipped
|
|
28
|
+
with no error — only a "skipped" indicator in the UI, which is easy to overlook.
|
|
29
|
+
|
|
30
|
+
This is the most-viewed GitHub Actions question on Stack Overflow (SO #58033366,
|
|
31
|
+
478,000+ views), confirming how frequently developers encounter this mismatch.
|
|
32
|
+
|
|
33
|
+
Common variants:
|
|
34
|
+
- `if: github.ref == 'main'` → always false
|
|
35
|
+
- `if: github.ref == 'master'` → always false
|
|
36
|
+
- `if: github.ref != 'main'` → always true (everything runs)
|
|
37
|
+
- `if: github.ref == 'refs/heads/main' && github.ref == 'refs/tags/v*'` → impossible
|
|
38
|
+
(a ref cannot be both)
|
|
39
|
+
- On tag push: `if: github.ref == 'refs/heads/main'` → always false (it is `refs/tags/…`)
|
|
40
|
+
fix: |
|
|
41
|
+
Option 1 — Use `github.ref_name` (recommended, available since runner 2.276.0 / Jan 2022):
|
|
42
|
+
`github.ref_name` returns just the branch or tag name without the `refs/heads/` or
|
|
43
|
+
`refs/tags/` prefix, making comparisons intuitive.
|
|
44
|
+
|
|
45
|
+
Option 2 — Compare against the full ref path:
|
|
46
|
+
Use `github.ref == 'refs/heads/main'` instead of `github.ref == 'main'`.
|
|
47
|
+
|
|
48
|
+
Option 3 — Match on event type:
|
|
49
|
+
Use `github.event_name == 'push'` to restrict steps to push (not PR) events, which is
|
|
50
|
+
often the underlying intent.
|
|
51
|
+
fix_code:
|
|
52
|
+
- language: yaml
|
|
53
|
+
label: 'Use github.ref_name for simple branch/tag name comparison'
|
|
54
|
+
code: |
|
|
55
|
+
jobs:
|
|
56
|
+
deploy:
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
steps:
|
|
59
|
+
- name: Deploy to production
|
|
60
|
+
# github.ref_name returns 'main', 'v1.0.0', etc. (no refs/ prefix)
|
|
61
|
+
if: github.ref_name == 'main'
|
|
62
|
+
run: echo "Deploying"
|
|
63
|
+
- language: yaml
|
|
64
|
+
label: 'Use full ref path if you need github.ref specifically'
|
|
65
|
+
code: |
|
|
66
|
+
jobs:
|
|
67
|
+
deploy:
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
steps:
|
|
70
|
+
- name: Only on main branch push (not tags or PRs)
|
|
71
|
+
if: github.ref == 'refs/heads/main'
|
|
72
|
+
run: echo "Deploying"
|
|
73
|
+
- name: Only on version tags
|
|
74
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
75
|
+
run: echo "Releasing"
|
|
76
|
+
- language: yaml
|
|
77
|
+
label: 'Use event_name to distinguish push from pull_request'
|
|
78
|
+
code: |
|
|
79
|
+
jobs:
|
|
80
|
+
deploy:
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
steps:
|
|
83
|
+
- name: Only on direct pushes, not PRs
|
|
84
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
85
|
+
run: echo "Deploying"
|
|
86
|
+
prevention:
|
|
87
|
+
- 'Never compare `github.ref` to a bare branch name — it always includes the `refs/heads/` prefix'
|
|
88
|
+
- 'Prefer `github.ref_name` for branch/tag name comparisons when using runner 2.276.0+ (Jan 2022)'
|
|
89
|
+
- 'Verify your `if:` logic by inspecting the actual `github.ref` value in a debug step: `run: echo "${{ github.ref }}"`'
|
|
90
|
+
- 'Use the GitHub Actions context documentation to confirm which format each context variable uses'
|
|
91
|
+
docs:
|
|
92
|
+
- url: 'https://docs.github.com/en/actions/learn-github-actions/contexts#github-context'
|
|
93
|
+
label: 'GitHub Docs: github context — github.ref and github.ref_name'
|
|
94
|
+
- url: 'https://stackoverflow.com/questions/58033366/how-to-get-the-current-branch-within-github-actions'
|
|
95
|
+
label: 'Stack Overflow #58033366 (428 votes, 478K views): How to get the current branch within GitHub Actions'
|
|
96
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-conditions-to-control-job-execution'
|
|
97
|
+
label: 'GitHub Docs: Using conditions to control job execution'
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
id: 'silent-failures-037'
|
|
2
|
+
title: '`github.event.head_commit` is null on `pull_request` and `workflow_dispatch` events — commit message access silently fails'
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- head-commit
|
|
7
|
+
- commit-message
|
|
8
|
+
- pull-request
|
|
9
|
+
- workflow-dispatch
|
|
10
|
+
- event-payload
|
|
11
|
+
- null-context
|
|
12
|
+
- silent-skip
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'github\.event\.head_commit\.(?:message|author|id|timestamp)'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'github\.event\.head_commit\s+is\s+null'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
error_messages:
|
|
19
|
+
- "Error: Unhandled error: TypeError: Cannot read properties of null (reading 'message')"
|
|
20
|
+
- "Expression evaluation failed: github.event.head_commit is null"
|
|
21
|
+
- "Step was skipped because the condition was false: contains(github.event.head_commit.message, '[skip ci]')"
|
|
22
|
+
root_cause: |
|
|
23
|
+
`github.event.head_commit` is only populated on `push` events. It is `null` (not an
|
|
24
|
+
empty object) on all other event types, including:
|
|
25
|
+
- `pull_request` and `pull_request_target`
|
|
26
|
+
- `workflow_dispatch`
|
|
27
|
+
- `workflow_call`
|
|
28
|
+
- `schedule`
|
|
29
|
+
- `release`
|
|
30
|
+
- `repository_dispatch`
|
|
31
|
+
|
|
32
|
+
This causes several silent failure patterns:
|
|
33
|
+
1. **Silent [skip ci] bypass**: A common pattern is
|
|
34
|
+
`if: "!contains(github.event.head_commit.message, '[skip ci]')"`.
|
|
35
|
+
On a `pull_request` event, `github.event.head_commit` is null, so the expression
|
|
36
|
+
evaluates as `!contains(null, '[skip ci]')` → `!false` → `true`. Every PR run
|
|
37
|
+
executes unconditionally, ignoring any [skip ci] intent.
|
|
38
|
+
2. **Runtime crash in scripts**: GitHub Script or shell steps that access
|
|
39
|
+
`${{ github.event.head_commit.message }}` on PR events receive an empty string
|
|
40
|
+
silently, or throw a TypeError if accessed via JavaScript.
|
|
41
|
+
3. **Commit metadata loss in multi-event workflows**: Workflows triggered by both
|
|
42
|
+
`push` and `pull_request` may use `head_commit` data in notifications or logs;
|
|
43
|
+
the PR trigger silently produces empty/null values for all commit fields.
|
|
44
|
+
|
|
45
|
+
This is documented in Stack Overflow question #63441440 (Score: 95, 100,000+ views).
|
|
46
|
+
fix: |
|
|
47
|
+
Use a multi-event compatible approach to access commit messages:
|
|
48
|
+
- For `pull_request`: use `github.event.pull_request.head.sha` and fetch the commit
|
|
49
|
+
via the API, or use `github-script` with `octokit.rest.repos.getCommit()`
|
|
50
|
+
- For `push`: `github.event.head_commit.message` works directly
|
|
51
|
+
- For a robust cross-event solution: use `actions/checkout` with fetch-depth and
|
|
52
|
+
run `git log -1 --format=%s` in a shell step — this works regardless of event type
|
|
53
|
+
- For [skip ci] detection: use the `github.event.commits` array (available on `push`)
|
|
54
|
+
or check PR body/title instead of commit message for multi-event workflows
|
|
55
|
+
fix_code:
|
|
56
|
+
- language: yaml
|
|
57
|
+
label: 'Guard head_commit access with event_name check'
|
|
58
|
+
code: |
|
|
59
|
+
steps:
|
|
60
|
+
- name: Get commit message (push events only)
|
|
61
|
+
if: github.event_name == 'push'
|
|
62
|
+
run: echo "Commit: ${{ github.event.head_commit.message }}"
|
|
63
|
+
|
|
64
|
+
- name: Get HEAD commit message (all events via shell)
|
|
65
|
+
run: |
|
|
66
|
+
MSG=$(git log -1 --format='%s')
|
|
67
|
+
echo "Commit: $MSG"
|
|
68
|
+
- language: yaml
|
|
69
|
+
label: 'Cross-event [skip ci] detection using shell git log'
|
|
70
|
+
code: |
|
|
71
|
+
jobs:
|
|
72
|
+
check-skip:
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
outputs:
|
|
75
|
+
skip: ${{ steps.skip-check.outputs.skip }}
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/checkout@v4
|
|
78
|
+
with:
|
|
79
|
+
fetch-depth: 2
|
|
80
|
+
- id: skip-check
|
|
81
|
+
run: |
|
|
82
|
+
MSG=$(git log -1 --format='%s')
|
|
83
|
+
if echo "$MSG" | grep -q '\[skip ci\]'; then
|
|
84
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
85
|
+
else
|
|
86
|
+
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
build:
|
|
90
|
+
needs: check-skip
|
|
91
|
+
if: needs.check-skip.outputs.skip != 'true'
|
|
92
|
+
runs-on: ubuntu-latest
|
|
93
|
+
steps:
|
|
94
|
+
- run: echo "Building"
|
|
95
|
+
- language: yaml
|
|
96
|
+
label: 'Fetch commit message via API for pull_request events'
|
|
97
|
+
code: |
|
|
98
|
+
steps:
|
|
99
|
+
- name: Get PR head commit message
|
|
100
|
+
if: github.event_name == 'pull_request'
|
|
101
|
+
uses: actions/github-script@v7
|
|
102
|
+
with:
|
|
103
|
+
script: |
|
|
104
|
+
const { data } = await github.rest.repos.getCommit({
|
|
105
|
+
owner: context.repo.owner,
|
|
106
|
+
repo: context.repo.repo,
|
|
107
|
+
ref: context.payload.pull_request.head.sha
|
|
108
|
+
});
|
|
109
|
+
console.log('Commit message:', data.commit.message);
|
|
110
|
+
prevention:
|
|
111
|
+
- 'Always guard `github.event.head_commit` access with `if: github.event_name == ''push''`'
|
|
112
|
+
- 'For multi-event workflows needing commit messages, use `git log -1 --format=%s` after checkout rather than the event context'
|
|
113
|
+
- 'Do not use `github.event.head_commit.message` for [skip ci] detection — it silently fails on PRs and dispatches'
|
|
114
|
+
- 'Add `run: echo "${{ toJSON(github.event) }}"` to debug which event fields are available for each trigger'
|
|
115
|
+
docs:
|
|
116
|
+
- url: 'https://docs.github.com/en/webhooks/webhook-events-and-payloads#push'
|
|
117
|
+
label: 'GitHub Docs: push event payload — head_commit field'
|
|
118
|
+
- url: 'https://docs.github.com/en/webhooks/webhook-events-and-payloads#pull_request'
|
|
119
|
+
label: 'GitHub Docs: pull_request event payload (no head_commit field)'
|
|
120
|
+
- url: 'https://stackoverflow.com/questions/63441440/get-github-commit-message-in-pull-request-actions'
|
|
121
|
+
label: 'Stack Overflow #63441440 (95 votes, 100K views): Get GitHub commit message in pull request Actions'
|
package/package.json
CHANGED