@htekdev/actions-debugger 1.0.30 → 1.0.32
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/download-artifact-v5-single-artifact-no-subdirectory.yml +106 -0
- package/errors/permissions-auth/ghs-token-new-jwt-format-breaks-length-pattern-validation.yml +95 -0
- package/errors/permissions-auth/tj-actions-changed-files-supply-chain-cve-2025-30066.yml +99 -0
- package/errors/runner-environment/immutable-actions-pkg-domain-not-allowlisted.yml +97 -0
- package/errors/silent-failures/download-artifact-merge-multiple-parallel-file-clobber.yml +119 -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/errors/triggers/copilot-coding-agent-workflows-require-approval.yml +104 -0
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
id: 'caching-artifacts-029'
|
|
2
|
+
title: '`download-artifact` v5+ omits artifact subdirectory when only one artifact exists — dynamic workflows break intermittently'
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- download-artifact
|
|
7
|
+
- v5-breaking-change
|
|
8
|
+
- path-behavior
|
|
9
|
+
- single-artifact
|
|
10
|
+
- subdirectory
|
|
11
|
+
- dynamic-matrix
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'uses:\s*actions/download-artifact@v[5-9]'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'An extra directory with the artifact name will be created for each download'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- 'No input name, artifact-ids or pattern filtered specified, downloading all artifacts'
|
|
19
|
+
- 'An extra directory with the artifact name will be created for each download'
|
|
20
|
+
root_cause: |
|
|
21
|
+
In actions/download-artifact v5 (released August 2025) and later (v6, v7), when
|
|
22
|
+
downloading ALL artifacts without specifying name: or id:, and only ONE artifact exists
|
|
23
|
+
in the workflow run, the action does NOT create a subdirectory for that artifact.
|
|
24
|
+
Files are extracted directly into the destination path (default ./):
|
|
25
|
+
|
|
26
|
+
Expected (v4 and v5+ with >=2 artifacts): ./{artifact-name}/file.ext
|
|
27
|
+
Actual (v5+ with exactly 1 artifact): ./file.ext
|
|
28
|
+
|
|
29
|
+
When two or more artifacts exist, subdirectories are created correctly. This
|
|
30
|
+
inconsistency breaks workflows where artifact count varies dynamically — e.g., matrix
|
|
31
|
+
strategies that produce 0, 1, or N artifacts based on test conditions.
|
|
32
|
+
|
|
33
|
+
The action logs "An extra directory with the artifact name will be created for each
|
|
34
|
+
download" even when it will NOT do this — making the bug invisible in logs.
|
|
35
|
+
|
|
36
|
+
Bug tracked in actions/download-artifact#455 (December 2025, still open).
|
|
37
|
+
Affected versions: v5.0.0+, v6.0.0+, v7.0.0+
|
|
38
|
+
v4 behavior (correct): always creates a named subdirectory, regardless of artifact count.
|
|
39
|
+
|
|
40
|
+
This is a regression from a path behavior fix introduced in v5 for single-artifact
|
|
41
|
+
downloads by ID (PR #416), which had an unintended side effect on the "all artifacts"
|
|
42
|
+
code path when n=1.
|
|
43
|
+
fix: |
|
|
44
|
+
Option 1 (simplest): Pin to actions/download-artifact@v4, which always creates a
|
|
45
|
+
subdirectory regardless of artifact count. v4 remains maintained for GHES compatibility.
|
|
46
|
+
|
|
47
|
+
Option 2: Always specify name: for each download to avoid the "all artifacts" code path.
|
|
48
|
+
|
|
49
|
+
Option 3: In downstream steps, detect the n=1 case at runtime and normalize the
|
|
50
|
+
directory structure before relying on subdirectory paths.
|
|
51
|
+
fix_code:
|
|
52
|
+
- language: yaml
|
|
53
|
+
label: 'Pin to download-artifact@v4 for consistent subdirectory behavior'
|
|
54
|
+
code: |
|
|
55
|
+
jobs:
|
|
56
|
+
deploy:
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
steps:
|
|
59
|
+
- name: Download all artifacts
|
|
60
|
+
uses: actions/download-artifact@v4 # v4: always creates subdirs
|
|
61
|
+
with:
|
|
62
|
+
path: artifacts/
|
|
63
|
+
# Result: artifacts/{name}/... for every artifact, even when n=1
|
|
64
|
+
|
|
65
|
+
- name: Deploy each artifact directory
|
|
66
|
+
run: |
|
|
67
|
+
for dir in artifacts/*/; do
|
|
68
|
+
name=$(basename "$dir")
|
|
69
|
+
echo "Deploying $name from $dir"
|
|
70
|
+
done
|
|
71
|
+
- language: yaml
|
|
72
|
+
label: 'Workaround for v5+: download by name into named subdirectories explicitly'
|
|
73
|
+
code: |
|
|
74
|
+
jobs:
|
|
75
|
+
deploy:
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
steps:
|
|
78
|
+
- name: List artifact names for this run
|
|
79
|
+
id: list
|
|
80
|
+
uses: actions/github-script@v7
|
|
81
|
+
with:
|
|
82
|
+
script: |
|
|
83
|
+
const { data } = await github.rest.actions.listWorkflowRunArtifacts({
|
|
84
|
+
owner: context.repo.owner,
|
|
85
|
+
repo: context.repo.repo,
|
|
86
|
+
run_id: context.runId
|
|
87
|
+
});
|
|
88
|
+
return data.artifacts.map(a => a.name);
|
|
89
|
+
|
|
90
|
+
- name: Download artifact into named subdirectory
|
|
91
|
+
uses: actions/download-artifact@v5
|
|
92
|
+
with:
|
|
93
|
+
name: my-artifact
|
|
94
|
+
path: artifacts/my-artifact/ # Explicit subdir — consistent always
|
|
95
|
+
prevention:
|
|
96
|
+
- 'Pin to download-artifact@v4 if your deployment logic assumes each artifact lands in its own named subdirectory'
|
|
97
|
+
- 'Test your workflow with exactly one artifact in CI — the n=1 edge case is easy to miss in standard multi-run testing'
|
|
98
|
+
- 'Avoid relying on implicit subdirectory creation; always specify explicit path: values that include the artifact name'
|
|
99
|
+
- 'Track actions/download-artifact#455 for a fix in v5+ that restores consistent subdirectory behavior'
|
|
100
|
+
docs:
|
|
101
|
+
- url: 'https://github.com/actions/download-artifact/issues/455'
|
|
102
|
+
label: 'actions/download-artifact#455: Single artifact omits subdirectory in v5+ (open, December 2025)'
|
|
103
|
+
- url: 'https://github.com/actions/download-artifact/releases/tag/v5.0.0'
|
|
104
|
+
label: 'download-artifact v5.0.0 release notes: path behavior breaking change'
|
|
105
|
+
- url: 'https://github.com/actions/download-artifact/blob/main/docs/MIGRATION.md'
|
|
106
|
+
label: 'download-artifact migration guide: v3/v4 to v5+'
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
id: 'permissions-auth-031'
|
|
2
|
+
title: 'GitHub App installation token new stateless JWT format (~520 chars) breaks hardcoded length and regex validation'
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- github-app-token
|
|
7
|
+
- ghs-token
|
|
8
|
+
- jwt-format
|
|
9
|
+
- token-validation
|
|
10
|
+
- installation-token
|
|
11
|
+
- breaking-change-2026
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'ghs_[A-Za-z0-9]{36}\b'
|
|
14
|
+
flags: ''
|
|
15
|
+
- regex: 'Data too long for column|token.*length.*40|varchar\s*\(\s*40\s*\)'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Data too long for column 'access_token' at row 1"
|
|
19
|
+
- 'Token validation failed: token length 521 does not match expected 40'
|
|
20
|
+
- 'Invalid token format: expected 40 characters, got 523'
|
|
21
|
+
- 'ghs_[A-Za-z0-9]{36} pattern did not match token starting with ghs_'
|
|
22
|
+
root_cause: |
|
|
23
|
+
Starting April 27, 2026, GitHub began a staged rollout that changes the format of all
|
|
24
|
+
GitHub App installation tokens (ghs_ prefixed tokens), including the GITHUB_TOKEN
|
|
25
|
+
issued to Actions workflows.
|
|
26
|
+
|
|
27
|
+
Old format: ghs_<36 alphanumeric chars> — exactly 40 characters, opaque string
|
|
28
|
+
New format: ghs_<AppID>_<JWT> — approximately 520 characters (variable length),
|
|
29
|
+
JWT payload is signed by a GitHub-internal issuer
|
|
30
|
+
|
|
31
|
+
Any code that validates or stores tokens with the old format assumption WILL break:
|
|
32
|
+
- Regex: ghs_[A-Za-z0-9]{36} — the {36} quantifier rejects the new variable-length JWT
|
|
33
|
+
- Length checks: len(token) == 40, token.length === 40, strlen($token) == 40
|
|
34
|
+
- Database columns: VARCHAR(40) or CHAR(40) — the new ~520-char token truncates or errors
|
|
35
|
+
- Secret scanning rules with exact-length matchers for ghs_ tokens miss leaked tokens
|
|
36
|
+
|
|
37
|
+
The rollout timeline:
|
|
38
|
+
- April 27 – mid-May 2026: GITHUB_TOKEN and first-party tokens (Dependabot, Slack, Teams)
|
|
39
|
+
- Mid-May to late-June 2026: staged rollout to all GitHub App installation tokens
|
|
40
|
+
|
|
41
|
+
Despite the new internal JWT structure, the token prefix ghs_ is unchanged. Clients
|
|
42
|
+
must treat the entire token as an opaque string and must NOT parse or validate the JWT.
|
|
43
|
+
fix: |
|
|
44
|
+
Treat all ghs_ tokens as variable-length opaque strings. Remove any assumptions about
|
|
45
|
+
exact length or character structure after the ghs_ prefix:
|
|
46
|
+
|
|
47
|
+
1. Update regex patterns: replace ghs_[A-Za-z0-9]{36} with ghs_[A-Za-z0-9._-]+
|
|
48
|
+
2. Increase database columns: VARCHAR(40) or CHAR(40) → TEXT or VARCHAR(1024)
|
|
49
|
+
3. Remove hardcoded length checks: len(token) == 40 → len(token) > 3
|
|
50
|
+
4. Update secret scanning rules to match variable-length ghs_ tokens
|
|
51
|
+
|
|
52
|
+
To test your tooling against the new format BEFORE the rollout reaches your integration,
|
|
53
|
+
use the X-GitHub-Stateless-S2S-Token: enabled header on access token API requests.
|
|
54
|
+
fix_code:
|
|
55
|
+
- language: yaml
|
|
56
|
+
label: 'Verify GITHUB_TOKEN length in a workflow — expect ~520 chars with new format'
|
|
57
|
+
code: |
|
|
58
|
+
jobs:
|
|
59
|
+
check-token-format:
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
steps:
|
|
62
|
+
- name: Check GITHUB_TOKEN length
|
|
63
|
+
env:
|
|
64
|
+
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
65
|
+
run: |
|
|
66
|
+
echo "Token length: ${#TOKEN}"
|
|
67
|
+
# Old format: exactly 40 chars
|
|
68
|
+
# New format (post-April 2026 rollout): ~520 chars (variable)
|
|
69
|
+
# If your tooling uses this token, ensure it handles both lengths
|
|
70
|
+
- language: yaml
|
|
71
|
+
label: 'Correct regex for ghs_ tokens in secret scanning custom patterns'
|
|
72
|
+
code: |
|
|
73
|
+
# WRONG — only matches old 40-char opaque tokens:
|
|
74
|
+
# ghs_[A-Za-z0-9]{36}
|
|
75
|
+
#
|
|
76
|
+
# CORRECT — matches old and new JWT format:
|
|
77
|
+
# ghs_[A-Za-z0-9._-]+
|
|
78
|
+
#
|
|
79
|
+
# .github/secret_scanning.yml custom pattern (if scanning for leaked tokens):
|
|
80
|
+
custom_patterns:
|
|
81
|
+
- name: GitHub App installation token (all formats)
|
|
82
|
+
regex: 'ghs_[A-Za-z0-9._-]+'
|
|
83
|
+
prevention:
|
|
84
|
+
- 'Treat all GitHub tokens as variable-length opaque strings — never hardcode expected length'
|
|
85
|
+
- 'Use TEXT or VARCHAR(1024) for any database column that stores GitHub App installation tokens'
|
|
86
|
+
- 'Validate ghs_ tokens by prefix only, not by exact length or post-prefix character class'
|
|
87
|
+
- 'Subscribe to the GitHub Changelog (github.blog/changelog) to catch token format changes early'
|
|
88
|
+
- 'Use X-GitHub-Stateless-S2S-Token: enabled header to test your tooling before the rollout reaches your repos'
|
|
89
|
+
docs:
|
|
90
|
+
- url: 'https://github.blog/changelog/2026-04-24-notice-about-upcoming-new-format-for-github-app-installation-tokens/'
|
|
91
|
+
label: 'GitHub Changelog: Notice about upcoming new format for GitHub App installation tokens (April 24, 2026)'
|
|
92
|
+
- url: 'https://github.blog/changelog/2026-05-15-github-app-installation-tokens-per-request-override-header/'
|
|
93
|
+
label: 'GitHub Changelog: Per-request override header for testing the new token format (May 15, 2026)'
|
|
94
|
+
- url: 'https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app'
|
|
95
|
+
label: 'GitHub Docs: Generating an installation access token for a GitHub App'
|
|
@@ -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,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,119 @@
|
|
|
1
|
+
id: 'silent-failures-038'
|
|
2
|
+
title: '`download-artifact` `merge-multiple: true` silently corrupts files when multiple artifacts contain identical filenames'
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- download-artifact
|
|
7
|
+
- merge-multiple
|
|
8
|
+
- file-corruption
|
|
9
|
+
- parallel-download
|
|
10
|
+
- race-condition
|
|
11
|
+
- artifacts
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'merge-multiple:\s*true'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'Downloading artifact:.*\n.*Downloading artifact:'
|
|
16
|
+
flags: 'im'
|
|
17
|
+
error_messages:
|
|
18
|
+
- 'Downloading artifact: build-linux to /home/runner/work/repo/repo/dist'
|
|
19
|
+
- 'Downloading artifact: build-macos to /home/runner/work/repo/repo/dist'
|
|
20
|
+
root_cause: |
|
|
21
|
+
When actions/download-artifact (v4+) is used with merge-multiple: true and two or more
|
|
22
|
+
artifacts contain files with identical relative paths (e.g., multiple matrix builds each
|
|
23
|
+
upload a file named checksums.txt or report.json), the action downloads all artifacts
|
|
24
|
+
in parallel using a PARALLEL_DOWNLOADS worker pool.
|
|
25
|
+
|
|
26
|
+
All parallel workers write to the same destination directory concurrently. When two
|
|
27
|
+
workers attempt to write to the same filename simultaneously, the result is undefined:
|
|
28
|
+
- The output file may contain interleaved bytes from both sources
|
|
29
|
+
- The file may be truncated to the size of the smaller upload
|
|
30
|
+
- One artifact's version may completely overwrite the other (last writer wins)
|
|
31
|
+
|
|
32
|
+
The action exits with code 0 and emits no error or warning. The log shows both artifacts
|
|
33
|
+
downloading normally. The resulting corrupt file is indistinguishable from a valid one
|
|
34
|
+
unless explicitly checksummed.
|
|
35
|
+
|
|
36
|
+
Bug confirmed in actions/download-artifact#395 (March 2025, still open as of Jan 2026).
|
|
37
|
+
Affects: v4.x, v5.x, v6.x, v7.x
|
|
38
|
+
|
|
39
|
+
Common patterns that trigger this bug:
|
|
40
|
+
- Matrix builds across OS/arch each producing a same-named binary or manifest
|
|
41
|
+
- Release workflows where each artifact contains a same-named README.md or LICENSE
|
|
42
|
+
- Merge-and-sign workflows where each per-platform artifact has a checksums.txt
|
|
43
|
+
fix: |
|
|
44
|
+
Option 1 — Rename artifact files to include OS/arch before uploading so no two
|
|
45
|
+
artifacts ever contain a file at the same relative path.
|
|
46
|
+
|
|
47
|
+
Option 2 — Download each artifact by name into a separate directory rather than using
|
|
48
|
+
merge-multiple: true.
|
|
49
|
+
|
|
50
|
+
Option 3 — Validate checksums of all critical files after downloading and before
|
|
51
|
+
using them, so corruption is detected rather than silently propagated.
|
|
52
|
+
|
|
53
|
+
Option 4 — Use pattern: to download specific artifact subsets one group at a time,
|
|
54
|
+
ensuring no overlapping filenames within each group.
|
|
55
|
+
fix_code:
|
|
56
|
+
- language: yaml
|
|
57
|
+
label: 'Include OS/arch in output filenames to eliminate same-path collisions'
|
|
58
|
+
code: |
|
|
59
|
+
jobs:
|
|
60
|
+
build:
|
|
61
|
+
strategy:
|
|
62
|
+
matrix:
|
|
63
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
64
|
+
runs-on: ${{ matrix.os }}
|
|
65
|
+
steps:
|
|
66
|
+
- name: Build and rename output to include OS identifier
|
|
67
|
+
run: |
|
|
68
|
+
cp build/output dist/output-${{ runner.os }}-${{ runner.arch }}
|
|
69
|
+
|
|
70
|
+
- uses: actions/upload-artifact@v4
|
|
71
|
+
with:
|
|
72
|
+
name: build-${{ runner.os }}-${{ runner.arch }}
|
|
73
|
+
path: dist/output-${{ runner.os }}-${{ runner.arch }}
|
|
74
|
+
|
|
75
|
+
release:
|
|
76
|
+
needs: build
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
steps:
|
|
79
|
+
- name: Download all artifacts (safe — all filenames are unique)
|
|
80
|
+
uses: actions/download-artifact@v4
|
|
81
|
+
with:
|
|
82
|
+
pattern: build-*
|
|
83
|
+
path: dist/
|
|
84
|
+
merge-multiple: true
|
|
85
|
+
- language: yaml
|
|
86
|
+
label: 'Download each artifact by name into a separate directory (avoids merge-multiple entirely)'
|
|
87
|
+
code: |
|
|
88
|
+
jobs:
|
|
89
|
+
release:
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
steps:
|
|
92
|
+
- uses: actions/download-artifact@v4
|
|
93
|
+
with:
|
|
94
|
+
name: build-Linux
|
|
95
|
+
path: dist/linux/
|
|
96
|
+
|
|
97
|
+
- uses: actions/download-artifact@v4
|
|
98
|
+
with:
|
|
99
|
+
name: build-macOS
|
|
100
|
+
path: dist/macos/
|
|
101
|
+
|
|
102
|
+
- uses: actions/download-artifact@v4
|
|
103
|
+
with:
|
|
104
|
+
name: build-Windows
|
|
105
|
+
path: dist/windows/
|
|
106
|
+
|
|
107
|
+
# Files are in separate directories — no parallel write collision possible
|
|
108
|
+
prevention:
|
|
109
|
+
- 'Always include a unique dimension (OS, arch, matrix value) in filenames when uploading from matrix jobs'
|
|
110
|
+
- 'Audit all artifacts before using merge-multiple: true to confirm no two artifacts share a file path'
|
|
111
|
+
- 'Verify checksums of all merged files before using them in signing, publishing, or deployment steps'
|
|
112
|
+
- 'Prefer downloading by name into separate directories over merge-multiple when file collisions are possible'
|
|
113
|
+
docs:
|
|
114
|
+
- url: 'https://github.com/actions/download-artifact/issues/395'
|
|
115
|
+
label: 'actions/download-artifact#395: merge-multiple silently corrupts files when same-named files exist (open, March 2025)'
|
|
116
|
+
- url: 'https://github.com/actions/download-artifact/blob/main/docs/MIGRATION.md#merging-multiple-artifacts'
|
|
117
|
+
label: 'download-artifact migration guide: Merging multiple artifacts'
|
|
118
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflows-do/storing-workflow-data-as-artifacts'
|
|
119
|
+
label: 'GitHub Docs: Storing workflow data as artifacts'
|
|
@@ -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'
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
id: 'triggers-027'
|
|
2
|
+
title: 'Copilot coding agent PR workflows stuck in `action_required` — require human approval before running'
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- copilot
|
|
7
|
+
- coding-agent
|
|
8
|
+
- approval-required
|
|
9
|
+
- action-required
|
|
10
|
+
- pull-request
|
|
11
|
+
- workflow-approval
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'action_required'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'waiting for approval.*(?:outside contributor|fork|bot)|Approve and run'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- 'This workflow is waiting for approval from a maintainer. Learn more about approving workflows from public forks.'
|
|
19
|
+
- 'Workflow run is in action_required state. Approval is required before jobs can start.'
|
|
20
|
+
root_cause: |
|
|
21
|
+
Starting April 2025, GitHub treats the Copilot coding agent as an "outside contributor"
|
|
22
|
+
for GitHub Actions workflow approval purposes — the same policy applied to first-time
|
|
23
|
+
contributors from public forks.
|
|
24
|
+
|
|
25
|
+
When Copilot coding agent opens a pull request or pushes commits to a branch:
|
|
26
|
+
- Associated CI/CD workflows do NOT run automatically
|
|
27
|
+
- The check suite is created with conclusion: action_required
|
|
28
|
+
- No jobs start until a human with repository write access (or actions:write) manually
|
|
29
|
+
clicks "Approve and run workflows" in the PR UI, or approves via the REST API
|
|
30
|
+
- Unapproved runs are automatically deleted after 30 days with no notification
|
|
31
|
+
|
|
32
|
+
This is by design: Copilot-triggered workflows may have access to GITHUB_TOKEN,
|
|
33
|
+
repository secrets, and deployment environments. The approval gate prevents a
|
|
34
|
+
compromised or misbehaving AI agent from triggering arbitrary CI/CD actions.
|
|
35
|
+
|
|
36
|
+
However, this significantly slows AI-assisted development loops where developers rely
|
|
37
|
+
on CI feedback to validate Copilot's work. Teams discover this when:
|
|
38
|
+
- All Copilot-opened PRs show "Waiting for approval" with no CI results
|
|
39
|
+
- Required status checks never populate, blocking PR merges
|
|
40
|
+
- Automated pipelines that expected CI to run on all PRs silently stall
|
|
41
|
+
|
|
42
|
+
Affects: all GitHub Actions workflows triggered by pull_request, push, or
|
|
43
|
+
pull_request_target events on branches where Copilot coding agent is the commit author.
|
|
44
|
+
fix: |
|
|
45
|
+
Option 1 (Recommended for trusted private repos): Enable automatic workflow runs for
|
|
46
|
+
Copilot coding agent in repository settings:
|
|
47
|
+
Settings → Actions → General → "Approval for running workflows triggered by Copilot
|
|
48
|
+
coding agent" → "Allow Copilot coding agent to trigger workflows without approval"
|
|
49
|
+
(Available since March 2026 changelog)
|
|
50
|
+
|
|
51
|
+
Option 2: Manually approve each run after Copilot opens a PR.
|
|
52
|
+
In the PR, click "Approve and run workflows" in the Checks section.
|
|
53
|
+
|
|
54
|
+
Option 3: Build a lightweight auto-approve workflow triggered by check_suite events
|
|
55
|
+
with action: requested, scoped to Copilot actor identity.
|
|
56
|
+
fix_code:
|
|
57
|
+
- language: yaml
|
|
58
|
+
label: 'Approve a pending workflow run via GitHub CLI'
|
|
59
|
+
code: |
|
|
60
|
+
# List runs awaiting approval on a Copilot PR branch:
|
|
61
|
+
# gh run list --repo owner/repo --branch copilot/issue-123
|
|
62
|
+
|
|
63
|
+
# Approve a specific run (requires write access):
|
|
64
|
+
# gh run review <run-id> --approve --repo owner/repo
|
|
65
|
+
|
|
66
|
+
# Or approve via REST API:
|
|
67
|
+
# POST /repos/{owner}/{repo}/actions/runs/{run_id}/approve
|
|
68
|
+
- language: yaml
|
|
69
|
+
label: 'Auto-approve workflow: trigger CI immediately on Copilot PRs (use with caution)'
|
|
70
|
+
code: |
|
|
71
|
+
name: Auto-approve Copilot workflows
|
|
72
|
+
on:
|
|
73
|
+
workflow_run:
|
|
74
|
+
workflows: ['CI']
|
|
75
|
+
types: [requested]
|
|
76
|
+
|
|
77
|
+
jobs:
|
|
78
|
+
approve:
|
|
79
|
+
runs-on: ubuntu-latest
|
|
80
|
+
# Only auto-approve if triggered by Copilot coding agent
|
|
81
|
+
if: github.event.workflow_run.actor.login == 'copilot-swe-agent[bot]'
|
|
82
|
+
permissions:
|
|
83
|
+
actions: write
|
|
84
|
+
steps:
|
|
85
|
+
- name: Approve the workflow run
|
|
86
|
+
env:
|
|
87
|
+
RUN_ID: ${{ github.event.workflow_run.id }}
|
|
88
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
89
|
+
run: |
|
|
90
|
+
gh run review $RUN_ID --approve --repo ${{ github.repository }}
|
|
91
|
+
prevention:
|
|
92
|
+
- 'Configure the "Allow Copilot coding agent to trigger workflows without approval" setting if your team uses Copilot heavily and the repo is trusted'
|
|
93
|
+
- 'Monitor for action_required check suite conclusions so stalled Copilot PRs do not block merges silently'
|
|
94
|
+
- 'Scope GITHUB_TOKEN permissions to the minimum required before enabling auto-approval, to reduce the blast radius if Copilot misbehaves'
|
|
95
|
+
- 'Set up required status checks carefully — if CI never runs, required checks will block PR merges indefinitely'
|
|
96
|
+
docs:
|
|
97
|
+
- url: 'https://github.blog/changelog/2025-04-15-upcoming-breaking-changes-and-releases-for-github-actions/'
|
|
98
|
+
label: 'GitHub Changelog: Copilot events not automatically triggering GitHub Actions workflows (April 2025)'
|
|
99
|
+
- url: 'https://github.blog/changelog/2026-03-13-optionally-skip-approval-for-copilot-coding-agent-actions-workflows/'
|
|
100
|
+
label: 'GitHub Changelog: Optionally skip approval for Copilot coding agent Actions workflows (March 2026)'
|
|
101
|
+
- url: 'https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/approving-workflow-runs-from-public-forks'
|
|
102
|
+
label: 'GitHub Docs: Approving workflow runs from public forks'
|
|
103
|
+
- url: 'https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/configuring-settings-for-github-copilot-coding-agent'
|
|
104
|
+
label: 'GitHub Docs: Configuring settings for GitHub Copilot coding agent'
|
package/package.json
CHANGED