@htekdev/actions-debugger 1.0.104 → 1.0.106
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-primary-key-prefix-matching-undocumented.yml +104 -0
- 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/concurrency-timing/batch-workflow-dispatch-static-concurrency-silent-cancel.yml +137 -0
- package/errors/runner-environment/checkout-lfs-double-authorization-header-non-github-host.yml +70 -0
- package/errors/runner-environment/sparse-checkout-persists-self-hosted-cross-run.yml +99 -0
- package/errors/yaml-syntax/hashfiles-newline-delimiter-returns-empty.yml +99 -0
- package/package.json +1 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
id: caching-artifacts-060
|
|
2
|
+
title: 'actions/cache Primary key Uses Undocumented Prefix Matching — Stale Cache Restored on Partial Hit'
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- actions/cache
|
|
8
|
+
- key
|
|
9
|
+
- prefix-match
|
|
10
|
+
- stale-cache
|
|
11
|
+
- cache-hit
|
|
12
|
+
- restore-keys
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'Cache restored from key:'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'cache-hit.*true'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
- regex: 'uses:\s*actions/cache@v[34]'
|
|
19
|
+
flags: 'i'
|
|
20
|
+
error_messages:
|
|
21
|
+
- "Cache restored from key: npm-cache-linux"
|
|
22
|
+
- "cache-hit: true"
|
|
23
|
+
- "Restored cache from key"
|
|
24
|
+
root_cause: |
|
|
25
|
+
GitHub Actions documentation states that `actions/cache`'s primary `key:`
|
|
26
|
+
parameter requires an **exact match** to trigger a cache hit. In practice,
|
|
27
|
+
the cache service backend uses **prefix matching** for primary keys, identical
|
|
28
|
+
to how `restore-keys:` works. This undocumented behavior was confirmed by the
|
|
29
|
+
GitHub Actions team in actions/cache#1433:
|
|
30
|
+
|
|
31
|
+
> "I've checked the internal service implementation and it appears that our
|
|
32
|
+
> documentation is incorrect. `key` uses prefix-matching similar to how
|
|
33
|
+
> `restore-keys` works."
|
|
34
|
+
|
|
35
|
+
**What this means in practice:**
|
|
36
|
+
- If an existing cached entry has key `npm-cache-linux` and the current run
|
|
37
|
+
uses key `npm-cache-linux-abc123`, the primary `key` lookup returns a hit on
|
|
38
|
+
`npm-cache-linux` (the existing key is a prefix of the requested key).
|
|
39
|
+
- The cache is restored silently with `cache-hit: true`, even though the
|
|
40
|
+
actual content may be outdated or from a completely different run.
|
|
41
|
+
- Downstream jobs that check `cache-hit == 'true'` to skip install steps will
|
|
42
|
+
skip them, causing builds to use stale dependencies.
|
|
43
|
+
|
|
44
|
+
This is especially problematic when:
|
|
45
|
+
1. A broad, short key (e.g., `linux-npm`) was saved long ago and a new, more
|
|
46
|
+
specific key (e.g., `linux-npm-${{ hashFiles('package-lock.json') }}`)
|
|
47
|
+
happens to have the old key as a prefix.
|
|
48
|
+
2. CI changes add a version suffix to cache keys, but old caches from before
|
|
49
|
+
the change are still present and match as prefixes.
|
|
50
|
+
fix: |
|
|
51
|
+
**Option 1 (recommended): Use unique, non-colliding key prefixes**
|
|
52
|
+
Design your cache key so that no broader key from a previous run can be a prefix
|
|
53
|
+
of your current key. Include the hash of lock files or use a version token that
|
|
54
|
+
changes with major dependency updates.
|
|
55
|
+
|
|
56
|
+
**Option 2: Rotate keys by appending a version token**
|
|
57
|
+
Prefix with a cache version that you bump whenever you want to guarantee a cold
|
|
58
|
+
start, preventing old keys from matching.
|
|
59
|
+
|
|
60
|
+
**Option 3: Use `restore-keys:` intentionally**
|
|
61
|
+
If you want prefix fallback, make it explicit with `restore-keys:` and verify
|
|
62
|
+
what you're getting. Check `cache-hit` output and run install even on partial
|
|
63
|
+
hits when correctness matters.
|
|
64
|
+
|
|
65
|
+
**Option 4: Delete stale caches via API**
|
|
66
|
+
Use `gh extension install actions/gh-actions-cache` or the REST API to delete
|
|
67
|
+
old broad-key caches that would match as prefixes, preventing unexpected hits.
|
|
68
|
+
fix_code:
|
|
69
|
+
- language: yaml
|
|
70
|
+
label: 'Use versioned prefix to prevent stale prefix matches'
|
|
71
|
+
code: |
|
|
72
|
+
- uses: actions/cache@v4
|
|
73
|
+
with:
|
|
74
|
+
path: ~/.npm
|
|
75
|
+
# v2 prefix ensures old "v1" caches never match as prefixes
|
|
76
|
+
key: v2-npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
|
77
|
+
restore-keys: |
|
|
78
|
+
v2-npm-${{ runner.os }}-
|
|
79
|
+
- language: yaml
|
|
80
|
+
label: 'Re-install deps on partial cache hit to avoid stale cache'
|
|
81
|
+
code: |
|
|
82
|
+
- uses: actions/cache@v4
|
|
83
|
+
id: npm-cache
|
|
84
|
+
with:
|
|
85
|
+
path: ~/.npm
|
|
86
|
+
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
|
87
|
+
restore-keys: npm-${{ runner.os }}-
|
|
88
|
+
|
|
89
|
+
- name: Install dependencies
|
|
90
|
+
# Always install when not an exact hit to avoid stale partial cache
|
|
91
|
+
if: steps.npm-cache.outputs.cache-hit != 'true'
|
|
92
|
+
run: npm ci
|
|
93
|
+
prevention:
|
|
94
|
+
- 'Always include a lockfile hash in cache keys: `key: npm-${{ runner.os }}-${{ hashFiles(''**/package-lock.json'') }}`'
|
|
95
|
+
- 'Prefix cache keys with a version token (e.g., `v1-`, `v2-`) so you can easily invalidate all old caches by bumping the version.'
|
|
96
|
+
- 'Do not rely solely on `cache-hit == true` to skip install steps; consider re-running `npm install --prefer-offline` even on hits to handle partial prefix matches.'
|
|
97
|
+
- 'Periodically audit and delete stale cache entries via the GitHub REST API or `gh actions-cache` extension.'
|
|
98
|
+
docs:
|
|
99
|
+
- url: 'https://github.com/actions/cache/issues/1433'
|
|
100
|
+
label: 'actions/cache#1433: Cache key uses prefix matching — documentation is incorrect'
|
|
101
|
+
- url: 'https://github.com/actions/cache/issues/1385'
|
|
102
|
+
label: 'actions/cache#1385: Unexpected cache hit from prefix match'
|
|
103
|
+
- url: 'https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows'
|
|
104
|
+
label: 'GitHub Docs: Caching dependencies to speed up workflows'
|
|
@@ -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/concurrency-timing/batch-workflow-dispatch-static-concurrency-silent-cancel.yml
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
id: concurrency-timing-049
|
|
2
|
+
title: 'Batch workflow_dispatch Runs Silently Cancel Each Other via Static Concurrency Group'
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- workflow-dispatch
|
|
8
|
+
- batch
|
|
9
|
+
- cancel
|
|
10
|
+
- static-group
|
|
11
|
+
- lost-run
|
|
12
|
+
- dispatch-api
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'This run was cancelled'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'concurrency:\s*\n\s*group:\s*[''"]?\$\{\{\s*github\.workflow\s*\}\}'
|
|
17
|
+
flags: 'im'
|
|
18
|
+
- regex: 'workflow_dispatch.*concurrency|concurrency.*workflow_dispatch'
|
|
19
|
+
flags: 'ims'
|
|
20
|
+
error_messages:
|
|
21
|
+
- "This run was cancelled."
|
|
22
|
+
- "Run has been cancelled because it is in a concurrency group with a newer run."
|
|
23
|
+
- "Canceling since a more recent run was started"
|
|
24
|
+
root_cause: |
|
|
25
|
+
When a workflow uses a **static concurrency group** — one that does not
|
|
26
|
+
incorporate any per-run unique value like `github.run_id` or a user-supplied
|
|
27
|
+
input — and multiple `workflow_dispatch` runs are triggered in rapid succession
|
|
28
|
+
(e.g., from a batch API script, a label webhook fan-out, or automation), GitHub
|
|
29
|
+
Actions cancels all but the last run.
|
|
30
|
+
|
|
31
|
+
GitHub Actions enforces a limit of **1 running + 1 pending** run per concurrency
|
|
32
|
+
group. Each new dispatch replaces the existing pending run:
|
|
33
|
+
|
|
34
|
+
1. Run A starts → becomes "running"
|
|
35
|
+
2. Run B dispatched → becomes "pending" (waits for A)
|
|
36
|
+
3. Run C dispatched → **cancels Run B** (B was pending), C becomes "pending"
|
|
37
|
+
4. Run D dispatched → **cancels Run C**, D becomes "pending"
|
|
38
|
+
5. Only Run D eventually runs after Run A finishes.
|
|
39
|
+
|
|
40
|
+
This produces silent data loss when each dispatch carries different `inputs:`
|
|
41
|
+
(e.g., different artifact IDs, tenant IDs, or tags to process). All runs except
|
|
42
|
+
the last are silently discarded with "This run was cancelled."
|
|
43
|
+
|
|
44
|
+
Commonly triggered by:
|
|
45
|
+
- A webhook handler that dispatches one workflow run per event, with bursts of
|
|
46
|
+
events arriving simultaneously.
|
|
47
|
+
- Automation scripts that loop over a list and call `gh workflow run` for each item.
|
|
48
|
+
- A cron job that fans out to per-repo dispatches sharing the same workflow name.
|
|
49
|
+
|
|
50
|
+
Note: this is distinct from `concurrency-timing-040` (push + workflow_dispatch
|
|
51
|
+
sharing a group) — this issue occurs with ONLY workflow_dispatch runs sharing a
|
|
52
|
+
static group with no event-name differentiation.
|
|
53
|
+
fix: |
|
|
54
|
+
**Option 1 (recommended): Include a unique input value in the concurrency group**
|
|
55
|
+
If each dispatch has a unique identifier as an input (e.g., a tenant ID, artifact
|
|
56
|
+
ID, or item key), include it in the concurrency group so different dispatches
|
|
57
|
+
get independent groups.
|
|
58
|
+
|
|
59
|
+
**Option 2: Include github.run_id in the concurrency group**
|
|
60
|
+
Using `github.run_id` makes every run its own concurrency group — effectively
|
|
61
|
+
disabling concurrency entirely. Use only if all runs are important and must
|
|
62
|
+
complete.
|
|
63
|
+
|
|
64
|
+
**Option 3: Use cancel-in-progress: false (no-queue mode)**
|
|
65
|
+
With `cancel-in-progress: false`, new pending runs do not cancel existing pending
|
|
66
|
+
ones. The 1-running + 1-pending limit still applies, but the *last* queued run
|
|
67
|
+
(not the most recent dispatch) survives. This still means high-burst dispatches
|
|
68
|
+
lose most runs.
|
|
69
|
+
|
|
70
|
+
**Option 4: Use a queue-based architecture**
|
|
71
|
+
For high-volume fan-out, avoid workflow_dispatch entirely. Instead, push items to
|
|
72
|
+
a queue (SQS, GitHub Issues, database) and have a single polling workflow process
|
|
73
|
+
items from the queue sequentially or in bounded parallel batches.
|
|
74
|
+
fix_code:
|
|
75
|
+
- language: yaml
|
|
76
|
+
label: 'Include unique input in concurrency group to isolate per-dispatch runs'
|
|
77
|
+
code: |
|
|
78
|
+
on:
|
|
79
|
+
workflow_dispatch:
|
|
80
|
+
inputs:
|
|
81
|
+
item_id:
|
|
82
|
+
description: 'Unique identifier for this dispatch'
|
|
83
|
+
required: true
|
|
84
|
+
type: string
|
|
85
|
+
|
|
86
|
+
concurrency:
|
|
87
|
+
# Each unique item_id gets its own concurrency slot — no cross-dispatch cancellation
|
|
88
|
+
group: ${{ github.workflow }}-${{ inputs.item_id }}
|
|
89
|
+
cancel-in-progress: true
|
|
90
|
+
- language: yaml
|
|
91
|
+
label: 'Use github.run_id to guarantee all dispatches run independently'
|
|
92
|
+
code: |
|
|
93
|
+
on:
|
|
94
|
+
workflow_dispatch:
|
|
95
|
+
|
|
96
|
+
concurrency:
|
|
97
|
+
# Every run is its own group — effectively disables concurrency queueing
|
|
98
|
+
group: ${{ github.workflow }}-${{ github.run_id }}
|
|
99
|
+
cancel-in-progress: false
|
|
100
|
+
- language: yaml
|
|
101
|
+
label: 'Fan-out batch dispatch via matrix instead of separate workflow runs'
|
|
102
|
+
code: |
|
|
103
|
+
# Instead of dispatching N separate workflow runs (which cancel each other),
|
|
104
|
+
# dispatch ONE run with a matrix input to process all items in parallel.
|
|
105
|
+
on:
|
|
106
|
+
workflow_dispatch:
|
|
107
|
+
inputs:
|
|
108
|
+
item_ids:
|
|
109
|
+
description: 'JSON array of item IDs to process'
|
|
110
|
+
required: true
|
|
111
|
+
type: string
|
|
112
|
+
|
|
113
|
+
concurrency:
|
|
114
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
115
|
+
cancel-in-progress: false
|
|
116
|
+
|
|
117
|
+
jobs:
|
|
118
|
+
process-items:
|
|
119
|
+
strategy:
|
|
120
|
+
matrix:
|
|
121
|
+
item_id: ${{ fromJSON(inputs.item_ids) }}
|
|
122
|
+
runs-on: ubuntu-latest
|
|
123
|
+
steps:
|
|
124
|
+
- name: Process item
|
|
125
|
+
run: echo "Processing ${{ matrix.item_id }}"
|
|
126
|
+
prevention:
|
|
127
|
+
- 'Never use a static concurrency group (e.g., `group: ${{ github.workflow }}`) for workflows dispatched in batch — include a unique per-dispatch key.'
|
|
128
|
+
- 'When dispatching multiple runs from a script, add a unique ID as an input and include it in the concurrency group.'
|
|
129
|
+
- 'For high-volume fan-out workloads, use a single matrix job instead of N separate workflow_dispatch runs.'
|
|
130
|
+
- 'Monitor workflow cancellation counts in the Actions UI — unexpected "This run was cancelled" on dispatch events signals a concurrency group collision.'
|
|
131
|
+
docs:
|
|
132
|
+
- url: 'https://github.com/github/gh-aw/issues/19467'
|
|
133
|
+
label: 'github/gh-aw#19467: Batch workflow_dispatch runs cancel each other via static concurrency group'
|
|
134
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-concurrency'
|
|
135
|
+
label: 'GitHub Docs: Using concurrency — 1 running + 1 pending limit per group'
|
|
136
|
+
- url: 'https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/manually-running-a-workflow'
|
|
137
|
+
label: 'GitHub Docs: Manually running a workflow'
|
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"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
id: runner-environment-174
|
|
2
|
+
title: 'Sparse Checkout Config Persists Across Workflow Runs on Self-Hosted Runners'
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- sparse-checkout
|
|
7
|
+
- self-hosted
|
|
8
|
+
- git-config
|
|
9
|
+
- workspace-reuse
|
|
10
|
+
- checkout
|
|
11
|
+
- non-ephemeral
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'core\.sparseCheckout\s*=\s*true'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'sparse.checkout.*persist|persist.*sparse.checkout'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'git sparse-checkout disable'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- "Run actions/checkout@v4"
|
|
21
|
+
- "Checkout was successful but expected files are missing from working directory"
|
|
22
|
+
- "sparse-checkout: /run/git/config: sparseCheckout = true"
|
|
23
|
+
root_cause: |
|
|
24
|
+
On non-ephemeral self-hosted runners, the workspace directory (e.g.
|
|
25
|
+
`/home/runner/work/my-repo/my-repo`) is reused across multiple workflow runs
|
|
26
|
+
on the same runner machine. When `actions/checkout` runs with `sparse-checkout:`
|
|
27
|
+
options, it sets `core.sparseCheckout = true` in the repository's `.git/config`
|
|
28
|
+
file.
|
|
29
|
+
|
|
30
|
+
In older versions of `actions/checkout` (before the fix in commit aadec89,
|
|
31
|
+
merged ~2025), the `disableSparseCheckout()` method reset `core.sparseCheckout`
|
|
32
|
+
only in `.git/config.worktree` (used in git worktrees), but NOT in the main
|
|
33
|
+
`.git/config`. As a result:
|
|
34
|
+
|
|
35
|
+
- **Workflow run N** uses `sparse-checkout:` → sets `core.sparseCheckout = true`
|
|
36
|
+
in `.git/config`.
|
|
37
|
+
- **Workflow run N+1** on the same runner does NOT use `sparse-checkout:`, but
|
|
38
|
+
inherits the stale `core.sparseCheckout = true` setting from the reused workspace.
|
|
39
|
+
- `actions/checkout` reports success, but the working directory contains only
|
|
40
|
+
the files that matched the previous run's sparse-checkout patterns. All other
|
|
41
|
+
files silently absent.
|
|
42
|
+
|
|
43
|
+
This is distinct from the same-job sticky cone mode issue (sf-008), which covers
|
|
44
|
+
two `actions/checkout` calls within a single job sharing sparse state. This issue
|
|
45
|
+
manifests across entirely separate workflow runs on a shared workspace.
|
|
46
|
+
|
|
47
|
+
Confirmed in actions/checkout#2249 (published Aug 2025) and actions/checkout#1992.
|
|
48
|
+
fix: |
|
|
49
|
+
**Option 1 (recommended): Upgrade to actions/checkout v4.2.0+**
|
|
50
|
+
The bug was fixed in commit aadec89. Pinning to a patched release ensures
|
|
51
|
+
`disableSparseCheckout()` explicitly sets `core.sparseCheckout = false` in
|
|
52
|
+
`.git/config` when no `sparse-checkout:` input is provided.
|
|
53
|
+
|
|
54
|
+
**Option 2: Add an explicit `git sparse-checkout disable` step**
|
|
55
|
+
Add this step at the beginning of workflows that do NOT use sparse-checkout
|
|
56
|
+
on a runner that may previously have run sparse-checkout workflows:
|
|
57
|
+
|
|
58
|
+
**Option 3: Use ephemeral self-hosted runners**
|
|
59
|
+
Configure runners so each job gets a fresh workspace (e.g., ARC ephemeral mode,
|
|
60
|
+
EC2 runners with fresh AMIs). Ephemeral runners eliminate all cross-run workspace
|
|
61
|
+
contamination.
|
|
62
|
+
|
|
63
|
+
**Option 4: Configure runner to clean workspace between runs**
|
|
64
|
+
Set `ACTIONS_RUNNER_CLEAN_WORKSPACE=true` in the runner environment (where
|
|
65
|
+
supported) to force workspace cleanup between job runs.
|
|
66
|
+
fix_code:
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: 'Upgrade to patched actions/checkout version'
|
|
69
|
+
code: |
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/checkout@v4 # Use v4.2.0+ for the sparse-checkout fix
|
|
72
|
+
# No sparse-checkout options — will correctly restore full tree
|
|
73
|
+
- language: yaml
|
|
74
|
+
label: 'Explicit sparse-checkout reset before checkout on shared runners'
|
|
75
|
+
code: |
|
|
76
|
+
steps:
|
|
77
|
+
- name: Reset sparse-checkout (safety guard for shared self-hosted runners)
|
|
78
|
+
shell: bash
|
|
79
|
+
run: |
|
|
80
|
+
if git -C "${{ github.workspace }}" rev-parse --git-dir > /dev/null 2>&1; then
|
|
81
|
+
git -C "${{ github.workspace }}" sparse-checkout disable 2>/dev/null || true
|
|
82
|
+
git -C "${{ github.workspace }}" config --unset core.sparseCheckout 2>/dev/null || true
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
- uses: actions/checkout@v4
|
|
86
|
+
prevention:
|
|
87
|
+
- 'On self-hosted runners, always pin `actions/checkout` to v4.2.0+ which fixes the sparse-checkout persistence bug.'
|
|
88
|
+
- 'Add a `git sparse-checkout disable` guard step at the start of any workflow that runs on a non-ephemeral self-hosted runner.'
|
|
89
|
+
- 'Use ephemeral runners (fresh environment per job) to eliminate all cross-run workspace contamination.'
|
|
90
|
+
- 'Do not mix sparse-checkout and full-checkout workflows on the same non-ephemeral runner without resetting `.git/config` between runs.'
|
|
91
|
+
docs:
|
|
92
|
+
- url: 'https://github.com/actions/checkout/issues/2249'
|
|
93
|
+
label: 'actions/checkout#2249: Sparse-checkout configuration persists across workflows on self-hosted runners'
|
|
94
|
+
- url: 'https://github.com/actions/checkout/issues/1992'
|
|
95
|
+
label: 'actions/checkout#1992: Sparscheckout breaks cached repository on self-hosted runners'
|
|
96
|
+
- url: 'https://github.com/actions/checkout/commit/aadec899646c8e0f34c52d9219c2faac36626b55'
|
|
97
|
+
label: 'Fix commit aadec89: Explicitly disable sparseCheckout in .git/config when not using sparse-checkout'
|
|
98
|
+
- url: 'https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners'
|
|
99
|
+
label: 'GitHub Docs: About GitHub-hosted runners (ephemeral vs self-hosted)'
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
id: yaml-syntax-066
|
|
2
|
+
title: 'hashFiles() Newline Delimiter in Single String Returns Empty — Use Multiple Arguments'
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- hashfiles
|
|
7
|
+
- expression
|
|
8
|
+
- cache-key
|
|
9
|
+
- empty-string
|
|
10
|
+
- multifile
|
|
11
|
+
- glob
|
|
12
|
+
- newline-delimiter
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'hashFiles\([''"].*\\n.*[''"]'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'hashFiles\s*\(\s*[''"][^''",]+\\n[^''",]+[''"]'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
error_messages:
|
|
19
|
+
- "${{ hashFiles('src/**\npackage.json') }}"
|
|
20
|
+
- "${{ hashFiles('**/*.lock\n**/*.toml') }}"
|
|
21
|
+
- "hashFiles result: ''"
|
|
22
|
+
root_cause: |
|
|
23
|
+
The `@actions/glob` internal implementation documents that file patterns can be
|
|
24
|
+
separated by newline (`\n`) characters when passed as a single string. This causes
|
|
25
|
+
developers to write `hashFiles('file.ts\nfile.tsx')` expecting it to hash both
|
|
26
|
+
files.
|
|
27
|
+
|
|
28
|
+
However, inside a GitHub Actions expression (`${{ ... }}`), string literals do
|
|
29
|
+
NOT interpret escape sequences. The `\n` in `'file.ts\nfile.tsx'` is a literal
|
|
30
|
+
two-character sequence: backslash + "n". The glob function then looks for a file
|
|
31
|
+
literally named `file.ts\nfile.tsx` — which does not exist — and returns an empty
|
|
32
|
+
string `''`.
|
|
33
|
+
|
|
34
|
+
**Result:**
|
|
35
|
+
- `cache-key: ${{ hashFiles('package.json\nyarn.lock') }}` → cache key ends with
|
|
36
|
+
an empty string, causing ALL runs to collide on the same constant prefix key.
|
|
37
|
+
- `if: hashFiles('src/**\ntest/**') != ''` → always false (no hash returned), so
|
|
38
|
+
cache/condition logic silently skips.
|
|
39
|
+
- No error is raised; the expression just evaluates to `''` silently.
|
|
40
|
+
|
|
41
|
+
Confirmed in actions/runner#3467 and actions/runner#4049.
|
|
42
|
+
|
|
43
|
+
Note: `hashFiles()` DOES accept multiple comma-separated string arguments, and
|
|
44
|
+
each argument is a full glob pattern. The variadic call form works correctly.
|
|
45
|
+
fix: |
|
|
46
|
+
Use multiple comma-separated arguments to `hashFiles()` instead of a
|
|
47
|
+
newline-delimited single string.
|
|
48
|
+
|
|
49
|
+
`hashFiles('pattern1', 'pattern2', ...)` correctly hashes all files matching
|
|
50
|
+
any of the provided glob patterns.
|
|
51
|
+
|
|
52
|
+
If you are building a dynamic list of patterns from a context value or step
|
|
53
|
+
output, write the patterns to a file and hash that file, or hash each pattern
|
|
54
|
+
in separate expressions and combine.
|
|
55
|
+
fix_code:
|
|
56
|
+
- language: yaml
|
|
57
|
+
label: 'Use multiple arguments (correct) instead of newline-delimited string (broken)'
|
|
58
|
+
code: |
|
|
59
|
+
# BROKEN — \n is a literal backslash-n, not a newline; returns empty string
|
|
60
|
+
# key: npm-${{ hashFiles('package.json\npackage-lock.json') }}
|
|
61
|
+
|
|
62
|
+
# CORRECT — comma-separated arguments, hashes both files
|
|
63
|
+
- uses: actions/cache@v4
|
|
64
|
+
with:
|
|
65
|
+
path: ~/.npm
|
|
66
|
+
key: npm-${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json') }}
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: 'Multiple lock files hashed with variadic hashFiles()'
|
|
69
|
+
code: |
|
|
70
|
+
- uses: actions/cache@v4
|
|
71
|
+
with:
|
|
72
|
+
path: |
|
|
73
|
+
~/.npm
|
|
74
|
+
~/.cache/pip
|
|
75
|
+
key: multi-${{ runner.os }}-${{ hashFiles('**/package-lock.json', '**/requirements*.txt', '**/Pipfile.lock') }}
|
|
76
|
+
restore-keys: |
|
|
77
|
+
multi-${{ runner.os }}-
|
|
78
|
+
- language: yaml
|
|
79
|
+
label: 'Conditional step using hashFiles() with multiple arguments'
|
|
80
|
+
code: |
|
|
81
|
+
# BROKEN — always evaluates to '' because \n is not a newline escape
|
|
82
|
+
# if: ${{ hashFiles('src/**\ntest/**') != '' }}
|
|
83
|
+
|
|
84
|
+
# CORRECT
|
|
85
|
+
- name: Check if source files changed
|
|
86
|
+
if: ${{ hashFiles('src/**', 'test/**') != '' }}
|
|
87
|
+
run: echo "Source files found"
|
|
88
|
+
prevention:
|
|
89
|
+
- 'Never use `\n` as a pattern delimiter inside a `hashFiles()` call in a workflow expression — it is a literal backslash-n, not a newline.'
|
|
90
|
+
- 'Use multiple comma-separated arguments: `hashFiles(''pattern1'', ''pattern2'')` to hash files from multiple glob patterns.'
|
|
91
|
+
- 'When a `hashFiles()` result is used as a cache key suffix, validate it is non-empty by running the workflow once and checking the logged cache key.'
|
|
92
|
+
- 'If you suspect an empty hash, add a debug step: `run: echo "hash=${{ hashFiles(''**/*.lock'') }}"` to verify the value before using it in a key.'
|
|
93
|
+
docs:
|
|
94
|
+
- url: 'https://github.com/actions/runner/issues/3467'
|
|
95
|
+
label: 'actions/runner#3467: hashFiles does not follow docs — newline delimiter in string returns empty'
|
|
96
|
+
- url: 'https://github.com/actions/runner/issues/4049'
|
|
97
|
+
label: 'actions/runner#4049: hashFiles newline pattern delimiter not interpreted in expressions'
|
|
98
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#hashfiles'
|
|
99
|
+
label: 'GitHub Docs: hashFiles function — variadic arguments'
|
package/package.json
CHANGED