@htekdev/actions-debugger 1.0.117 → 1.0.119
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/caching-artifacts-069.yml +133 -0
- package/errors/caching-artifacts/caching-artifacts-070.yml +94 -0
- package/errors/concurrency-timing/concurrency-timing-056.yml +127 -0
- package/errors/concurrency-timing/concurrency-timing-057.yml +115 -0
- package/errors/concurrency-timing/workflow-run-head-branch-null-schedule-dispatch-concurrency.yml +135 -0
- package/errors/known-unsolved/known-unsolved-067.yml +117 -0
- package/errors/known-unsolved/known-unsolved-068.yml +124 -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/runner-environment-210.yml +105 -0
- package/errors/runner-environment/runner-environment-213.yml +142 -0
- package/errors/runner-environment/runner-environment-214.yml +107 -0
- package/errors/runner-environment/runner-environment-215.yml +93 -0
- package/errors/runner-environment/runner-environment-216.yml +82 -0
- package/errors/runner-environment/runner-environment-217.yml +99 -0
- package/errors/runner-environment/runner-environment-218.yml +111 -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/silent-failures/silent-failures-109.yml +119 -0
- package/errors/silent-failures/silent-failures-110.yml +91 -0
- package/errors/silent-failures/silent-failures-111.yml +107 -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/errors/yaml-syntax/yaml-syntax-072.yml +93 -0
- package/errors/yaml-syntax/yaml-syntax-073.yml +103 -0
- package/package.json +1 -1
|
@@ -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,94 @@
|
|
|
1
|
+
id: caching-artifacts-070
|
|
2
|
+
title: "setup-python Post step fails — pip cache directory doesn't exist on disk"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- setup-python
|
|
7
|
+
- pip
|
|
8
|
+
- cache
|
|
9
|
+
- post-step
|
|
10
|
+
- cache-miss
|
|
11
|
+
- no-dependencies
|
|
12
|
+
- python-version-bump
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'Cache folder path is retrieved for pip but doesn.t exist on disk'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'likely indicates that there are no dependencies to cache'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
- regex: 'Post Setup Python.*fail|Post.*setup-python.*error'
|
|
19
|
+
flags: 'i'
|
|
20
|
+
error_messages:
|
|
21
|
+
- "Cache folder path is retrieved for pip but doesn't exist on disk: /home/runner/.cache/pip. This likely indicates that there are no dependencies to cache. Consider removing the cache step if it is not needed."
|
|
22
|
+
root_cause: |
|
|
23
|
+
When actions/setup-python is configured with cache: 'pip', the action records the expected
|
|
24
|
+
pip cache directory path at setup time (/home/runner/.cache/pip on Linux, equivalent on
|
|
25
|
+
macOS/Windows). The Post Setup Python step runs at job end and attempts to save the cache
|
|
26
|
+
to that path. If the directory does not exist on disk at save time, the Post step fails
|
|
27
|
+
with this error.
|
|
28
|
+
|
|
29
|
+
Two common causes:
|
|
30
|
+
|
|
31
|
+
1. No pip install ran in the job: The job uses cache: 'pip' but only runs linting or other
|
|
32
|
+
pre-installed tools without installing any Python packages. Pip never creates its cache
|
|
33
|
+
directory, so there is nothing to save. The job's actual steps appear green while the
|
|
34
|
+
Post step fails and turns the overall workflow run red.
|
|
35
|
+
|
|
36
|
+
2. Python version bump causes cache key miss: When the Python patch version changes (e.g.,
|
|
37
|
+
from 3.13.5 to 3.13.6), the setup-python cache key changes and the first run after the
|
|
38
|
+
bump experiences a full cache miss. If pip install runs but writes packages to a virtual
|
|
39
|
+
environment rather than the global pip cache (/home/runner/.cache/pip), the expected
|
|
40
|
+
directory remains empty and the Post step fails. Subsequent runs after the new cache is
|
|
41
|
+
warmed succeed.
|
|
42
|
+
|
|
43
|
+
The failure is deceptive because it surfaces in the Post Setup Python cleanup step — well
|
|
44
|
+
after the test or build steps have already succeeded — making it easy to overlook the root
|
|
45
|
+
cause.
|
|
46
|
+
fix: |
|
|
47
|
+
- Remove cache: 'pip' from any setup-python step in jobs that do not call pip install.
|
|
48
|
+
Linting-only jobs, type-check-only jobs, and jobs that rely entirely on pre-installed
|
|
49
|
+
system Python do not benefit from pip caching.
|
|
50
|
+
- If pip install does run, ensure it runs against the global pip (not a virtualenv that
|
|
51
|
+
bypasses /home/runner/.cache/pip) so the post step can find and save the cache.
|
|
52
|
+
- Upgrade to actions/setup-python@v5 or later: newer versions emit a warning annotation
|
|
53
|
+
instead of failing the step when the cache directory is missing.
|
|
54
|
+
- After a Python version bump, the first run is expected to cache-miss; monitor for Post
|
|
55
|
+
step failures on that first run and confirm subsequent runs succeed.
|
|
56
|
+
fix_code:
|
|
57
|
+
- language: yaml
|
|
58
|
+
label: 'Fix: Remove cache when no pip install runs'
|
|
59
|
+
code: |
|
|
60
|
+
# WRONG: cache: pip set but job only lints — no pip install → Post step fails
|
|
61
|
+
- uses: actions/setup-python@v4
|
|
62
|
+
with:
|
|
63
|
+
python-version: '3.12'
|
|
64
|
+
cache: 'pip' # ← REMOVE when no pip install follows
|
|
65
|
+
- name: Lint
|
|
66
|
+
run: flake8 . # no pip install; /home/runner/.cache/pip never created
|
|
67
|
+
|
|
68
|
+
# CORRECT: omit cache when the job does not install packages
|
|
69
|
+
- uses: actions/setup-python@v4
|
|
70
|
+
with:
|
|
71
|
+
python-version: '3.12'
|
|
72
|
+
# no cache key — Post step skips cache save attempt entirely
|
|
73
|
+
- name: Lint
|
|
74
|
+
run: flake8 .
|
|
75
|
+
- language: yaml
|
|
76
|
+
label: 'Fix: Upgrade to setup-python@v5 for graceful handling'
|
|
77
|
+
code: |
|
|
78
|
+
# CORRECT: v5+ emits a warning annotation instead of failing when cache path missing
|
|
79
|
+
- uses: actions/setup-python@v5
|
|
80
|
+
with:
|
|
81
|
+
python-version: '3.12'
|
|
82
|
+
cache: 'pip'
|
|
83
|
+
- name: Install dependencies
|
|
84
|
+
run: pip install -r requirements.txt
|
|
85
|
+
prevention:
|
|
86
|
+
- 'Only set cache: pip on jobs that actually run pip install — linting-only jobs should omit it.'
|
|
87
|
+
- 'Use actions/setup-python@v5 or later; it handles missing cache directories with a warning instead of a failure.'
|
|
88
|
+
- 'After bumping the Python patch version in python-version:, expect one cache-miss run and watch for Post step failures on that run only.'
|
|
89
|
+
- 'When using virtual environments (venv/pipenv/poetry), ensure pip still writes to the global cache or configure cache-dependency-path appropriately.'
|
|
90
|
+
docs:
|
|
91
|
+
- url: 'https://github.com/actions/setup-python/issues/1169'
|
|
92
|
+
label: 'setup-python#1169: Cache folder path doesn''t exist on disk (Aug 2025)'
|
|
93
|
+
- url: 'https://github.com/actions/setup-python'
|
|
94
|
+
label: 'actions/setup-python: Caching packages documentation'
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
id: concurrency-timing-056
|
|
2
|
+
title: 'Workflow-level and job-level concurrency share same group key — deadlock cancellation fires immediately'
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- deadlock
|
|
8
|
+
- workflow-level
|
|
9
|
+
- job-level
|
|
10
|
+
- reusable-workflow
|
|
11
|
+
- workflow-call
|
|
12
|
+
- github-workflow-context
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'Canceling since a deadlock for concurrency group .* was detected between'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'deadlock.*concurrency group|concurrency group.*deadlock'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
error_messages:
|
|
19
|
+
- "Canceling since a deadlock for concurrency group 'ci-refs/heads/main' was detected between 'top level workflow' and 'deploy'"
|
|
20
|
+
- "Canceling since a deadlock for concurrency group 'release-refs/heads/main' was detected between 'top level workflow' and 'api'"
|
|
21
|
+
root_cause: |
|
|
22
|
+
GitHub Actions fires a deadlock error and immediately cancels the offending job when the
|
|
23
|
+
same concurrency group name is held simultaneously by two levels within a single workflow
|
|
24
|
+
execution. Two distinct scenarios trigger this:
|
|
25
|
+
|
|
26
|
+
Scenario 1 — Same-workflow self-deadlock: A workflow file defines concurrency: at the
|
|
27
|
+
workflow level AND one of its jobs also defines jobs.<id>.concurrency: using an expression
|
|
28
|
+
that evaluates to the same string:
|
|
29
|
+
|
|
30
|
+
concurrency:
|
|
31
|
+
group: ${{ github.workflow }}-${{ github.ref }} # workflow-level slot
|
|
32
|
+
|
|
33
|
+
jobs:
|
|
34
|
+
deploy:
|
|
35
|
+
concurrency:
|
|
36
|
+
group: ${{ github.workflow }}-${{ github.ref }} # same string → deadlock
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
|
|
39
|
+
Scenario 2 — Reusable callee inherits caller context: A calling workflow has workflow-level
|
|
40
|
+
concurrency using ${{ github.workflow }}-${{ github.ref }}. The called reusable workflow
|
|
41
|
+
also defines workflow-level concurrency with the same expression. Because github.workflow
|
|
42
|
+
inside a reusable workflow inherits the CALLER's workflow name (not the callee's filename),
|
|
43
|
+
both evaluate to the identical group key and GitHub detects a deadlock.
|
|
44
|
+
|
|
45
|
+
github.workflow_ref also inherits from the top-level caller and does NOT produce a unique
|
|
46
|
+
value in the callee context; it cannot be used to distinguish caller from callee.
|
|
47
|
+
fix: |
|
|
48
|
+
Scenario 1 — Remove the duplicate concurrency block. Keep either the workflow-level OR
|
|
49
|
+
the job-level declaration, not both with the same key. If per-job isolation is needed,
|
|
50
|
+
append ${{ github.job }} to the job-level group name:
|
|
51
|
+
|
|
52
|
+
jobs:
|
|
53
|
+
deploy:
|
|
54
|
+
concurrency:
|
|
55
|
+
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.job }}
|
|
56
|
+
|
|
57
|
+
Scenario 2 — Remove the concurrency: block entirely from the reusable workflow. The
|
|
58
|
+
caller's workflow-level concurrency already governs the entire execution. If the callee
|
|
59
|
+
needs standalone concurrency when triggered via workflow_dispatch, use a hardcoded unique
|
|
60
|
+
prefix instead of ${{ github.workflow }}:
|
|
61
|
+
|
|
62
|
+
concurrency:
|
|
63
|
+
group: deploy-${{ github.ref }} # hardcoded prefix avoids collision with any caller
|
|
64
|
+
cancel-in-progress: true
|
|
65
|
+
fix_code:
|
|
66
|
+
- language: yaml
|
|
67
|
+
label: 'Scenario 1 fix: remove duplicate job-level concurrency'
|
|
68
|
+
code: |
|
|
69
|
+
# WRONG — identical group at workflow level and job level → deadlock
|
|
70
|
+
concurrency:
|
|
71
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
72
|
+
cancel-in-progress: true
|
|
73
|
+
|
|
74
|
+
jobs:
|
|
75
|
+
deploy:
|
|
76
|
+
concurrency:
|
|
77
|
+
group: ${{ github.workflow }}-${{ github.ref }} # ← DELETE THIS
|
|
78
|
+
runs-on: ubuntu-latest
|
|
79
|
+
steps:
|
|
80
|
+
- run: echo deploying
|
|
81
|
+
|
|
82
|
+
# CORRECT — concurrency only at workflow level
|
|
83
|
+
concurrency:
|
|
84
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
85
|
+
cancel-in-progress: true
|
|
86
|
+
|
|
87
|
+
jobs:
|
|
88
|
+
deploy:
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
steps:
|
|
91
|
+
- run: echo deploying
|
|
92
|
+
- language: yaml
|
|
93
|
+
label: 'Scenario 2 fix: remove concurrency from reusable workflow'
|
|
94
|
+
code: |
|
|
95
|
+
# deploy.yml (reusable) — WRONG: workflow-level concurrency collides with caller
|
|
96
|
+
# because github.workflow returns the CALLER's name in reusable context
|
|
97
|
+
on:
|
|
98
|
+
workflow_call:
|
|
99
|
+
workflow_dispatch:
|
|
100
|
+
# concurrency: ← DELETE this entire block from the reusable workflow
|
|
101
|
+
# group: ${{ github.workflow }}-${{ github.ref }}
|
|
102
|
+
# cancel-in-progress: true
|
|
103
|
+
|
|
104
|
+
jobs:
|
|
105
|
+
deploy:
|
|
106
|
+
runs-on: ubuntu-latest
|
|
107
|
+
steps:
|
|
108
|
+
- run: echo deploying
|
|
109
|
+
|
|
110
|
+
# If standalone concurrency is needed for workflow_dispatch calls, use hardcoded prefix:
|
|
111
|
+
# concurrency:
|
|
112
|
+
# group: deploy-${{ github.ref }} # hardcoded "deploy-" avoids collision
|
|
113
|
+
# cancel-in-progress: true
|
|
114
|
+
prevention:
|
|
115
|
+
- 'Before adding concurrency: to a reusable workflow, check if it will be called via workflow_call — if so, remove it or use a hardcoded prefix.'
|
|
116
|
+
- 'Never use the same concurrency group expression at both the workflow level and job level in the same file.'
|
|
117
|
+
- 'Note that ${{ github.workflow }} and ${{ github.workflow_ref }} both return the top-level caller''s values inside a reusable workflow; neither provides the callee''s filename.'
|
|
118
|
+
- 'Use actionlint to statically detect identical concurrency groups — issue actionlint#538 tracks adding this check.'
|
|
119
|
+
docs:
|
|
120
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs'
|
|
121
|
+
label: 'GitHub Docs: Controlling the concurrency of workflows and jobs'
|
|
122
|
+
- url: 'https://stackoverflow.com/questions/78101326/github-actions-concurrency-deadlock'
|
|
123
|
+
label: 'SO: GitHub Actions concurrency deadlock (Score 6, 1.7K views)'
|
|
124
|
+
- url: 'https://stackoverflow.com/questions/79511940/using-workflow-filename-in-concurrency-group-for-workflows-started-by-workflow-c'
|
|
125
|
+
label: 'SO: Using workflow filename in concurrency group for workflow_call (Score 3)'
|
|
126
|
+
- url: 'https://github.com/github/vscode-github-actions/issues/135'
|
|
127
|
+
label: 'vscode-github-actions#135: Identical concurrency groups cause silent never-run (14 reactions)'
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
id: concurrency-timing-057
|
|
2
|
+
title: "Fork PRs with Identical Branch Names Share Concurrency Group and Cancel Each Other"
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- fork
|
|
8
|
+
- pull_request
|
|
9
|
+
- head_ref
|
|
10
|
+
- cancel-in-progress
|
|
11
|
+
- silent-cancel
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'group.*github\.head_ref'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'This run was cancelled'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "This run was cancelled."
|
|
19
|
+
- "Run was cancelled."
|
|
20
|
+
root_cause: |
|
|
21
|
+
When a workflow uses 'github.head_ref' as the sole identifier in a
|
|
22
|
+
concurrency group key (a common pattern to cancel stale runs on the same
|
|
23
|
+
branch), all pull requests that share a branch name across different forks
|
|
24
|
+
map to the SAME concurrency group. With 'cancel-in-progress: true', the
|
|
25
|
+
latest queued run cancels all earlier runs in that group — including runs
|
|
26
|
+
from completely unrelated PRs in OTHER contributor forks.
|
|
27
|
+
|
|
28
|
+
Example scenario:
|
|
29
|
+
- Fork A (alice/myrepo) opens PR from branch 'fix/auth-bug'.
|
|
30
|
+
- Fork B (bob/myrepo) opens a PR from a branch also named 'fix/auth-bug'.
|
|
31
|
+
- Both PRs target the same upstream repo (org/myrepo).
|
|
32
|
+
- Concurrency group: 'ci-fix/auth-bug' (from github.head_ref).
|
|
33
|
+
- When Fork B's PR triggers a run, it cancels Fork A's in-progress run.
|
|
34
|
+
|
|
35
|
+
The cancellation appears as "This run was cancelled" with no explanation
|
|
36
|
+
that a different fork's PR caused it. Maintainers and contributors see
|
|
37
|
+
flaky-looking CI with no obvious cause.
|
|
38
|
+
|
|
39
|
+
This is especially common in:
|
|
40
|
+
- Large open-source projects where many contributors use the same
|
|
41
|
+
conventional branch names (fix/typo, docs/readme, feature/x).
|
|
42
|
+
- Dependabot/Renovate PRs across forks — all use the same structured
|
|
43
|
+
branch name pattern (dependabot/npm_and_yarn/lodash-4.0.0).
|
|
44
|
+
fix: |
|
|
45
|
+
Include 'github.event.pull_request.number' in the concurrency group key.
|
|
46
|
+
PR numbers are unique per repository, so two PRs from different forks
|
|
47
|
+
always have different numbers even if their branch names collide.
|
|
48
|
+
|
|
49
|
+
Alternative: use 'github.run_id' for maximum uniqueness (no cancellation
|
|
50
|
+
across runs at all), but this defeats the purpose of cancel-in-progress
|
|
51
|
+
for the same PR.
|
|
52
|
+
|
|
53
|
+
The recommended pattern from GitHub documentation:
|
|
54
|
+
group: '${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}'
|
|
55
|
+
|
|
56
|
+
The '|| github.ref' fallback handles non-PR events (push, schedule)
|
|
57
|
+
where 'github.event.pull_request.number' is empty.
|
|
58
|
+
fix_code:
|
|
59
|
+
- language: yaml
|
|
60
|
+
label: "Broken — github.head_ref alone causes cross-fork cancellation"
|
|
61
|
+
code: |
|
|
62
|
+
concurrency:
|
|
63
|
+
group: ci-${{ github.head_ref }} # ❌ Collides across forks with same branch name
|
|
64
|
+
cancel-in-progress: true
|
|
65
|
+
|
|
66
|
+
- language: yaml
|
|
67
|
+
label: "Fixed — include PR number to ensure per-PR uniqueness"
|
|
68
|
+
code: |
|
|
69
|
+
concurrency:
|
|
70
|
+
# PR number is unique per repo — different forks never collide
|
|
71
|
+
group: '${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}'
|
|
72
|
+
cancel-in-progress: true
|
|
73
|
+
|
|
74
|
+
- language: yaml
|
|
75
|
+
label: "Alternative — include repo owner to scope per fork"
|
|
76
|
+
code: |
|
|
77
|
+
concurrency:
|
|
78
|
+
# Include head repo full_name to distinguish forks explicitly
|
|
79
|
+
group: >-
|
|
80
|
+
${{ github.workflow }}-
|
|
81
|
+
${{ github.event.pull_request.head.repo.full_name || github.repository }}-
|
|
82
|
+
${{ github.event.pull_request.number || github.ref }}
|
|
83
|
+
cancel-in-progress: true
|
|
84
|
+
|
|
85
|
+
- language: yaml
|
|
86
|
+
label: "Recommended pattern from GitHub docs — workflow + PR number or ref"
|
|
87
|
+
code: |
|
|
88
|
+
name: CI
|
|
89
|
+
on:
|
|
90
|
+
pull_request:
|
|
91
|
+
push:
|
|
92
|
+
branches: [main]
|
|
93
|
+
|
|
94
|
+
concurrency:
|
|
95
|
+
group: '${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}'
|
|
96
|
+
cancel-in-progress: true
|
|
97
|
+
|
|
98
|
+
jobs:
|
|
99
|
+
test:
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
steps:
|
|
102
|
+
- uses: actions/checkout@v4
|
|
103
|
+
- run: ./run-tests.sh
|
|
104
|
+
|
|
105
|
+
prevention:
|
|
106
|
+
- "Never use github.head_ref alone as a concurrency group key for pull_request workflows."
|
|
107
|
+
- "Always pair github.head_ref with github.event.pull_request.number to scope to the specific PR."
|
|
108
|
+
- "Use the GitHub-recommended pattern: '${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}'."
|
|
109
|
+
- "Test concurrency behavior by opening two PRs from different forks with the same branch name before merging concurrency configuration."
|
|
110
|
+
- "On public repos with many external contributors, audit all concurrency group keys for cross-fork collision risk."
|
|
111
|
+
docs:
|
|
112
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-concurrency"
|
|
113
|
+
label: "GitHub Docs — Using concurrency (recommended group key pattern)"
|
|
114
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#concurrency"
|
|
115
|
+
label: "GitHub Docs — Workflow syntax: concurrency"
|
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'
|