@htekdev/actions-debugger 1.0.105 → 1.0.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/errors/caching-artifacts/cache-save-restore-path-mismatch-silent-miss.yml +93 -0
- package/errors/caching-artifacts/setup-java-gradle-windows-lock-files-device-busy.yml +81 -0
- package/errors/runner-environment/checkout-lfs-double-authorization-header-non-github-host.yml +70 -0
- package/errors/runner-environment/hashfiles-fails-on-macos-fail-to-hash-files-under-directory.yml +116 -0
- package/errors/silent-failures/reusable-workflow-input-default-bypassed-by-empty-string.yml +144 -0
- package/errors/triggers/pull-request-paths-filter-evaluates-entire-pr-diff-not-latest-commit.yml +137 -0
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
id: caching-artifacts-062
|
|
2
|
+
title: "actions/cache save and restore Separate Actions Silently Miss When Paths Differ"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- cache-save
|
|
8
|
+
- cache-restore
|
|
9
|
+
- path-mismatch
|
|
10
|
+
- cache-miss
|
|
11
|
+
- silent
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'Cache not found for input keys'
|
|
14
|
+
flags: i
|
|
15
|
+
error_messages:
|
|
16
|
+
- "Cache not found for input keys: my-cache-key"
|
|
17
|
+
root_cause: |
|
|
18
|
+
When using actions/cache/save and actions/cache/restore as separate actions (split across
|
|
19
|
+
different jobs), the cache archive is indexed by BOTH the cache key AND the path provided
|
|
20
|
+
to the save action. The restore action must specify the EXACT same path as the save action
|
|
21
|
+
or no cache will be found, even when the key matches perfectly.
|
|
22
|
+
|
|
23
|
+
This is undocumented behaviour: the path is part of the cache version hash used to locate
|
|
24
|
+
the archive, not just the key. Using a relative vs absolute path, a trailing slash, or any
|
|
25
|
+
variation in path formatting between save and restore results in "Cache not found for input
|
|
26
|
+
keys" with no hint that path mismatch is the cause.
|
|
27
|
+
|
|
28
|
+
This differs from using the combined actions/cache action in a single job, where the path
|
|
29
|
+
is always consistent between save and restore. Reported in actions/cache#1444.
|
|
30
|
+
fix: |
|
|
31
|
+
Ensure the path: value in actions/cache/restore exactly matches the path: value used in
|
|
32
|
+
actions/cache/save — including the same relative-vs-absolute format and no trailing slash
|
|
33
|
+
differences.
|
|
34
|
+
|
|
35
|
+
Best practice: extract the path into a shared env variable or workflow-level output, or
|
|
36
|
+
document the exact path string in both jobs.
|
|
37
|
+
fix_code:
|
|
38
|
+
- language: yaml
|
|
39
|
+
label: "Wrong: different paths between save and restore jobs"
|
|
40
|
+
code: |
|
|
41
|
+
jobs:
|
|
42
|
+
build:
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
steps:
|
|
45
|
+
- name: Create files
|
|
46
|
+
run: mkdir my-files && echo "data" > my-files/data.txt
|
|
47
|
+
- uses: actions/cache/save@v4
|
|
48
|
+
with:
|
|
49
|
+
path: my-files # saved with relative path
|
|
50
|
+
key: my-cache-${{ github.run_id }}
|
|
51
|
+
|
|
52
|
+
deploy:
|
|
53
|
+
needs: build
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/cache/restore@v4
|
|
57
|
+
with:
|
|
58
|
+
path: ./my-files # WRONG: './my-files' != 'my-files' -- cache not found
|
|
59
|
+
key: my-cache-${{ github.run_id }}
|
|
60
|
+
- language: yaml
|
|
61
|
+
label: "Correct: identical path strings in save and restore"
|
|
62
|
+
code: |
|
|
63
|
+
jobs:
|
|
64
|
+
build:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- name: Create files
|
|
68
|
+
run: mkdir my-files && echo "data" > my-files/data.txt
|
|
69
|
+
- uses: actions/cache/save@v4
|
|
70
|
+
with:
|
|
71
|
+
path: my-files # use identical path
|
|
72
|
+
key: my-cache-${{ github.run_id }}
|
|
73
|
+
|
|
74
|
+
deploy:
|
|
75
|
+
needs: build
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
steps:
|
|
78
|
+
- uses: actions/cache/restore@v4
|
|
79
|
+
with:
|
|
80
|
+
path: my-files # exact match — cache found
|
|
81
|
+
key: my-cache-${{ github.run_id }}
|
|
82
|
+
prevention:
|
|
83
|
+
- "Copy the exact path: string from your save step into your restore step — do not retype it"
|
|
84
|
+
- "Avoid mixing relative (my-dir) and absolute (${{ github.workspace }}/my-dir) paths across jobs"
|
|
85
|
+
- "If in doubt, use the combined actions/cache@v4 action in a single job instead of split save/restore"
|
|
86
|
+
- "Print the cache key and path in both jobs to make debugging easier"
|
|
87
|
+
docs:
|
|
88
|
+
- url: "https://github.com/actions/cache/issues/1444"
|
|
89
|
+
label: "actions/cache#1444 — Restore reports cache not found when path differs from save path"
|
|
90
|
+
- url: "https://github.com/actions/cache/tree/main/save"
|
|
91
|
+
label: "actions/cache/save — Split save/restore documentation"
|
|
92
|
+
- url: "https://github.com/actions/cache/tree/main/restore"
|
|
93
|
+
label: "actions/cache/restore — Split save/restore documentation"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
id: caching-artifacts-061
|
|
2
|
+
title: "setup-java cache: 'gradle' Silently Corrupts Cache on Windows — Gradle Lock Files Device or Resource Busy"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- setup-java
|
|
7
|
+
- gradle
|
|
8
|
+
- windows
|
|
9
|
+
- cache
|
|
10
|
+
- lock-files
|
|
11
|
+
- device-busy
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: '\.lock.*Read error.*Device or resource busy'
|
|
14
|
+
flags: i
|
|
15
|
+
- regex: 'fileContent\.lock.*Device or resource busy'
|
|
16
|
+
flags: i
|
|
17
|
+
- regex: '\.gradle/caches.*Read error.*byte 0'
|
|
18
|
+
flags: i
|
|
19
|
+
error_messages:
|
|
20
|
+
- "/usr/bin/tar: C\\:/Users/runneradmin/.gradle/caches/8.7/fileContent/fileContent.lock: Read error at byte 0, while reading 38 bytes: Device or resource busy"
|
|
21
|
+
- "/usr/bin/tar: Exiting with failure status due to previous errors"
|
|
22
|
+
- "Warning: Failed to save: ... failed with exit code 2"
|
|
23
|
+
root_cause: |
|
|
24
|
+
On Windows runners, actions/setup-java with cache: 'gradle' triggers a cache save in its
|
|
25
|
+
post-job step. At that point, the Gradle daemon process started during the build step may
|
|
26
|
+
still be running and holding open file locks on these cache directories:
|
|
27
|
+
- .gradle/caches/*/fileContent.lock
|
|
28
|
+
- .gradle/caches/*/fileHashes.lock
|
|
29
|
+
- .gradle/caches/*/generated-gradle-jars.lock
|
|
30
|
+
- .gradle/caches/*/javaCompile.lock
|
|
31
|
+
- .gradle/caches/journal-1/journal-1.lock
|
|
32
|
+
- .gradle/caches/modules-*/modules-*.lock
|
|
33
|
+
|
|
34
|
+
The GNU tar bundled with Git for Windows (used by the GitHub Actions runner) cannot read
|
|
35
|
+
Windows-locked files and exits with code 2. Despite this failure, setup-java logs
|
|
36
|
+
"Cache saved with the key: ..." because the cache service accepted a partial archive.
|
|
37
|
+
|
|
38
|
+
The silent failure: subsequent runs restore the partial cache, missing the locked files.
|
|
39
|
+
Gradle then redownloads dependencies on every run, and the cache save repeats the same
|
|
40
|
+
error. Cache size appears normal in the UI but the archive contents are incomplete.
|
|
41
|
+
Reported in actions/setup-java#633.
|
|
42
|
+
fix: |
|
|
43
|
+
Stop the Gradle daemon explicitly before the post-job step fires. The setup-java
|
|
44
|
+
post-step runs after all workflow steps complete, so stopping the daemon in the last
|
|
45
|
+
step ensures no lock files are held when the post-step archives the cache:
|
|
46
|
+
fix_code:
|
|
47
|
+
- language: yaml
|
|
48
|
+
label: "Fix: stop Gradle daemon before post-step cache save"
|
|
49
|
+
code: |
|
|
50
|
+
jobs:
|
|
51
|
+
build:
|
|
52
|
+
runs-on: windows-latest
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
- uses: actions/setup-java@v4
|
|
56
|
+
with:
|
|
57
|
+
java-version: '21'
|
|
58
|
+
distribution: 'temurin'
|
|
59
|
+
cache: 'gradle'
|
|
60
|
+
- name: Build with Gradle
|
|
61
|
+
run: ./gradlew build
|
|
62
|
+
- name: Stop Gradle daemon before cache save
|
|
63
|
+
if: always()
|
|
64
|
+
run: ./gradlew --stop
|
|
65
|
+
- language: yaml
|
|
66
|
+
label: "Alternative: build with --no-daemon (no daemon started, no lock files)"
|
|
67
|
+
code: |
|
|
68
|
+
- name: Build with Gradle (no daemon)
|
|
69
|
+
run: ./gradlew build --no-daemon
|
|
70
|
+
prevention:
|
|
71
|
+
- "Always add a './gradlew --stop' step (with if: always()) after your build step on Windows runners"
|
|
72
|
+
- "Use --no-daemon for Gradle builds in CI to avoid daemon lock file issues entirely"
|
|
73
|
+
- "Verify cache usefulness by checking whether Gradle redownloads dependencies on runs after cache save"
|
|
74
|
+
- "Consider using the official gradle/actions/setup-gradle action which handles daemon lifecycle better"
|
|
75
|
+
docs:
|
|
76
|
+
- url: "https://github.com/actions/setup-java/issues/633"
|
|
77
|
+
label: "actions/setup-java#633 — Gradle caching post-task fails on Windows with 'device or resource busy'"
|
|
78
|
+
- url: "https://docs.gradle.org/current/userguide/gradle_daemon.html"
|
|
79
|
+
label: "Gradle Daemon documentation — stopping and disabling"
|
|
80
|
+
- url: "https://github.com/gradle/actions/blob/main/docs/setup-gradle.md"
|
|
81
|
+
label: "gradle/actions — recommended Gradle CI caching action"
|
package/errors/runner-environment/checkout-lfs-double-authorization-header-non-github-host.yml
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
id: runner-environment-175
|
|
2
|
+
title: "checkout@v4 LFS with Non-GitHub Host Sends Double Authorization Header -- 400 Bad Request"
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- checkout
|
|
7
|
+
- lfs
|
|
8
|
+
- git-lfs
|
|
9
|
+
- non-github-host
|
|
10
|
+
- authorization-header
|
|
11
|
+
- submodule
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'Authorization.*Authorization'
|
|
14
|
+
flags: i
|
|
15
|
+
- regex: 'HTTP.*400.*lfs'
|
|
16
|
+
flags: i
|
|
17
|
+
error_messages:
|
|
18
|
+
- "trace git-lfs: HTTP: 400"
|
|
19
|
+
- "batch request failed with status 400"
|
|
20
|
+
root_cause: |
|
|
21
|
+
When actions/checkout is used with lfs: true and the repository contains submodules
|
|
22
|
+
pointing to a non-GitHub host (Gitea, Bitbucket, self-hosted GitLab, etc.), the checkout
|
|
23
|
+
action writes an HTTP Authorization header into .git/config via http.<url>.extraheader.
|
|
24
|
+
For GitHub.com remotes this works correctly. However, for submodule remotes on other hosts,
|
|
25
|
+
the git-lfs client then sends BOTH this injected header AND its own credential-helper-derived
|
|
26
|
+
Authorization header in the same LFS batch request.
|
|
27
|
+
|
|
28
|
+
Most non-GitHub servers reject duplicate Authorization headers with HTTP 400 Bad Request,
|
|
29
|
+
as the HTTP spec treats repeated Authorization headers as ambiguous. GitHub.com silently
|
|
30
|
+
deduplicates them, hiding the problem. Only non-GitHub hosts expose this bug.
|
|
31
|
+
|
|
32
|
+
Diagnosed by enabling GIT_CURL_VERBOSE: 1 and GIT_TRACE: 1 in the workflow environment,
|
|
33
|
+
which reveals two Authorization: Basic lines in the same LFS request headers.
|
|
34
|
+
Reported in actions/checkout#1830 (15 reactions).
|
|
35
|
+
fix: |
|
|
36
|
+
Use SSH key authentication for the non-GitHub submodule remote — no HTTP Authorization
|
|
37
|
+
header is injected for SSH remotes, eliminating the conflict:
|
|
38
|
+
fix_code:
|
|
39
|
+
- language: yaml
|
|
40
|
+
label: "Recommended: use ssh-key for non-GitHub submodule host"
|
|
41
|
+
code: |
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
with:
|
|
44
|
+
submodules: recursive
|
|
45
|
+
lfs: true
|
|
46
|
+
ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }}
|
|
47
|
+
- language: yaml
|
|
48
|
+
label: "Alternative: disable lfs on checkout and pull LFS per-host manually"
|
|
49
|
+
code: |
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/checkout@v4
|
|
52
|
+
with:
|
|
53
|
+
submodules: recursive
|
|
54
|
+
lfs: false # prevents checkout from injecting LFS auth header
|
|
55
|
+
- name: Clear injected extraheader and pull LFS for non-GitHub host
|
|
56
|
+
env:
|
|
57
|
+
GIT_CURL_VERBOSE: "1"
|
|
58
|
+
run: |
|
|
59
|
+
cd path/to/non-github-submodule
|
|
60
|
+
git config --unset http.https://your-host.example.com/.extraheader || true
|
|
61
|
+
git lfs pull
|
|
62
|
+
prevention:
|
|
63
|
+
- "Use SSH keys for non-GitHub submodule remotes to avoid HTTP credential injection conflicts"
|
|
64
|
+
- "Enable GIT_CURL_VERBOSE=1 to diagnose duplicate Authorization headers in LFS requests"
|
|
65
|
+
- "Test LFS workflows after upgrading checkout to a new major version"
|
|
66
|
+
docs:
|
|
67
|
+
- url: "https://github.com/actions/checkout/issues/1830"
|
|
68
|
+
label: "actions/checkout#1830 -- LFS fails with double auth header on non-GitHub host"
|
|
69
|
+
- url: "https://github.com/actions/checkout#readme"
|
|
70
|
+
label: "actions/checkout -- lfs and submodules input documentation"
|
package/errors/runner-environment/hashfiles-fails-on-macos-fail-to-hash-files-under-directory.yml
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
id: runner-environment-176
|
|
2
|
+
title: "hashFiles() fails on macOS with 'Fail to hash files under directory'"
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- hashfiles
|
|
7
|
+
- macos
|
|
8
|
+
- runner-images
|
|
9
|
+
- cache
|
|
10
|
+
- expression
|
|
11
|
+
- image-regression
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'hashFiles\(.+\) failed\. Fail to hash files under directory'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'The template is not valid.*hashFiles.*Fail to hash files'
|
|
16
|
+
flags: 'is'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Error: The template is not valid. /Users/runner/work/.../action.yml (Line: 48, Col: 12): hashFiles('**/go.sum') failed. Fail to hash files under directory '/Users/runner/work/...'"
|
|
19
|
+
- "hashFiles('**/package-lock.json') failed. Fail to hash files under directory '/Users/runner/work/...'"
|
|
20
|
+
root_cause: |
|
|
21
|
+
Certain macOS runner image versions (including macos-15-arm64 image
|
|
22
|
+
20251119.0020 through approximately 20251122) introduced a regression
|
|
23
|
+
where the hashFiles() expression function fails to enumerate files
|
|
24
|
+
under the workspace directory on macOS. The same workflow succeeds on
|
|
25
|
+
Linux and Windows runners.
|
|
26
|
+
|
|
27
|
+
The failure manifests whenever hashFiles() is used in any expression
|
|
28
|
+
context — strategy.matrix, cache key, conditional — on the affected
|
|
29
|
+
macOS runner image versions. The underlying cause is a filesystem
|
|
30
|
+
enumeration permission or path-resolution change introduced by the
|
|
31
|
+
runner image update (see actions/runner-images #13341). The issue was
|
|
32
|
+
not present in the previous macOS image version and reappeared without
|
|
33
|
+
workflow changes.
|
|
34
|
+
|
|
35
|
+
Root cause in runner-images: the runner image update changed system
|
|
36
|
+
configuration in a way that prevented the runner process from
|
|
37
|
+
recursively listing files under the workspace directory on macOS. The
|
|
38
|
+
fix was a runner image rollback / patch released around December 12,
|
|
39
|
+
2025.
|
|
40
|
+
|
|
41
|
+
This failure is a runner-image regression, not a workflow bug. It can
|
|
42
|
+
recur if future macOS runner image updates introduce similar filesystem
|
|
43
|
+
configuration changes.
|
|
44
|
+
fix: |
|
|
45
|
+
If you encounter this error on macOS:
|
|
46
|
+
|
|
47
|
+
1. Wait for GitHub to release a patched macOS runner image — runner
|
|
48
|
+
image regressions are typically resolved within days. Monitor
|
|
49
|
+
actions/runner-images for the fix.
|
|
50
|
+
|
|
51
|
+
2. Pin your workflow to a specific macOS runner image version that
|
|
52
|
+
predates the regression while waiting for the fix.
|
|
53
|
+
|
|
54
|
+
3. As a temporary workaround, replace hashFiles() with a manual
|
|
55
|
+
hash computation step using sha256sum or similar:
|
|
56
|
+
|
|
57
|
+
- name: Compute hash manually (macOS workaround)
|
|
58
|
+
id: hash
|
|
59
|
+
run: |
|
|
60
|
+
echo "hash=$(find . -name 'go.sum' -exec sha256sum {} \; | sha256sum | cut -d' ' -f1)" >> $GITHUB_OUTPUT
|
|
61
|
+
- uses: actions/cache@v4
|
|
62
|
+
with:
|
|
63
|
+
path: ~/go/pkg/mod
|
|
64
|
+
key: ${{ runner.os }}-go-${{ steps.hash.outputs.hash }}
|
|
65
|
+
|
|
66
|
+
4. After the runner image is updated to a fixed version, remove the
|
|
67
|
+
workaround and restore hashFiles().
|
|
68
|
+
fix_code:
|
|
69
|
+
- language: yaml
|
|
70
|
+
label: "Manual hash workaround for macOS hashFiles() regression"
|
|
71
|
+
code: |
|
|
72
|
+
jobs:
|
|
73
|
+
build:
|
|
74
|
+
runs-on: macos-latest
|
|
75
|
+
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/checkout@v4
|
|
78
|
+
|
|
79
|
+
# Workaround: compute cache key hash manually when hashFiles() fails on macOS
|
|
80
|
+
- name: Compute go.sum hash
|
|
81
|
+
id: hash
|
|
82
|
+
run: |
|
|
83
|
+
echo "hash=$(find . -name 'go.sum' | sort | xargs sha256sum | sha256sum | cut -d' ' -f1)" \
|
|
84
|
+
>> "$GITHUB_OUTPUT"
|
|
85
|
+
|
|
86
|
+
- uses: actions/cache@v4
|
|
87
|
+
with:
|
|
88
|
+
path: ~/go/pkg/mod
|
|
89
|
+
key: ${{ runner.os }}-go-${{ steps.hash.outputs.hash }}
|
|
90
|
+
|
|
91
|
+
- run: go build ./...
|
|
92
|
+
- language: yaml
|
|
93
|
+
label: "Pin to a known-good macOS runner image while waiting for fix"
|
|
94
|
+
code: |
|
|
95
|
+
jobs:
|
|
96
|
+
build:
|
|
97
|
+
# Pin to a specific macOS runner image version that predates the regression.
|
|
98
|
+
# Check https://github.com/actions/runner-images/releases for available versions.
|
|
99
|
+
runs-on: macos-15-arm64
|
|
100
|
+
# Alternatively, use an older pinned version: macos-14 or macos-15-xlarge
|
|
101
|
+
# when the current macos-latest image has the regression.
|
|
102
|
+
prevention:
|
|
103
|
+
- "Subscribe to actions/runner-images release notifications to detect image regressions early."
|
|
104
|
+
- "When hashFiles() fails only on macOS (Linux/Windows succeed), suspect a runner image regression
|
|
105
|
+
rather than a workflow bug — check the actions/runner-images issue tracker for reports."
|
|
106
|
+
- "If CI budget allows, run a short nightly workflow that tests hashFiles() on all target OS
|
|
107
|
+
combinations to detect image regressions before they block your main CI."
|
|
108
|
+
- "When pinning a runner image version as a workaround, add a TODO comment and a link to the
|
|
109
|
+
upstream issue so the pin is removed once the fix is released."
|
|
110
|
+
docs:
|
|
111
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#hashfiles"
|
|
112
|
+
label: "GitHub Actions: hashFiles() expression function"
|
|
113
|
+
- url: "https://github.com/actions/runner/issues/4134"
|
|
114
|
+
label: "actions/runner #4134: hashFiles on macOS fails with 'Fail to hash files under directory' (14 reactions, closed Dec 2025)"
|
|
115
|
+
- url: "https://github.com/actions/runner-images/issues/13341"
|
|
116
|
+
label: "actions/runner-images #13341: macOS hashFiles regression report (Nov 2025)"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
id: silent-failures-094
|
|
2
|
+
title: "Reusable workflow input default: value silently ignored when caller passes empty string"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- reusable-workflow
|
|
7
|
+
- workflow-call
|
|
8
|
+
- inputs
|
|
9
|
+
- default
|
|
10
|
+
- empty-string
|
|
11
|
+
- silent-failure
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'on:\s*\n\s*workflow_call:'
|
|
14
|
+
flags: 'im'
|
|
15
|
+
- regex: 'inputs\.\w+\.default:'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'with:\s*\n(?:\s+\w+:\s*\$\{\{[^}]+\}\}\s*\n)+'
|
|
18
|
+
flags: 'im'
|
|
19
|
+
error_messages:
|
|
20
|
+
- "Reusable workflow input default value not used when caller passes empty string"
|
|
21
|
+
- "Expected default message, got empty string"
|
|
22
|
+
- "inputs.message is empty even though default is set"
|
|
23
|
+
root_cause: |
|
|
24
|
+
The default: field in on.workflow_call.inputs is only applied when the input is
|
|
25
|
+
completely absent (not provided by the caller at all). It is NOT applied when the
|
|
26
|
+
caller explicitly passes an empty string "".
|
|
27
|
+
|
|
28
|
+
A very common pattern is a caller workflow that passes a workflow_dispatch input
|
|
29
|
+
directly to a reusable workflow:
|
|
30
|
+
|
|
31
|
+
# caller.yml
|
|
32
|
+
on:
|
|
33
|
+
workflow_dispatch:
|
|
34
|
+
inputs:
|
|
35
|
+
message:
|
|
36
|
+
type: string
|
|
37
|
+
required: false # defaults to "" when not provided
|
|
38
|
+
|
|
39
|
+
jobs:
|
|
40
|
+
call:
|
|
41
|
+
uses: ./.github/workflows/reusable.yml
|
|
42
|
+
with:
|
|
43
|
+
message: ${{ inputs.message }} # passes "" when dispatch runs without input
|
|
44
|
+
|
|
45
|
+
# reusable.yml
|
|
46
|
+
on:
|
|
47
|
+
workflow_call:
|
|
48
|
+
inputs:
|
|
49
|
+
message:
|
|
50
|
+
type: string
|
|
51
|
+
default: 'default message'
|
|
52
|
+
|
|
53
|
+
When a developer triggers the caller workflow_dispatch without providing the
|
|
54
|
+
message input, inputs.message resolves to "" (the workflow_dispatch default for
|
|
55
|
+
unset string inputs). The caller then passes "" to the reusable workflow. The
|
|
56
|
+
reusable workflow receives "" as an explicitly-provided value, so its own
|
|
57
|
+
default: 'default message' is never used. The job runs with message = "".
|
|
58
|
+
|
|
59
|
+
This is by design: GitHub Actions treats "" as a valid non-absent value. The
|
|
60
|
+
default: key only activates for truly absent inputs (when with: does not include
|
|
61
|
+
the key at all). Since ${{ inputs.message }} always resolves to some string (even
|
|
62
|
+
"", it is always present in the with: block, so the called workflow's default
|
|
63
|
+
is always bypassed.
|
|
64
|
+
fix: |
|
|
65
|
+
Move the default value handling to the calling workflow rather than the reusable
|
|
66
|
+
workflow, using a conditional expression to fall back to the default:
|
|
67
|
+
|
|
68
|
+
# caller.yml — handle empty-string fallback here
|
|
69
|
+
jobs:
|
|
70
|
+
call:
|
|
71
|
+
uses: ./.github/workflows/reusable.yml
|
|
72
|
+
with:
|
|
73
|
+
message: ${{ inputs.message != '' && inputs.message || 'default message' }}
|
|
74
|
+
|
|
75
|
+
Alternatively, handle the fallback inside the reusable workflow steps using an
|
|
76
|
+
env variable substitution:
|
|
77
|
+
|
|
78
|
+
# reusable.yml
|
|
79
|
+
jobs:
|
|
80
|
+
run:
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
env:
|
|
83
|
+
EFFECTIVE_MESSAGE: ${{ inputs.message != '' && inputs.message || 'default message' }}
|
|
84
|
+
steps:
|
|
85
|
+
- run: echo "Message is $EFFECTIVE_MESSAGE"
|
|
86
|
+
|
|
87
|
+
Do NOT rely on default: in on.workflow_call.inputs when the caller may pass ""
|
|
88
|
+
via ${{ inputs.x }} from a workflow_dispatch input.
|
|
89
|
+
fix_code:
|
|
90
|
+
- language: yaml
|
|
91
|
+
label: "Apply the default in the caller with a conditional expression (recommended)"
|
|
92
|
+
code: |
|
|
93
|
+
# caller.yml
|
|
94
|
+
on:
|
|
95
|
+
workflow_dispatch:
|
|
96
|
+
inputs:
|
|
97
|
+
message:
|
|
98
|
+
type: string
|
|
99
|
+
required: false
|
|
100
|
+
|
|
101
|
+
jobs:
|
|
102
|
+
call:
|
|
103
|
+
uses: ./.github/workflows/reusable.yml
|
|
104
|
+
with:
|
|
105
|
+
# Use || to fall back to default when inputs.message is empty
|
|
106
|
+
message: ${{ inputs.message != '' && inputs.message || 'default message' }}
|
|
107
|
+
- language: yaml
|
|
108
|
+
label: "Apply the fallback inside the reusable workflow using env:"
|
|
109
|
+
code: |
|
|
110
|
+
# reusable.yml
|
|
111
|
+
on:
|
|
112
|
+
workflow_call:
|
|
113
|
+
inputs:
|
|
114
|
+
message:
|
|
115
|
+
type: string
|
|
116
|
+
required: false
|
|
117
|
+
# Note: default: here is bypassed when caller passes ""
|
|
118
|
+
|
|
119
|
+
jobs:
|
|
120
|
+
run:
|
|
121
|
+
runs-on: ubuntu-latest
|
|
122
|
+
env:
|
|
123
|
+
# Handle empty string at job level — inputs.message may be "" not null
|
|
124
|
+
EFFECTIVE_MESSAGE: ${{ inputs.message != '' && inputs.message || 'default message' }}
|
|
125
|
+
steps:
|
|
126
|
+
- name: Use effective message
|
|
127
|
+
run: echo "Message is $EFFECTIVE_MESSAGE"
|
|
128
|
+
prevention:
|
|
129
|
+
- "Never place default values only in on.workflow_call.inputs.default when the caller
|
|
130
|
+
passes the input via ${{ inputs.x }} from a workflow_dispatch — the empty string
|
|
131
|
+
will always bypass the default."
|
|
132
|
+
- "Place fallback logic in the caller using '${{ inputs.x != '''''' && inputs.x || ''default'' }}'
|
|
133
|
+
to handle both absent and empty-string cases."
|
|
134
|
+
- "If you need the reusable workflow to have its own default, handle it via env: fallback
|
|
135
|
+
inside the job steps, not via on.workflow_call.inputs.default."
|
|
136
|
+
- "This behavior stems from GitHub Actions treating \"\" as an explicitly provided string
|
|
137
|
+
value rather than an absent/null input. Document this distinction for your team."
|
|
138
|
+
docs:
|
|
139
|
+
- url: "https://docs.github.com/en/actions/sharing-automations/reusing-workflows#using-inputs-and-secrets-in-a-reusable-workflow"
|
|
140
|
+
label: "GitHub Actions: Using inputs and secrets in a reusable workflow"
|
|
141
|
+
- url: "https://github.com/actions/runner/issues/2907"
|
|
142
|
+
label: "actions/runner #2907: Reusable workflow input default not used when caller passes empty string (16 reactions)"
|
|
143
|
+
- url: "https://github.com/actions/runner/issues/924"
|
|
144
|
+
label: "actions/runner #924: Differentiate between input parameter empty or not present (76 reactions, still open)"
|
package/errors/triggers/pull-request-paths-filter-evaluates-entire-pr-diff-not-latest-commit.yml
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
id: triggers-068
|
|
2
|
+
title: "pull_request paths/paths-ignore filter evaluates entire PR diff, not latest commit"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- paths-filter
|
|
7
|
+
- paths-ignore
|
|
8
|
+
- pull_request
|
|
9
|
+
- three-dot-diff
|
|
10
|
+
- silent-trigger
|
|
11
|
+
- monorepo
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'paths-ignore:'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'on:\s*\n\s*pull_request:'
|
|
16
|
+
flags: 'im'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Workflow triggers on push to README.md even though README.md is listed in paths-ignore"
|
|
19
|
+
- "paths-ignore not respected after one commit touches files outside the ignore list"
|
|
20
|
+
- "Workflow keeps running for all subsequent commits in PR even though only ignored files changed"
|
|
21
|
+
root_cause: |
|
|
22
|
+
GitHub's paths and paths-ignore filters for pull_request (and pull_request_target)
|
|
23
|
+
events use a three-dot diff — a comparison between the most recent version of the
|
|
24
|
+
topic branch HEAD and the commit where the topic branch last diverged from the base
|
|
25
|
+
branch (the merge base). This diff encompasses ALL files changed across the entire
|
|
26
|
+
PR, not just the files changed in the most recent commit/push.
|
|
27
|
+
|
|
28
|
+
The practical consequence is: once any commit in the PR has touched a file that is
|
|
29
|
+
NOT in paths-ignore, the three-dot diff permanently includes that file for the
|
|
30
|
+
lifetime of the PR. All subsequent pushes to the same PR will also trigger the
|
|
31
|
+
workflow, even if those pushes only modify files listed in paths-ignore.
|
|
32
|
+
|
|
33
|
+
Example: a PR starts by pushing a Go source file (triggers the workflow). The
|
|
34
|
+
developer then pushes a README.md-only commit. Despite README.md being in
|
|
35
|
+
paths-ignore, the entire PR diff still includes the earlier Go change, so the
|
|
36
|
+
workflow fires again.
|
|
37
|
+
|
|
38
|
+
This behavior is documented under "Git diff comparisons" in the GitHub Actions
|
|
39
|
+
workflow syntax reference but is widely misunderstood.
|
|
40
|
+
|
|
41
|
+
Contrast with push events: push filters only examine files changed in the
|
|
42
|
+
individual push commit(s), so paths-ignore works as most developers expect.
|
|
43
|
+
fix: |
|
|
44
|
+
Option 1 — Use dorny/paths-filter to gate job execution at the step/job level
|
|
45
|
+
based on files changed only in the current push (most reliable workaround):
|
|
46
|
+
|
|
47
|
+
jobs:
|
|
48
|
+
guard:
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
outputs:
|
|
51
|
+
changed: ${{ steps.filter.outputs.src }}
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
- uses: dorny/paths-filter@v3
|
|
55
|
+
id: filter
|
|
56
|
+
with:
|
|
57
|
+
filters: |
|
|
58
|
+
src:
|
|
59
|
+
- 'src/**'
|
|
60
|
+
- '!README.md'
|
|
61
|
+
|
|
62
|
+
build:
|
|
63
|
+
needs: guard
|
|
64
|
+
if: needs.guard.outputs.changed == 'true'
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- run: echo "only runs if src changed in latest push"
|
|
68
|
+
|
|
69
|
+
Option 2 — Accept that pull_request paths filtering is PR-wide and use it only
|
|
70
|
+
for coarse-grained "should this workflow category run at all" decisions, not for
|
|
71
|
+
per-commit granularity.
|
|
72
|
+
|
|
73
|
+
Option 3 — Remove paths/paths-ignore from the pull_request trigger entirely and
|
|
74
|
+
use conditional job-level steps with dorny/paths-filter instead.
|
|
75
|
+
fix_code:
|
|
76
|
+
- language: yaml
|
|
77
|
+
label: "Use dorny/paths-filter for per-commit path gating (recommended)"
|
|
78
|
+
code: |
|
|
79
|
+
on:
|
|
80
|
+
pull_request:
|
|
81
|
+
branches: [main]
|
|
82
|
+
# No paths/paths-ignore here — filter is done per-commit in the job
|
|
83
|
+
|
|
84
|
+
jobs:
|
|
85
|
+
check-changes:
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
outputs:
|
|
88
|
+
src_changed: ${{ steps.filter.outputs.src }}
|
|
89
|
+
steps:
|
|
90
|
+
- uses: actions/checkout@v4
|
|
91
|
+
- uses: dorny/paths-filter@v3
|
|
92
|
+
id: filter
|
|
93
|
+
with:
|
|
94
|
+
filters: |
|
|
95
|
+
src:
|
|
96
|
+
- 'src/**'
|
|
97
|
+
- '*.go'
|
|
98
|
+
- '!**/*.md'
|
|
99
|
+
|
|
100
|
+
build:
|
|
101
|
+
needs: check-changes
|
|
102
|
+
if: needs.check-changes.outputs.src_changed == 'true'
|
|
103
|
+
runs-on: ubuntu-latest
|
|
104
|
+
steps:
|
|
105
|
+
- uses: actions/checkout@v4
|
|
106
|
+
- run: make build
|
|
107
|
+
- language: yaml
|
|
108
|
+
label: "What NOT to do — paths-ignore on pull_request silently fails after first non-ignored commit"
|
|
109
|
+
code: |
|
|
110
|
+
# This looks correct but DOES NOT work as expected for pull_request events:
|
|
111
|
+
on:
|
|
112
|
+
pull_request:
|
|
113
|
+
paths-ignore:
|
|
114
|
+
- '**.md' # <-- evaluated against entire PR diff, not latest push
|
|
115
|
+
- 'docs/**'
|
|
116
|
+
|
|
117
|
+
# After any commit in the PR touches a non-md file, ALL subsequent commits
|
|
118
|
+
# trigger this workflow, even if those commits only change .md files.
|
|
119
|
+
prevention:
|
|
120
|
+
- "Do not rely on paths-ignore to prevent a workflow from running on documentation-only
|
|
121
|
+
follow-up commits in a PR — it only works until any non-ignored file is committed to
|
|
122
|
+
the PR branch."
|
|
123
|
+
- "Use dorny/paths-filter or similar per-commit path detection actions instead of workflow-level
|
|
124
|
+
paths-ignore for pull_request events when per-commit granularity is needed."
|
|
125
|
+
- "For monorepos, consider separate workflows per component triggered by dorny/paths-filter
|
|
126
|
+
outputs rather than native paths filters."
|
|
127
|
+
- "paths-ignore works reliably for push events (single commit diff) — the behavior difference
|
|
128
|
+
between push and pull_request is a common source of confusion."
|
|
129
|
+
docs:
|
|
130
|
+
- url: "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#git-diff-comparisons"
|
|
131
|
+
label: "GitHub Actions workflow syntax: Git diff comparisons"
|
|
132
|
+
- url: "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#onpushpull_requestpull_request_targetpathspaths-ignore"
|
|
133
|
+
label: "GitHub Actions: paths and paths-ignore filter syntax"
|
|
134
|
+
- url: "https://github.com/actions/runner/issues/2324"
|
|
135
|
+
label: "actions/runner #2324: on.pull_request.paths-ignore are not respected correctly (64 reactions, still open)"
|
|
136
|
+
- url: "https://github.com/dorny/paths-filter"
|
|
137
|
+
label: "dorny/paths-filter — per-commit path change detection action"
|
package/package.json
CHANGED