@htekdev/actions-debugger 1.0.116 → 1.0.118
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-key-windows-path-separator-never-matches.yml +107 -0
- package/errors/caching-artifacts/caching-artifacts-069.yml +133 -0
- package/errors/concurrency-timing/rerun-failed-jobs-bypasses-concurrency-group.yml +89 -0
- package/errors/concurrency-timing/workflow-run-head-branch-null-schedule-dispatch-concurrency.yml +135 -0
- package/errors/known-unsolved/empty-matrix-fromjson-workflow-failure-no-conditional-skip.yml +108 -0
- package/errors/known-unsolved/node-action-post-step-wrong-inputs-nested-composite.yml +133 -0
- package/errors/known-unsolved/ubuntu-24-04-arm64-missing-binder-ashmem-kernel-modules.yml +149 -0
- package/errors/permissions-auth/permissions-auth-069.yml +161 -0
- package/errors/runner-environment/arc-autoscalinglistener-ephemeralrunnerset-stale-after-upgrade.yml +134 -0
- package/errors/runner-environment/broker-server-socket-exception-nat-timeout-linux.yml +114 -0
- package/errors/runner-environment/checkout-v603-hash-algorithm-api-rate-limiting.yml +100 -0
- package/errors/runner-environment/macos-self-hosted-listener-aad-ghost-busy-stall.yml +126 -0
- package/errors/runner-environment/runner-environment-210.yml +105 -0
- package/errors/runner-environment/runner-environment-213.yml +142 -0
- package/errors/runner-environment/setup-node-ebaddevengines-devengines-packagemanager.yml +103 -0
- package/errors/runner-environment/ubuntu-24-man-db-dpkg-trigger-apt-install-stall.yml +94 -0
- package/errors/runner-environment/ubuntu-26-04-missing-preinstalled-tools.yml +178 -0
- package/errors/runner-environment/upload-artifact-v6-proxy-headers-leak-strict-proxy-fail.yml +101 -0
- package/errors/silent-failures/silent-failures-108.yml +108 -0
- package/errors/triggers/pull-request-labeled-fires-all-labels-no-name-filter.yml +110 -0
- package/errors/yaml-syntax/duplicate-step-id-within-job-scope-validation-error.yml +130 -0
- package/package.json +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
id: caching-artifacts-068
|
|
2
|
+
title: '`actions/cache` key containing Windows backslash path separators never matches on restore — cache miss every run'
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- windows
|
|
8
|
+
- path-separator
|
|
9
|
+
- backslash
|
|
10
|
+
- cache-miss
|
|
11
|
+
- cross-platform
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'Cache not found for input keys'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'cache.*miss.*windows|windows.*cache.*miss'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- 'Cache not found for input keys:'
|
|
19
|
+
- 'cache hit for key: false'
|
|
20
|
+
root_cause: |
|
|
21
|
+
GitHub Actions cache keys are plain strings matched byte-for-byte. On Windows
|
|
22
|
+
runners, several common patterns produce cache keys containing backslashes (`\`):
|
|
23
|
+
|
|
24
|
+
1. Embedding ${{ runner.temp }} or ${{ github.workspace }} directly in the key:
|
|
25
|
+
key: ${{ runner.os }}-${{ runner.temp }}-${{ hashFiles('**/package-lock.json') }}
|
|
26
|
+
On Windows, runner.temp resolves to `D:\a\_temp` producing a key with `\`.
|
|
27
|
+
|
|
28
|
+
2. Using hashFiles() with backslash glob patterns:
|
|
29
|
+
key: ${{ hashFiles('**\node_modules\**') }}
|
|
30
|
+
hashFiles() on Windows sometimes receives backslash-escaped paths in its
|
|
31
|
+
argument, encoding them into the resulting hash string.
|
|
32
|
+
|
|
33
|
+
3. String concatenation with Windows path variables set in earlier steps:
|
|
34
|
+
TOOL_PATH: C:\hostedtoolcache\node\18\x64
|
|
35
|
+
When $TOOL_PATH is embedded in the cache key, the `\` chars are literal.
|
|
36
|
+
|
|
37
|
+
The cache service stores and retrieves keys as exact string matches. A cache
|
|
38
|
+
saved with key `npm-D:\a\_temp-abc123` is never found by a lookup for
|
|
39
|
+
`npm-D:/a/_temp-abc123` or by a lookup on a different run where the runner.temp
|
|
40
|
+
path differs.
|
|
41
|
+
|
|
42
|
+
On Linux/macOS runners, paths always use `/` so this issue is Windows-specific.
|
|
43
|
+
Cross-platform matrix workflows are especially affected: the Linux cache is found
|
|
44
|
+
on restore; the Windows cache is always missed.
|
|
45
|
+
fix: |
|
|
46
|
+
Normalize all path separators to forward slashes before including in cache keys.
|
|
47
|
+
|
|
48
|
+
Option 1 — Avoid runner.temp and github.workspace in cache keys entirely.
|
|
49
|
+
Use runner.os and a fixed path pattern instead:
|
|
50
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
51
|
+
|
|
52
|
+
Option 2 — If you must embed a path, normalize in a prior step:
|
|
53
|
+
- name: Set normalized cache path
|
|
54
|
+
shell: bash
|
|
55
|
+
run: echo "CACHE_PATH=$(echo '${{ runner.temp }}' | tr '\\' '/')" >> $GITHUB_ENV
|
|
56
|
+
Then:
|
|
57
|
+
key: ${{ runner.os }}-${{ env.CACHE_PATH }}-${{ hashFiles(...) }}
|
|
58
|
+
|
|
59
|
+
Option 3 — Always use forward-slash glob patterns in hashFiles():
|
|
60
|
+
key: ${{ hashFiles('**/package-lock.json') }} # correct
|
|
61
|
+
# NOT: ${{ hashFiles('**\package-lock.json') }} # Windows-style — avoid
|
|
62
|
+
|
|
63
|
+
Option 4 — Use a bash shell step to compute the key and store in GITHUB_ENV,
|
|
64
|
+
ensuring all path manipulation happens in a POSIX shell that normalizes separators.
|
|
65
|
+
fix_code:
|
|
66
|
+
- language: yaml
|
|
67
|
+
label: 'Bad: runner.temp in cache key produces backslashes on Windows'
|
|
68
|
+
code: |
|
|
69
|
+
# ❌ runner.temp on Windows = "D:\a\_temp" — backslashes in key → never matches
|
|
70
|
+
- uses: actions/cache@v4
|
|
71
|
+
with:
|
|
72
|
+
path: ${{ runner.temp }}/cache
|
|
73
|
+
key: ${{ runner.os }}-${{ runner.temp }}-${{ hashFiles('**/lock.json') }}
|
|
74
|
+
- language: yaml
|
|
75
|
+
label: 'Good: use runner.os and a fixed path — no path variables in key'
|
|
76
|
+
code: |
|
|
77
|
+
# ✅ No Windows path separators in key
|
|
78
|
+
- uses: actions/cache@v4
|
|
79
|
+
with:
|
|
80
|
+
path: ~/.npm
|
|
81
|
+
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
|
82
|
+
- language: yaml
|
|
83
|
+
label: 'Good: normalize path separators with bash tr before embedding in key'
|
|
84
|
+
code: |
|
|
85
|
+
- name: Normalize cache path
|
|
86
|
+
shell: bash
|
|
87
|
+
run: |
|
|
88
|
+
NORM_PATH=$(echo '${{ runner.tool_cache }}' | tr '\\' '/')
|
|
89
|
+
echo "NORM_TOOL_CACHE=$NORM_PATH" >> $GITHUB_ENV
|
|
90
|
+
|
|
91
|
+
- uses: actions/cache@v4
|
|
92
|
+
with:
|
|
93
|
+
path: ${{ runner.tool_cache }}
|
|
94
|
+
key: ${{ runner.os }}-tools-${{ env.NORM_TOOL_CACHE }}-${{ hashFiles('**/tool-versions') }}
|
|
95
|
+
prevention:
|
|
96
|
+
- 'Never include runner.temp, runner.tool_cache, or github.workspace in cache keys — these resolve to OS-specific paths with backslashes on Windows'
|
|
97
|
+
- 'Always use runner.os (e.g., "Windows") as the OS discriminator in cross-platform cache keys, not path-based variables'
|
|
98
|
+
- 'Use forward-slash glob patterns in hashFiles() expressions on all platforms'
|
|
99
|
+
- 'When path variables must appear in a cache key, normalize separators to forward slashes in a bash step first and use the env variable'
|
|
100
|
+
- 'Test cache hit rate explicitly in CI by checking the cache-hit output of the restore step on Windows runners'
|
|
101
|
+
docs:
|
|
102
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows'
|
|
103
|
+
label: 'GitHub Docs: Caching dependencies to speed up workflows'
|
|
104
|
+
- url: 'https://github.com/actions/cache/issues/671'
|
|
105
|
+
label: 'actions/cache#671: Cache key with Windows path separators causes cache miss'
|
|
106
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables'
|
|
107
|
+
label: 'GitHub Docs: Default environment variables (runner.temp, etc.)'
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
id: caching-artifacts-069
|
|
2
|
+
title: 'actions/cache save@v5 "Unable to Reserve Cache" When Key Already Exists for Same Branch'
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: warning
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- cache-save
|
|
8
|
+
- v5
|
|
9
|
+
- immutable
|
|
10
|
+
- key-exists
|
|
11
|
+
- save-only
|
|
12
|
+
- reserve-cache
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'Unable to reserve cache with key .+, another job may be creating this cache'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'More Details: Key already reserved duplicate'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
- regex: 'Failed to save: Unable to reserve cache'
|
|
19
|
+
flags: 'i'
|
|
20
|
+
error_messages:
|
|
21
|
+
- 'Failed to save: Unable to reserve cache with key my-cache-key, another job may be creating this cache.'
|
|
22
|
+
- 'Warning: Cache save failed.'
|
|
23
|
+
- 'More Details: Key already reserved duplicate'
|
|
24
|
+
root_cause: |
|
|
25
|
+
GitHub's cache service is write-once per key+branch combination. Once a cache entry
|
|
26
|
+
has been successfully written for a given key on a given branch/ref, that key cannot
|
|
27
|
+
be overwritten. The slot is permanently "reserved" by the first writer.
|
|
28
|
+
|
|
29
|
+
When using `actions/cache/save@v5` (the standalone save action, separate from the
|
|
30
|
+
combined restore+save `actions/cache@v5`), the action does NOT automatically skip the
|
|
31
|
+
save if an exact cache key match already exists. It attempts to write the cache, the
|
|
32
|
+
service returns "Key already reserved duplicate" (v5 API uses proper JSON errors
|
|
33
|
+
unlike v3/v4 which returned HTML DOCTYPE responses), and the action emits:
|
|
34
|
+
"Failed to save: Unable to reserve cache with key X, another job may be creating this cache."
|
|
35
|
+
"Warning: Cache save failed."
|
|
36
|
+
|
|
37
|
+
This differs from the combined `actions/cache@v5` which checks the CACHE_HIT output
|
|
38
|
+
from its restore step and automatically skips the save on an exact key match. When
|
|
39
|
+
using the split restore/save pattern (actions/cache/restore + actions/cache/save),
|
|
40
|
+
the save action has no restore step context, so it always attempts to write —
|
|
41
|
+
triggering this warning when the key already exists from a previous workflow run.
|
|
42
|
+
|
|
43
|
+
Common trigger scenarios:
|
|
44
|
+
- Using actions/cache/save@v5 in an always() post step to save a cache built during
|
|
45
|
+
the job, on the second+ run of the same workflow on the same branch
|
|
46
|
+
- Using @actions/cache npm package's saveCache() directly without first checking
|
|
47
|
+
whether the key was already restored (restoreCache returned an exact match)
|
|
48
|
+
- Parallel jobs on the same branch all attempting to save under the same static key
|
|
49
|
+
fix: |
|
|
50
|
+
Option 1 (recommended): Gate the save step on cache-miss.
|
|
51
|
+
Use actions/cache/restore@v5 first and only run the save step when the primary key
|
|
52
|
+
was not an exact hit:
|
|
53
|
+
|
|
54
|
+
- name: Restore cache
|
|
55
|
+
id: restore
|
|
56
|
+
uses: actions/cache/restore@v5
|
|
57
|
+
with:
|
|
58
|
+
key: ${{ env.CACHE_KEY }}
|
|
59
|
+
path: ~/.cache/my-tool
|
|
60
|
+
|
|
61
|
+
- name: Build
|
|
62
|
+
run: make build
|
|
63
|
+
|
|
64
|
+
- name: Save cache
|
|
65
|
+
if: steps.restore.outputs.cache-hit != 'true'
|
|
66
|
+
uses: actions/cache/save@v5
|
|
67
|
+
with:
|
|
68
|
+
key: ${{ env.CACHE_KEY }}
|
|
69
|
+
path: ~/.cache/my-tool
|
|
70
|
+
|
|
71
|
+
Option 2: Use the combined actions/cache@v5 which handles this automatically.
|
|
72
|
+
|
|
73
|
+
Option 3 (programmatic): If using @actions/cache npm package, check restoreCache
|
|
74
|
+
return value before calling saveCache — skip save if the return value matches the
|
|
75
|
+
primary key (exact hit).
|
|
76
|
+
fix_code:
|
|
77
|
+
- language: yaml
|
|
78
|
+
label: 'Option 1 — gate save on cache-miss from restore step'
|
|
79
|
+
code: |
|
|
80
|
+
steps:
|
|
81
|
+
- name: Restore cache
|
|
82
|
+
id: cache-restore
|
|
83
|
+
uses: actions/cache/restore@v5
|
|
84
|
+
with:
|
|
85
|
+
key: ${{ runner.os }}-build-${{ hashFiles('**/lockfiles') }}
|
|
86
|
+
restore-keys: |
|
|
87
|
+
${{ runner.os }}-build-
|
|
88
|
+
path: |
|
|
89
|
+
~/.cache/my-tool
|
|
90
|
+
node_modules
|
|
91
|
+
|
|
92
|
+
- name: Build
|
|
93
|
+
run: make all
|
|
94
|
+
|
|
95
|
+
# Only save when the primary key was not an exact hit
|
|
96
|
+
- name: Save cache
|
|
97
|
+
if: steps.cache-restore.outputs.cache-hit != 'true'
|
|
98
|
+
uses: actions/cache/save@v5
|
|
99
|
+
with:
|
|
100
|
+
key: ${{ runner.os }}-build-${{ hashFiles('**/lockfiles') }}
|
|
101
|
+
path: |
|
|
102
|
+
~/.cache/my-tool
|
|
103
|
+
node_modules
|
|
104
|
+
- language: yaml
|
|
105
|
+
label: 'Option 2 — use combined cache action (auto-skips save on exact hit)'
|
|
106
|
+
code: |
|
|
107
|
+
steps:
|
|
108
|
+
- name: Cache dependencies
|
|
109
|
+
uses: actions/cache@v5
|
|
110
|
+
with:
|
|
111
|
+
key: ${{ runner.os }}-build-${{ hashFiles('**/lockfiles') }}
|
|
112
|
+
restore-keys: |
|
|
113
|
+
${{ runner.os }}-build-
|
|
114
|
+
path: |
|
|
115
|
+
~/.cache/my-tool
|
|
116
|
+
node_modules
|
|
117
|
+
prevention:
|
|
118
|
+
- 'Prefer the combined actions/cache@v5 over split restore/save when the primary key
|
|
119
|
+
is static or content-hash-based — the combined action skips save on exact hit automatically'
|
|
120
|
+
- 'When using split restore/save, always gate the save step with
|
|
121
|
+
if: steps.<restore-id>.outputs.cache-hit != ''true'''
|
|
122
|
+
- 'Never treat "Warning: Cache save failed." as a hard error when using static cache keys
|
|
123
|
+
across repeated runs — add continue-on-error: true to the save step if the warning
|
|
124
|
+
is expected and the restored cache is already valid'
|
|
125
|
+
- 'For programmatic use via @actions/cache npm package, check the return value of
|
|
126
|
+
restoreCache() before calling saveCache() — skip save when result === primaryKey'
|
|
127
|
+
docs:
|
|
128
|
+
- url: 'https://github.com/actions/cache/issues/1707'
|
|
129
|
+
label: 'actions/cache#1707 — Unable to reserve cache (actions/cache/save@v5)'
|
|
130
|
+
- url: 'https://github.com/actions/cache/tree/main/save#readme'
|
|
131
|
+
label: 'actions/cache save action README — Case 1: reuse key as-is'
|
|
132
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows'
|
|
133
|
+
label: 'GitHub Docs — Caching dependencies to speed up workflows'
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
id: concurrency-timing-054
|
|
2
|
+
title: '"Re-run failed jobs" bypasses concurrency group — runs in parallel with a newly triggered run for the same group'
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- rerun
|
|
8
|
+
- re-run-failed-jobs
|
|
9
|
+
- parallel
|
|
10
|
+
- cancel-in-progress
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Re-run failed jobs'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'This run was automatically cancelled'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
error_messages:
|
|
17
|
+
- 'This run was automatically cancelled'
|
|
18
|
+
root_cause: |
|
|
19
|
+
When a developer clicks "Re-run failed jobs" (as opposed to "Re-run all jobs"),
|
|
20
|
+
GitHub Actions does not evaluate the workflow's concurrency group before starting
|
|
21
|
+
the rerun. The rerun begins immediately and runs in parallel with any currently
|
|
22
|
+
in-progress or newly triggered run that occupies the same concurrency group.
|
|
23
|
+
|
|
24
|
+
This is distinct from "Re-run all jobs", which DOES re-trigger the concurrency
|
|
25
|
+
check and will cancel the currently in-progress run if cancel-in-progress is true,
|
|
26
|
+
or queue behind it if cancel-in-progress is false.
|
|
27
|
+
|
|
28
|
+
Common scenario:
|
|
29
|
+
1. A push triggers run A, which starts and partially fails.
|
|
30
|
+
2. A second push triggers run B for the same branch (same concurrency group).
|
|
31
|
+
Run B is queued or cancels run A depending on cancel-in-progress setting.
|
|
32
|
+
3. Developer hits "Re-run failed jobs" on run A.
|
|
33
|
+
4. Run A's rerun starts immediately — now run A (rerun) and run B are both
|
|
34
|
+
executing concurrently, violating the intent of the concurrency group.
|
|
35
|
+
|
|
36
|
+
Side effects:
|
|
37
|
+
- Two deploys to the same environment can run simultaneously.
|
|
38
|
+
- A self-hosted runner with a single slot gets double-booked.
|
|
39
|
+
- Race conditions between two concurrent jobs writing to the same artifact.
|
|
40
|
+
|
|
41
|
+
Root cause: GitHub's "Re-run failed jobs" path was implemented as a targeted job
|
|
42
|
+
restart that bypasses workflow-level orchestration including concurrency evaluation.
|
|
43
|
+
This behavior is tracked in actions/runner#2294 (open).
|
|
44
|
+
fix: |
|
|
45
|
+
Option 1 — Use "Re-run all jobs" instead of "Re-run failed jobs" when the workflow
|
|
46
|
+
has a concurrency group. "Re-run all jobs" triggers a fresh run that participates
|
|
47
|
+
in concurrency evaluation normally.
|
|
48
|
+
|
|
49
|
+
Option 2 — Include github.run_attempt in the concurrency group key so each attempt
|
|
50
|
+
gets its own group slot, preventing cancellation loops during reruns while still
|
|
51
|
+
serializing independent triggers:
|
|
52
|
+
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.run_attempt }}
|
|
53
|
+
cancel-in-progress: false
|
|
54
|
+
|
|
55
|
+
Option 3 — For deployment workflows, use environment protection rules as the
|
|
56
|
+
serialization mechanism instead of (or in addition to) concurrency groups. A
|
|
57
|
+
pending environment review gate serializes deployments even when concurrency is
|
|
58
|
+
bypassed.
|
|
59
|
+
|
|
60
|
+
Option 4 — Accept the behavior: if the failed jobs don't interact with shared
|
|
61
|
+
resources, concurrent partial reruns may be safe. Restrict "Re-run failed jobs" to
|
|
62
|
+
jobs that are idempotent and isolated.
|
|
63
|
+
fix_code:
|
|
64
|
+
- language: yaml
|
|
65
|
+
label: 'Option A: include run_attempt in group key — each attempt gets its own slot'
|
|
66
|
+
code: |
|
|
67
|
+
concurrency:
|
|
68
|
+
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.run_attempt }}
|
|
69
|
+
cancel-in-progress: false
|
|
70
|
+
- language: yaml
|
|
71
|
+
label: 'Option B: environment protection to serialize deployments independent of concurrency'
|
|
72
|
+
code: |
|
|
73
|
+
jobs:
|
|
74
|
+
deploy:
|
|
75
|
+
environment: production # Requires reviewer approval; serializes even without concurrency
|
|
76
|
+
steps:
|
|
77
|
+
- run: ./deploy.sh
|
|
78
|
+
prevention:
|
|
79
|
+
- 'Prefer "Re-run all jobs" over "Re-run failed jobs" when your workflow uses a concurrency group to prevent duplicate concurrent runs'
|
|
80
|
+
- 'Include ${{ github.run_attempt }} in the concurrency group key to give each attempt its own slot and avoid silent bypass'
|
|
81
|
+
- 'For deployment workflows with shared infrastructure, combine concurrency groups with environment protection rules for defense-in-depth serialization'
|
|
82
|
+
- 'Document in your workflow YAML that "Re-run failed jobs" bypasses concurrency to alert future maintainers'
|
|
83
|
+
docs:
|
|
84
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-concurrency'
|
|
85
|
+
label: 'GitHub Docs: Using concurrency'
|
|
86
|
+
- url: 'https://github.com/actions/runner/issues/2294'
|
|
87
|
+
label: 'actions/runner#2294: Re-run failed jobs bypasses concurrency group (open)'
|
|
88
|
+
- url: 'https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/re-running-workflows-and-jobs'
|
|
89
|
+
label: 'GitHub Docs: Re-running workflows and jobs'
|
package/errors/concurrency-timing/workflow-run-head-branch-null-schedule-dispatch-concurrency.yml
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
id: concurrency-timing-055
|
|
2
|
+
title: '`github.event.workflow_run.head_branch` is null for `schedule`- and `workflow_dispatch`-triggered
|
|
3
|
+
parent workflows — concurrency group key collapses all downstream runs into one slot'
|
|
4
|
+
category: concurrency-timing
|
|
5
|
+
severity: silent-failure
|
|
6
|
+
tags:
|
|
7
|
+
- workflow_run
|
|
8
|
+
- concurrency
|
|
9
|
+
- head_branch
|
|
10
|
+
- schedule
|
|
11
|
+
- null-key
|
|
12
|
+
- deployment
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'on:\s+workflow_run:'
|
|
15
|
+
flags: 'si'
|
|
16
|
+
- regex: 'group:.*event\.workflow_run\.head_branch'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
error_messages:
|
|
19
|
+
- "Canceling since a higher priority waiting run was found for the same concurrency group"
|
|
20
|
+
- "This run has been cancelled. Concurrency group: deploy-"
|
|
21
|
+
root_cause: |
|
|
22
|
+
`github.event.workflow_run.head_branch` is null/empty when the triggering (upstream)
|
|
23
|
+
workflow was itself triggered by `schedule`, `workflow_dispatch`, `repository_dispatch`,
|
|
24
|
+
or any other non-branch event. Only workflows triggered by branch-based events (push,
|
|
25
|
+
pull_request, etc.) produce a non-null head_branch value.
|
|
26
|
+
|
|
27
|
+
The standard recommendation for workflow_run-triggered workflows (see also
|
|
28
|
+
concurrency-timing-045) is to key the concurrency group on head_branch:
|
|
29
|
+
|
|
30
|
+
concurrency:
|
|
31
|
+
group: deploy-${{ github.event.workflow_run.head_branch }}
|
|
32
|
+
|
|
33
|
+
For branch-based parent triggers this works correctly. For schedule- or
|
|
34
|
+
workflow_dispatch-triggered parent workflows, head_branch is null, making the
|
|
35
|
+
concurrency group key evaluate to `deploy-` (empty suffix). Every downstream run
|
|
36
|
+
where the parent was triggered by schedule or dispatch shares the SAME concurrency
|
|
37
|
+
slot. The second scheduled downstream run cancels the first — silently, because
|
|
38
|
+
`${{ null }}` evaluates to an empty string in GitHub Actions expressions with no
|
|
39
|
+
warning or error.
|
|
40
|
+
|
|
41
|
+
This is commonly introduced when teams follow a deploy-after-CI pattern where the
|
|
42
|
+
nightly build (schedule) triggers a downstream deploy/notify workflow_run workflow
|
|
43
|
+
and copy the branch-scoped concurrency pattern from another workflow.
|
|
44
|
+
fix: |
|
|
45
|
+
Add a fallback value to handle null head_branch, or use a more robust key.
|
|
46
|
+
|
|
47
|
+
Option 1 — head_branch with workflow_run.id fallback (recommended):
|
|
48
|
+
Use head_branch when available (branch-based parent) and fall back to the unique
|
|
49
|
+
workflow_run id (schedule/dispatch parent) to guarantee no cross-run collisions.
|
|
50
|
+
|
|
51
|
+
Option 2 — workflow_run.id only:
|
|
52
|
+
If the workflow_run-triggered downstream workflow is exclusively for non-branch
|
|
53
|
+
parents (schedule, dispatch), key the group on github.event.workflow_run.id.
|
|
54
|
+
Every downstream run gets its own unique slot.
|
|
55
|
+
|
|
56
|
+
Option 3 — Restrict trigger with branches: filter:
|
|
57
|
+
Limit the workflow_run trigger to branch-based parent runs using `branches:`.
|
|
58
|
+
Schedule/dispatch-triggered completions are silently ignored so there is no
|
|
59
|
+
concurrency collision to manage.
|
|
60
|
+
fix_code:
|
|
61
|
+
- language: yaml
|
|
62
|
+
label: 'WRONG — head_branch is null for schedule-triggered parents; all runs share one slot'
|
|
63
|
+
code: |
|
|
64
|
+
on:
|
|
65
|
+
workflow_run:
|
|
66
|
+
workflows: ["Nightly Build"]
|
|
67
|
+
types: [completed]
|
|
68
|
+
|
|
69
|
+
concurrency:
|
|
70
|
+
group: deploy-${{ github.event.workflow_run.head_branch }}
|
|
71
|
+
cancel-in-progress: true
|
|
72
|
+
|
|
73
|
+
jobs:
|
|
74
|
+
deploy:
|
|
75
|
+
if: github.event.workflow_run.conclusion == 'success'
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
steps:
|
|
78
|
+
- run: echo "Deploying..."
|
|
79
|
+
- language: yaml
|
|
80
|
+
label: 'RIGHT — fallback to workflow_run.id prevents slot collision for schedule parents'
|
|
81
|
+
code: |
|
|
82
|
+
on:
|
|
83
|
+
workflow_run:
|
|
84
|
+
workflows: ["Nightly Build"]
|
|
85
|
+
types: [completed]
|
|
86
|
+
|
|
87
|
+
# head_branch is populated for push/pull_request-triggered parents
|
|
88
|
+
# workflow_run.id is always unique — prevents cross-run collision for schedule/dispatch parents
|
|
89
|
+
concurrency:
|
|
90
|
+
group: deploy-${{ github.event.workflow_run.head_branch || github.event.workflow_run.id }}
|
|
91
|
+
cancel-in-progress: true
|
|
92
|
+
|
|
93
|
+
jobs:
|
|
94
|
+
deploy:
|
|
95
|
+
if: github.event.workflow_run.conclusion == 'success'
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
steps:
|
|
98
|
+
- run: echo "Deploying commit ${{ github.event.workflow_run.head_sha }}"
|
|
99
|
+
- language: yaml
|
|
100
|
+
label: 'RIGHT — restrict workflow_run to branch-based parents only'
|
|
101
|
+
code: |
|
|
102
|
+
on:
|
|
103
|
+
workflow_run:
|
|
104
|
+
workflows: ["CI"]
|
|
105
|
+
types: [completed]
|
|
106
|
+
branches: [main, 'release/**']
|
|
107
|
+
# schedule- and workflow_dispatch-triggered parent completions have no branch;
|
|
108
|
+
# the branches: filter excludes them, so no null head_branch issue
|
|
109
|
+
|
|
110
|
+
concurrency:
|
|
111
|
+
group: deploy-${{ github.event.workflow_run.head_branch }}
|
|
112
|
+
cancel-in-progress: true
|
|
113
|
+
|
|
114
|
+
jobs:
|
|
115
|
+
deploy:
|
|
116
|
+
if: github.event.workflow_run.conclusion == 'success'
|
|
117
|
+
runs-on: ubuntu-latest
|
|
118
|
+
steps:
|
|
119
|
+
- run: echo "Deploying branch ${{ github.event.workflow_run.head_branch }}"
|
|
120
|
+
prevention:
|
|
121
|
+
- 'Always check whether parent workflows may be triggered by schedule or workflow_dispatch before
|
|
122
|
+
keying a workflow_run downstream concurrency group on head_branch.'
|
|
123
|
+
- 'Add a || fallback for any workflow_run context property that may be null — head_branch, head_sha,
|
|
124
|
+
and pull_requests are null for schedule/dispatch-triggered parents.'
|
|
125
|
+
- 'Add a debug step that echoes github.event.workflow_run.head_branch early in the workflow to verify
|
|
126
|
+
it is populated for all expected parent trigger types during initial rollout.'
|
|
127
|
+
- 'Use branches: on the workflow_run trigger to restrict to branch-based events if schedule/dispatch
|
|
128
|
+
parent completions do not need to be handled.'
|
|
129
|
+
docs:
|
|
130
|
+
- url: 'https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_run'
|
|
131
|
+
label: 'GitHub Webhook Docs: workflow_run payload — head_branch property'
|
|
132
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_run'
|
|
133
|
+
label: 'GitHub Docs: workflow_run event trigger'
|
|
134
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-concurrency'
|
|
135
|
+
label: 'GitHub Docs: Using concurrency'
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
id: known-unsolved-064
|
|
2
|
+
title: 'No conditional matrix skip — an empty matrix from `fromJSON([])` always fails the workflow with a validation error'
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: limitation
|
|
5
|
+
tags:
|
|
6
|
+
- matrix
|
|
7
|
+
- dynamic-matrix
|
|
8
|
+
- fromJSON
|
|
9
|
+
- conditional
|
|
10
|
+
- strategy
|
|
11
|
+
- limitation
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'The strategy/matrix must contain at least one'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'matrix must define at least one vector'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'Error when evaluating .strategy. for job.*matrix.*empty'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- 'The strategy/matrix must contain at least one vector'
|
|
21
|
+
- 'matrix must define at least one vector'
|
|
22
|
+
- 'Error when evaluating ''strategy'' for job'
|
|
23
|
+
root_cause: |
|
|
24
|
+
GitHub Actions validates that a matrix strategy always produces at least one job.
|
|
25
|
+
When a prior step outputs an empty JSON array and the matrix job uses
|
|
26
|
+
`fromJSON(steps.generate.outputs.matrix)`, the workflow fails at plan time with
|
|
27
|
+
a validation error before any jobs run.
|
|
28
|
+
|
|
29
|
+
There is NO supported way to:
|
|
30
|
+
- Provide an `if:` condition on the matrix strategy to skip it entirely
|
|
31
|
+
- Use `strategy: if: ${{ condition }}` syntax (does not exist)
|
|
32
|
+
- Have a job run zero times when its matrix is empty
|
|
33
|
+
|
|
34
|
+
This is a fundamental platform limitation: matrix jobs must always expand to at
|
|
35
|
+
least one job instance. The validation runs before job execution, so even an `if:`
|
|
36
|
+
on the job itself does not help — the matrix must be non-empty regardless.
|
|
37
|
+
|
|
38
|
+
Common scenarios:
|
|
39
|
+
- CI that builds only changed packages: if no packages changed, the matrix
|
|
40
|
+
is empty and the entire workflow fails.
|
|
41
|
+
- Release workflows that conditionally matrix over artifacts: if nothing was
|
|
42
|
+
built, the matrix is empty.
|
|
43
|
+
- PR labeler workflows that matrix over affected services: a trivial change
|
|
44
|
+
affects no services, producing an empty list.
|
|
45
|
+
|
|
46
|
+
Note: yaml-syntax-008 covers the technical "fromJSON parse error" message. This
|
|
47
|
+
entry covers the LIMITATION: there is no native mechanism to conditionally skip
|
|
48
|
+
matrix jobs or provide a zero-matrix strategy.
|
|
49
|
+
fix: |
|
|
50
|
+
Workaround 1 — Always include at least one sentinel/dummy entry and guard with if:
|
|
51
|
+
In the matrix-generating step, append a sentinel entry when the list is empty:
|
|
52
|
+
matrix=$(echo "$matrix" | jq 'if length == 0 then [{"skip":true}] else map(. + {"skip":false}) end')
|
|
53
|
+
Then in the job:
|
|
54
|
+
if: ${{ !matrix.skip }}
|
|
55
|
+
|
|
56
|
+
Workaround 2 — Use a separate check job with needs: to gate the matrix job:
|
|
57
|
+
Add an upstream job that outputs a boolean "has_work". The matrix job then uses
|
|
58
|
+
`needs: check` and reads `needs.check.outputs.has_work` in an `if:` condition.
|
|
59
|
+
This still requires the matrix to be non-empty (use sentinel), but the `if:`
|
|
60
|
+
prevents actual work from running.
|
|
61
|
+
|
|
62
|
+
Workaround 3 — Always include a no-op entry in the matrix output:
|
|
63
|
+
Ensure the matrix-generating script never outputs an empty array by always
|
|
64
|
+
appending a `{"target":"none"}` entry, then:
|
|
65
|
+
if: matrix.target != 'none'
|
|
66
|
+
|
|
67
|
+
None of these workarounds are elegant. This is a known limitation with no native
|
|
68
|
+
fix scheduled. Tracked in actions/runner#1502 (96+ reactions, open since 2021).
|
|
69
|
+
fix_code:
|
|
70
|
+
- language: yaml
|
|
71
|
+
label: 'Workaround: inject sentinel entry when matrix is empty, skip in job if:'
|
|
72
|
+
code: |
|
|
73
|
+
jobs:
|
|
74
|
+
generate-matrix:
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
outputs:
|
|
77
|
+
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
|
78
|
+
steps:
|
|
79
|
+
- id: set-matrix
|
|
80
|
+
run: |
|
|
81
|
+
# Build your matrix list; inject sentinel when empty
|
|
82
|
+
matrix='[]' # Replace with real generation logic
|
|
83
|
+
if [ "$(echo "$matrix" | jq 'length')" -eq 0 ]; then
|
|
84
|
+
matrix='[{"target":"__skip__"}]'
|
|
85
|
+
fi
|
|
86
|
+
echo "matrix=$matrix" >> $GITHUB_OUTPUT
|
|
87
|
+
|
|
88
|
+
build:
|
|
89
|
+
needs: generate-matrix
|
|
90
|
+
if: ${{ fromJSON(needs.generate-matrix.outputs.matrix)[0].target != '__skip__' }}
|
|
91
|
+
strategy:
|
|
92
|
+
matrix:
|
|
93
|
+
include: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}
|
|
94
|
+
runs-on: ubuntu-latest
|
|
95
|
+
steps:
|
|
96
|
+
- run: echo "Building ${{ matrix.target }}"
|
|
97
|
+
prevention:
|
|
98
|
+
- 'Never pass a potentially empty array directly to fromJSON() in a matrix strategy — always guard with a sentinel entry'
|
|
99
|
+
- 'Add an upstream check job that validates matrix size before the matrix job runs'
|
|
100
|
+
- 'Document the sentinel pattern in your workflow so future maintainers understand why a dummy entry exists'
|
|
101
|
+
- 'Vote/watch actions/runner#1502 for native conditional matrix support'
|
|
102
|
+
docs:
|
|
103
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow'
|
|
104
|
+
label: 'GitHub Docs: Running variations of jobs in a workflow (matrix)'
|
|
105
|
+
- url: 'https://github.com/actions/runner/issues/1502'
|
|
106
|
+
label: 'actions/runner#1502: Support for empty matrix (96+ reactions, open)'
|
|
107
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix'
|
|
108
|
+
label: 'GitHub Docs: jobs.<job_id>.strategy.matrix'
|