@htekdev/actions-debugger 1.0.56 → 1.0.57
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-038.yml +95 -0
- package/errors/caching-artifacts/caching-artifacts-039.yml +110 -0
- package/errors/concurrency-timing/concurrency-timing-033.yml +104 -0
- package/errors/concurrency-timing/concurrency-timing-034.yml +123 -0
- package/errors/known-unsolved/known-unsolved-038.yml +124 -0
- package/errors/known-unsolved/known-unsolved-039.yml +102 -0
- package/errors/permissions-auth/permissions-auth-041.yml +110 -0
- package/errors/runner-environment/runner-environment-112.yml +98 -0
- package/errors/runner-environment/runner-environment-113.yml +118 -0
- package/errors/runner-environment/runner-environment-114.yml +130 -0
- package/errors/runner-environment/runner-environment-115.yml +120 -0
- package/errors/runner-environment/runner-environment-116.yml +106 -0
- package/errors/runner-environment/runner-environment-117.yml +109 -0
- package/errors/silent-failures/silent-failures-057.yml +120 -0
- package/errors/silent-failures/silent-failures-058.yml +126 -0
- package/errors/triggers/triggers-041.yml +105 -0
- package/errors/triggers/triggers-042.yml +110 -0
- package/errors/triggers/triggers-043.yml +125 -0
- package/errors/yaml-syntax/yaml-syntax-040.yml +135 -0
- package/errors/yaml-syntax/yaml-syntax-041.yml +147 -0
- package/package.json +1 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
id: caching-artifacts-038
|
|
2
|
+
title: "hashFiles() returns empty string when no matching files found — constant cache key causes cross-branch cache pollution"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- hashFiles
|
|
8
|
+
- cache-key
|
|
9
|
+
- silent-failure
|
|
10
|
+
- cross-branch
|
|
11
|
+
- lock-file
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'hashfiles\s*\(.*\)\s*==\s*''"""'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'cache-hit.*true.*hashfiles.*empty'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Cache restored successfully"
|
|
19
|
+
- "Cache hit: true"
|
|
20
|
+
root_cause: |
|
|
21
|
+
The `hashFiles()` expression function returns an empty string when no files match the
|
|
22
|
+
provided glob pattern. It does NOT throw an error or fail the step.
|
|
23
|
+
|
|
24
|
+
A common pattern is:
|
|
25
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
26
|
+
|
|
27
|
+
If `package-lock.json` does not exist (e.g., using yarn, pnpm, or no lock file present),
|
|
28
|
+
`hashFiles('**/package-lock.json')` returns `""` and the key becomes:
|
|
29
|
+
"Linux-node-"
|
|
30
|
+
|
|
31
|
+
This constant key means every workflow run hits the same cache entry regardless of
|
|
32
|
+
dependency changes or branch. The first job saves its node_modules under this key, and
|
|
33
|
+
every subsequent run on every branch restores those same cached modules. This silently
|
|
34
|
+
masks dependency update failures or serves stale packages from a different branch's state.
|
|
35
|
+
|
|
36
|
+
The failure is invisible: `cache-hit: true` appears in logs and the job succeeds, hiding
|
|
37
|
+
the fact that no lock file was actually hashed. Developers notice only when flaky test
|
|
38
|
+
failures or "wrong version installed" errors occur.
|
|
39
|
+
fix: |
|
|
40
|
+
Use the correct lock file for your package manager, or add a fallback to prevent a
|
|
41
|
+
constant key when hashFiles returns empty.
|
|
42
|
+
|
|
43
|
+
Option 1: Use the correct lock file:
|
|
44
|
+
- npm: **/package-lock.json
|
|
45
|
+
- yarn: **/yarn.lock
|
|
46
|
+
- pnpm: **/pnpm-lock.yaml
|
|
47
|
+
- pip: **/requirements.txt or **/poetry.lock
|
|
48
|
+
|
|
49
|
+
Option 2: Add a github.run_id fallback to ensure key uniqueness when files are missing:
|
|
50
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') || github.run_id }}
|
|
51
|
+
|
|
52
|
+
Option 3: Use hashFiles with multiple patterns so at least one matches:
|
|
53
|
+
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json', '**/yarn.lock', '**/pnpm-lock.yaml') }}
|
|
54
|
+
|
|
55
|
+
Option 4: Add a validation step to fail fast if no lock file is found rather than
|
|
56
|
+
silently using a constant cache key.
|
|
57
|
+
fix_code:
|
|
58
|
+
- language: yaml
|
|
59
|
+
label: "run_id fallback prevents constant cache key when lock file is absent"
|
|
60
|
+
code: |
|
|
61
|
+
- name: Cache node modules
|
|
62
|
+
uses: actions/cache@v4
|
|
63
|
+
with:
|
|
64
|
+
path: ~/.npm
|
|
65
|
+
# hashFiles returns "" if no lock file found; run_id fallback prevents constant key
|
|
66
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') || github.run_id }}
|
|
67
|
+
restore-keys: |
|
|
68
|
+
${{ runner.os }}-node-
|
|
69
|
+
- language: yaml
|
|
70
|
+
label: "Multi-ecosystem lock file glob — matches whichever lock file is present"
|
|
71
|
+
code: |
|
|
72
|
+
- name: Cache dependencies
|
|
73
|
+
uses: actions/cache@v4
|
|
74
|
+
with:
|
|
75
|
+
path: |
|
|
76
|
+
~/.npm
|
|
77
|
+
~/.yarn/cache
|
|
78
|
+
~/.pnpm-store
|
|
79
|
+
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json', '**/yarn.lock', '**/pnpm-lock.yaml') }}
|
|
80
|
+
restore-keys: |
|
|
81
|
+
${{ runner.os }}-deps-
|
|
82
|
+
prevention:
|
|
83
|
+
- "Verify your lock file glob matches an actual file before relying on it for cache key uniqueness"
|
|
84
|
+
- "Add || github.run_id fallback: hashFiles('...') || github.run_id prevents silent constant-key caching"
|
|
85
|
+
- "Use restore-keys as a fallback strategy rather than depending on an exact key hit"
|
|
86
|
+
- "In setup steps, validate the expected lock file exists before caching (e.g., Test-Path package-lock.json)"
|
|
87
|
+
docs:
|
|
88
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#hashfiles"
|
|
89
|
+
label: "GitHub Docs — hashFiles() expression function"
|
|
90
|
+
- url: "https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows"
|
|
91
|
+
label: "GitHub Docs — Caching dependencies to speed up workflows"
|
|
92
|
+
- url: "https://github.com/actions/cache/blob/main/tips-and-workarounds.md"
|
|
93
|
+
label: "actions/cache — Tips and workarounds"
|
|
94
|
+
- url: "https://github.com/orgs/community/discussions/25064"
|
|
95
|
+
label: "GitHub Community — hashFiles returns empty when no files match"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
id: caching-artifacts-039
|
|
2
|
+
title: "actions/cache split save/restore: path must match exactly or restore silently reports cache miss"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- actions/cache
|
|
8
|
+
- save-restore
|
|
9
|
+
- path-mismatch
|
|
10
|
+
- cache-miss
|
|
11
|
+
- split-cache
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'Cache not found for input keys'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'cache/restore.*path.*not.*found'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Cache not found for input keys: <key>"
|
|
19
|
+
- "No cache found for key: <key>"
|
|
20
|
+
root_cause: |
|
|
21
|
+
When using the split `actions/cache/save@v4` and `actions/cache/restore@v4` actions
|
|
22
|
+
separately (rather than the combined `actions/cache@v4`), the `path:` value in the
|
|
23
|
+
restore step must exactly match the `path:` value used in the save step — including
|
|
24
|
+
relative vs absolute paths, trailing slashes, and directory names.
|
|
25
|
+
|
|
26
|
+
If the paths differ even slightly, the cache entry is stored under a hash that
|
|
27
|
+
incorporates the path value. The restore step computes a different hash and reports
|
|
28
|
+
"Cache not found for input keys" — identical to the message shown when the cache key
|
|
29
|
+
itself is wrong. Nothing in the error output indicates that a path mismatch is the
|
|
30
|
+
actual cause, making this extremely difficult to debug.
|
|
31
|
+
|
|
32
|
+
This is an intentional implementation detail of the split actions: the combined
|
|
33
|
+
`actions/cache@v4` avoids this because the same `path:` string is used for both
|
|
34
|
+
operations automatically. Developers migrating from the combined action to split
|
|
35
|
+
save/restore for more granular control frequently encounter this.
|
|
36
|
+
|
|
37
|
+
Common path mismatch pitfalls:
|
|
38
|
+
- Save uses `./node_modules`, restore uses `node_modules` (leading `./` difference)
|
|
39
|
+
- Save uses `/home/runner/.cache/pip`, restore uses `~/.cache/pip`
|
|
40
|
+
- Save uses a trailing slash `dist/`, restore uses `dist`
|
|
41
|
+
- Save in one job uses a relative path that resolves differently in another job
|
|
42
|
+
fix: |
|
|
43
|
+
Ensure the `path:` value in `actions/cache/restore@v4` is the exact same string
|
|
44
|
+
as used in the corresponding `actions/cache/save@v4` step. Copy-paste the value
|
|
45
|
+
rather than retyping it to prevent subtle differences.
|
|
46
|
+
|
|
47
|
+
When using the split actions across different jobs, prefer absolute paths or paths
|
|
48
|
+
anchored from a stable workspace directory. Alternatively, use the combined
|
|
49
|
+
`actions/cache@v4` with `save-always: true` to avoid managing save/restore separately.
|
|
50
|
+
fix_code:
|
|
51
|
+
- language: yaml
|
|
52
|
+
label: "Identical path values in split save and restore steps"
|
|
53
|
+
code: |
|
|
54
|
+
jobs:
|
|
55
|
+
build:
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v4
|
|
59
|
+
|
|
60
|
+
- name: Restore cached build output
|
|
61
|
+
uses: actions/cache/restore@v4
|
|
62
|
+
id: cache-restore
|
|
63
|
+
with:
|
|
64
|
+
path: .cache/build-output # Must match save step below exactly
|
|
65
|
+
key: build-${{ github.sha }}
|
|
66
|
+
restore-keys: build-
|
|
67
|
+
|
|
68
|
+
- name: Build
|
|
69
|
+
if: steps.cache-restore.outputs.cache-hit != 'true'
|
|
70
|
+
run: npm run build
|
|
71
|
+
|
|
72
|
+
- name: Save build cache
|
|
73
|
+
if: steps.cache-restore.outputs.cache-hit != 'true'
|
|
74
|
+
uses: actions/cache/save@v4
|
|
75
|
+
with:
|
|
76
|
+
path: .cache/build-output # Copy-pasted from restore step — no differences
|
|
77
|
+
key: build-${{ github.sha }}
|
|
78
|
+
|
|
79
|
+
- language: yaml
|
|
80
|
+
label: "Combined cache action avoids path mismatch entirely"
|
|
81
|
+
code: |
|
|
82
|
+
jobs:
|
|
83
|
+
build:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
|
|
88
|
+
- name: Cache build output
|
|
89
|
+
uses: actions/cache@v4
|
|
90
|
+
with:
|
|
91
|
+
path: .cache/build-output
|
|
92
|
+
key: build-${{ github.sha }}
|
|
93
|
+
restore-keys: build-
|
|
94
|
+
save-always: true # Always saves even when cache-hit is true
|
|
95
|
+
|
|
96
|
+
- run: npm run build
|
|
97
|
+
prevention:
|
|
98
|
+
- "Copy-paste the path value between save and restore steps — never retype it"
|
|
99
|
+
- "Use the combined actions/cache@v4 with save-always: true unless you specifically need split save/restore behavior"
|
|
100
|
+
- "Prefer absolute paths (e.g., /home/runner/.cache/pip) over relative paths when save and restore are in different jobs"
|
|
101
|
+
- "When debugging cache misses, add a run: echo '${{ toJSON(steps.cache.outputs) }}' step to inspect what the cache lookup returned"
|
|
102
|
+
docs:
|
|
103
|
+
- url: "https://github.com/actions/cache/blob/main/save/README.md"
|
|
104
|
+
label: "actions/cache/save — README"
|
|
105
|
+
- url: "https://github.com/actions/cache/blob/main/restore/README.md"
|
|
106
|
+
label: "actions/cache/restore — README"
|
|
107
|
+
- url: "https://github.com/actions/cache/issues/1444"
|
|
108
|
+
label: "GitHub Issue #1444 — Path mismatch causes misleading cache miss error"
|
|
109
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows"
|
|
110
|
+
label: "GitHub Docs — Caching dependencies to speed up workflows"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
id: concurrency-timing-033
|
|
2
|
+
title: "Matrix jobs silently cancel each other when concurrency group key excludes matrix dimension values"
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- matrix
|
|
8
|
+
- cancel-in-progress
|
|
9
|
+
- silent-failure
|
|
10
|
+
- matrix-strategy
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'This run was cancelled\.'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'The operation was cancelled\.'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
error_messages:
|
|
17
|
+
- "This run was cancelled."
|
|
18
|
+
- "The operation was cancelled."
|
|
19
|
+
root_cause: |
|
|
20
|
+
When a workflow defines a `concurrency` group that does not include any matrix-specific
|
|
21
|
+
expression values, all matrix jobs resolve to the exact same concurrency group key.
|
|
22
|
+
|
|
23
|
+
With `cancel-in-progress: true`, each queued matrix job cancels the previously queued
|
|
24
|
+
or running job with the same key. Since all matrix jobs are queued nearly simultaneously,
|
|
25
|
+
they race for the single concurrency slot and only the last-scheduled job survives.
|
|
26
|
+
All others silently show "This run was cancelled." with no further explanation.
|
|
27
|
+
|
|
28
|
+
Example problematic configuration:
|
|
29
|
+
concurrency:
|
|
30
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
31
|
+
cancel-in-progress: true
|
|
32
|
+
strategy:
|
|
33
|
+
matrix:
|
|
34
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
35
|
+
|
|
36
|
+
All three matrix jobs resolve to the same key, e.g., "test-refs/heads/main", and
|
|
37
|
+
cancel each other until only one remains.
|
|
38
|
+
|
|
39
|
+
Without `cancel-in-progress`, matrix jobs queue serially behind the single slot instead
|
|
40
|
+
of running in parallel, also incorrect and defeating the purpose of the matrix.
|
|
41
|
+
fix: |
|
|
42
|
+
Include at least one matrix dimension value in the concurrency group key so each matrix
|
|
43
|
+
job gets a unique group name and can run in parallel without cancelling each other.
|
|
44
|
+
|
|
45
|
+
Place `concurrency:` at the job level and reference the matrix variable:
|
|
46
|
+
|
|
47
|
+
jobs:
|
|
48
|
+
test:
|
|
49
|
+
concurrency:
|
|
50
|
+
group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.os }}
|
|
51
|
+
cancel-in-progress: true
|
|
52
|
+
|
|
53
|
+
If the goal is to cancel ALL previous matrix jobs when a new push arrives, apply
|
|
54
|
+
workflow-level concurrency with a key unique to the entire run (e.g., github.sha),
|
|
55
|
+
but note this serializes matrix sets rather than running jobs in parallel.
|
|
56
|
+
fix_code:
|
|
57
|
+
- language: yaml
|
|
58
|
+
label: "Job-level concurrency includes matrix.os — each OS runs in its own slot"
|
|
59
|
+
code: |
|
|
60
|
+
jobs:
|
|
61
|
+
test:
|
|
62
|
+
runs-on: ${{ matrix.os }}
|
|
63
|
+
concurrency:
|
|
64
|
+
group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.os }}
|
|
65
|
+
cancel-in-progress: true
|
|
66
|
+
strategy:
|
|
67
|
+
fail-fast: false
|
|
68
|
+
matrix:
|
|
69
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/checkout@v4
|
|
72
|
+
- run: npm ci && npm test
|
|
73
|
+
- language: yaml
|
|
74
|
+
label: "Multi-dimension matrix — include all axis values in group key"
|
|
75
|
+
code: |
|
|
76
|
+
jobs:
|
|
77
|
+
test:
|
|
78
|
+
runs-on: ubuntu-latest
|
|
79
|
+
concurrency:
|
|
80
|
+
group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.node }}-${{ matrix.os }}
|
|
81
|
+
cancel-in-progress: true
|
|
82
|
+
strategy:
|
|
83
|
+
fail-fast: false
|
|
84
|
+
matrix:
|
|
85
|
+
node: ['18', '20', '22']
|
|
86
|
+
os: [ubuntu-latest, windows-latest]
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/checkout@v4
|
|
89
|
+
- uses: actions/setup-node@v4
|
|
90
|
+
with:
|
|
91
|
+
node-version: ${{ matrix.node }}
|
|
92
|
+
- run: npm test
|
|
93
|
+
prevention:
|
|
94
|
+
- "Always include at least one matrix dimension variable in the concurrency group key when combining matrix strategy with concurrency"
|
|
95
|
+
- "Use job-level concurrency (under jobs.<name>:) rather than workflow-level when matrix is involved, for per-job slot control"
|
|
96
|
+
- "Review the Actions UI — if matrix jobs show 'This run was cancelled' without an explicit failure, check for concurrency group key collisions"
|
|
97
|
+
- "Run actionlint on your workflow files to catch common concurrency and expression issues"
|
|
98
|
+
docs:
|
|
99
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs"
|
|
100
|
+
label: "GitHub Docs — Controlling the concurrency of workflows and jobs"
|
|
101
|
+
- url: "https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs"
|
|
102
|
+
label: "GitHub Docs — Using a matrix for your jobs"
|
|
103
|
+
- url: "https://github.com/orgs/community/discussions/24616"
|
|
104
|
+
label: "GitHub Community — Matrix jobs cancelled unexpectedly with concurrency groups"
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
id: concurrency-timing-034
|
|
2
|
+
title: "Job-level cancel-in-progress only cancels that job — sibling jobs from the same run continue executing"
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- cancel-in-progress
|
|
8
|
+
- job-level
|
|
9
|
+
- workflow-level
|
|
10
|
+
- deployment
|
|
11
|
+
- silent-failure
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'This run was cancelled\.'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'cancel-in-progress.*true'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "This run was cancelled."
|
|
19
|
+
root_cause: |
|
|
20
|
+
When `concurrency:` is placed at the job level (under `jobs.<job-id>:`) rather than at
|
|
21
|
+
the workflow level (top-level key), cancellation scope is limited to that single job only.
|
|
22
|
+
Other jobs in the same workflow run — whether still queued, in-progress, or already
|
|
23
|
+
completed — are not affected by the job-level concurrency signal.
|
|
24
|
+
|
|
25
|
+
Developers commonly place `concurrency:` under a single job (e.g., `deploy`) while
|
|
26
|
+
expecting it to cancel the entire previous workflow run. The result is a dangerous race:
|
|
27
|
+
|
|
28
|
+
Run 1: build (succeeded) → deploy (running, concurrency group "deploy-prod")
|
|
29
|
+
Run 2: build (starts) → deploy (queued, same group)
|
|
30
|
+
|
|
31
|
+
Run 2's deploy job fires cancel-in-progress and cancels Run 1's deploy job — but only
|
|
32
|
+
if Run 1's deploy is still in-progress. If Run 1's build was slow and deploy hasn't
|
|
33
|
+
started yet, or if both runs' builds overlap, the serialization guarantee breaks down.
|
|
34
|
+
|
|
35
|
+
More critically: if `concurrency:` is absent from the build job, Run 1's build and
|
|
36
|
+
Run 2's build run simultaneously. Run 1's build (old code) may complete and trigger
|
|
37
|
+
Run 1's deploy AFTER Run 2's deploy has already deployed new code, producing a
|
|
38
|
+
rollback to old code with no warning.
|
|
39
|
+
|
|
40
|
+
Workflow-level concurrency cancels the ENTIRE run (all jobs). Job-level concurrency
|
|
41
|
+
cancels only THAT job. This distinction is frequently misunderstood.
|
|
42
|
+
fix: |
|
|
43
|
+
Place `concurrency:` at the workflow level (top-level) to cancel the entire previous
|
|
44
|
+
workflow run — all jobs — when a new run starts. Use job-level concurrency only when
|
|
45
|
+
you deliberately want different cancellation granularity per job.
|
|
46
|
+
|
|
47
|
+
For CI/CD where the entire pipeline should be replaced by the latest push:
|
|
48
|
+
concurrency:
|
|
49
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
50
|
+
cancel-in-progress: true
|
|
51
|
+
jobs:
|
|
52
|
+
build: ...
|
|
53
|
+
deploy: ...
|
|
54
|
+
|
|
55
|
+
For serializing deploys while allowing parallel builds:
|
|
56
|
+
jobs:
|
|
57
|
+
build:
|
|
58
|
+
# No concurrency key — builds run in parallel
|
|
59
|
+
deploy:
|
|
60
|
+
needs: build
|
|
61
|
+
concurrency:
|
|
62
|
+
group: deploy-${{ github.ref }}
|
|
63
|
+
cancel-in-progress: false # Queue, do not cancel in-progress deployments
|
|
64
|
+
fix_code:
|
|
65
|
+
- language: yaml
|
|
66
|
+
label: "Workflow-level concurrency cancels entire run including all jobs"
|
|
67
|
+
code: |
|
|
68
|
+
name: CI/CD Pipeline
|
|
69
|
+
|
|
70
|
+
on:
|
|
71
|
+
push:
|
|
72
|
+
branches: [main]
|
|
73
|
+
|
|
74
|
+
# Top-level: cancels ALL jobs in the previous workflow run on new push
|
|
75
|
+
concurrency:
|
|
76
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
77
|
+
cancel-in-progress: true
|
|
78
|
+
|
|
79
|
+
jobs:
|
|
80
|
+
build:
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
steps:
|
|
83
|
+
- uses: actions/checkout@v4
|
|
84
|
+
- run: npm ci && npm run build
|
|
85
|
+
|
|
86
|
+
deploy:
|
|
87
|
+
needs: build
|
|
88
|
+
runs-on: ubuntu-latest
|
|
89
|
+
environment: production
|
|
90
|
+
steps:
|
|
91
|
+
- run: ./scripts/deploy.sh
|
|
92
|
+
- language: yaml
|
|
93
|
+
label: "Job-level concurrency for deploy serialization only (intentional pattern)"
|
|
94
|
+
code: |
|
|
95
|
+
jobs:
|
|
96
|
+
build:
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
# No concurrency key — parallel builds are fine
|
|
99
|
+
steps:
|
|
100
|
+
- uses: actions/checkout@v4
|
|
101
|
+
- run: npm ci && npm run build
|
|
102
|
+
|
|
103
|
+
deploy:
|
|
104
|
+
needs: build
|
|
105
|
+
runs-on: ubuntu-latest
|
|
106
|
+
# Job-level: serialize deploys, but never cancel an in-progress deployment
|
|
107
|
+
concurrency:
|
|
108
|
+
group: deploy-${{ github.ref }}
|
|
109
|
+
cancel-in-progress: false
|
|
110
|
+
steps:
|
|
111
|
+
- run: ./scripts/deploy.sh
|
|
112
|
+
prevention:
|
|
113
|
+
- "Use workflow-level concurrency (top-level key, not under a job) when the entire pipeline should be replaced by the latest push"
|
|
114
|
+
- "Use job-level concurrency only when you intentionally want different cancellation behavior per job"
|
|
115
|
+
- "Set cancel-in-progress: false for deploy jobs — cancelling an in-progress deployment can leave infrastructure in a partially updated state"
|
|
116
|
+
- "Add concurrency to EVERY job in a pipeline if using job-level groups, not just the deploy job"
|
|
117
|
+
docs:
|
|
118
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs"
|
|
119
|
+
label: "GitHub Docs — Controlling the concurrency of workflows and jobs"
|
|
120
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#concurrency"
|
|
121
|
+
label: "GitHub Actions workflow syntax — concurrency"
|
|
122
|
+
- url: "https://github.com/orgs/community/discussions/57540"
|
|
123
|
+
label: "GitHub Community — Job-level vs workflow-level concurrency scoping"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
id: known-unsolved-038
|
|
2
|
+
title: "workflow_dispatch 'Run workflow' button invisible until workflow file merges to default branch"
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: limitation
|
|
5
|
+
tags:
|
|
6
|
+
- workflow-dispatch
|
|
7
|
+
- ui
|
|
8
|
+
- default-branch
|
|
9
|
+
- known-limitation
|
|
10
|
+
- feature-branch
|
|
11
|
+
- testing
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'run workflow.*not.*appear'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'workflow_dispatch.*button.*missing'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'workflow.*not.*present.*default.*branch'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- "This workflow has a workflow_dispatch event trigger, however a workflow_dispatch event workflow run cannot be created as this workflow is not present in the default branch."
|
|
21
|
+
root_cause: |
|
|
22
|
+
The "Run workflow" button in the GitHub Actions UI only appears when the workflow file
|
|
23
|
+
containing `on.workflow_dispatch` exists on the repository's default branch (e.g., main).
|
|
24
|
+
|
|
25
|
+
When you add a new `on.workflow_dispatch` workflow to a feature branch:
|
|
26
|
+
- The workflow file exists in the branch and can be referenced
|
|
27
|
+
- The "Run workflow" button does NOT appear in the GitHub Actions UI for that branch
|
|
28
|
+
- The GitHub REST API returns an error if you attempt to dispatch it
|
|
29
|
+
|
|
30
|
+
The REST API error message is:
|
|
31
|
+
"This workflow has a workflow_dispatch event trigger, however a workflow_dispatch event
|
|
32
|
+
workflow run cannot be created as this workflow is not present in the default branch."
|
|
33
|
+
|
|
34
|
+
This creates a catch-22: you want to validate the workflow before merging, but the trigger
|
|
35
|
+
mechanism (UI button and REST API dispatch) only activates after merging to the default branch.
|
|
36
|
+
|
|
37
|
+
The limitation applies consistently across:
|
|
38
|
+
- GitHub web UI "Run workflow" button
|
|
39
|
+
- REST API create_workflow_dispatch endpoint
|
|
40
|
+
- GitHub CLI: gh workflow run (requires workflow on default branch)
|
|
41
|
+
|
|
42
|
+
Contrast this with `on.push` and `on.pull_request` which fire correctly from any branch
|
|
43
|
+
that contains the workflow file.
|
|
44
|
+
fix: |
|
|
45
|
+
There is no way to trigger workflow_dispatch from the GitHub UI or standard API before
|
|
46
|
+
the workflow file reaches the default branch. Use these workarounds:
|
|
47
|
+
|
|
48
|
+
Option 1 (recommended): Add `on.push` temporarily during development alongside
|
|
49
|
+
`on.workflow_dispatch`. Push triggers fire from any branch. Remove push trigger before
|
|
50
|
+
merging to avoid unintended production runs.
|
|
51
|
+
|
|
52
|
+
Option 2: Use `act` (nektos/act) locally to simulate workflow_dispatch with custom inputs
|
|
53
|
+
before pushing to GitHub:
|
|
54
|
+
act workflow_dispatch -e event.json
|
|
55
|
+
|
|
56
|
+
Option 3: Merge the workflow file to a long-lived integration branch first, use it as a
|
|
57
|
+
staging default, then promote to main after validation.
|
|
58
|
+
|
|
59
|
+
Option 4: Commit the workflow directly to the default branch with a no-op body (echo "test"),
|
|
60
|
+
validate the dispatch mechanism works, then iterate in place on the default branch.
|
|
61
|
+
|
|
62
|
+
Note: Using gh api with a specific ref pointing to a branch that has the file does NOT
|
|
63
|
+
bypass the default-branch requirement for workflow_dispatch. The REST API enforces this
|
|
64
|
+
server-side regardless of ref parameter.
|
|
65
|
+
fix_code:
|
|
66
|
+
- language: yaml
|
|
67
|
+
label: "Temporary on.push alongside workflow_dispatch for feature-branch testing"
|
|
68
|
+
code: |
|
|
69
|
+
on:
|
|
70
|
+
# Production trigger (only works after merge to default branch)
|
|
71
|
+
workflow_dispatch:
|
|
72
|
+
inputs:
|
|
73
|
+
environment:
|
|
74
|
+
description: 'Target environment'
|
|
75
|
+
required: true
|
|
76
|
+
type: choice
|
|
77
|
+
options: [staging, production]
|
|
78
|
+
|
|
79
|
+
# DEVELOPMENT ONLY: allows testing from feature branch via push
|
|
80
|
+
# Remove this push trigger before merging to main
|
|
81
|
+
push:
|
|
82
|
+
branches: ['feature/my-new-dispatch-workflow']
|
|
83
|
+
|
|
84
|
+
jobs:
|
|
85
|
+
deploy:
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/checkout@v4
|
|
89
|
+
- name: Deploy
|
|
90
|
+
env:
|
|
91
|
+
TARGET: ${{ inputs.environment || 'staging' }}
|
|
92
|
+
run: ./scripts/deploy.sh "$TARGET"
|
|
93
|
+
- language: yaml
|
|
94
|
+
label: "Minimal no-op workflow — merge first, iterate after to unlock dispatch button"
|
|
95
|
+
code: |
|
|
96
|
+
# Step 1: Merge this minimal workflow to main first to unlock the Run Workflow button
|
|
97
|
+
# Step 2: Iterate on the job steps on main or in a branch
|
|
98
|
+
on:
|
|
99
|
+
workflow_dispatch:
|
|
100
|
+
inputs:
|
|
101
|
+
dry_run:
|
|
102
|
+
description: 'Dry run mode'
|
|
103
|
+
type: boolean
|
|
104
|
+
default: true
|
|
105
|
+
|
|
106
|
+
jobs:
|
|
107
|
+
placeholder:
|
|
108
|
+
runs-on: ubuntu-latest
|
|
109
|
+
steps:
|
|
110
|
+
- run: echo "Workflow dispatch confirmed working. Replace this with real steps."
|
|
111
|
+
prevention:
|
|
112
|
+
- "When creating new workflow_dispatch workflows, plan to merge a minimal skeleton to main first before adding full logic"
|
|
113
|
+
- "Add on.push with a branch filter during development of any workflow_dispatch workflow — remove before merging"
|
|
114
|
+
- "Use act locally (nektos/act) to test workflow_dispatch input handling before pushing"
|
|
115
|
+
- "Document in the PR description that the Run Workflow button will appear only after merge"
|
|
116
|
+
docs:
|
|
117
|
+
- url: "https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch"
|
|
118
|
+
label: "GitHub Docs — workflow_dispatch event"
|
|
119
|
+
- url: "https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#create-a-workflow-dispatch-event"
|
|
120
|
+
label: "GitHub REST API — Create a workflow dispatch event"
|
|
121
|
+
- url: "https://github.com/orgs/community/discussions/24357"
|
|
122
|
+
label: "GitHub Community — workflow_dispatch button not showing in feature branch"
|
|
123
|
+
- url: "https://github.com/nektos/act"
|
|
124
|
+
label: "nektos/act — Run Actions locally including workflow_dispatch simulation"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
id: known-unsolved-039
|
|
2
|
+
title: "on.schedule minimum interval is 5 minutes — sub-5-minute cron patterns silently run every 5 minutes"
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: limitation
|
|
5
|
+
tags:
|
|
6
|
+
- schedule
|
|
7
|
+
- cron
|
|
8
|
+
- on.schedule
|
|
9
|
+
- rate-limit
|
|
10
|
+
- interval
|
|
11
|
+
- minimum-frequency
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: '^\s*-\s*cron:\s*[''"]?\*\/[1-4]\s'
|
|
14
|
+
flags: 'm'
|
|
15
|
+
- regex: 'schedule.*cron.*every\s+(?:1|2|3|4)\s+minute'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "*/1 * * * *"
|
|
19
|
+
- "*/2 * * * *"
|
|
20
|
+
- "*/3 * * * *"
|
|
21
|
+
- "*/4 * * * *"
|
|
22
|
+
root_cause: |
|
|
23
|
+
GitHub Actions enforces a minimum cron schedule interval of 5 minutes. Cron
|
|
24
|
+
expressions that would trigger more frequently — such as */1, */2, */3, or */4
|
|
25
|
+
in the minutes field — are silently capped and run every 5 minutes instead of
|
|
26
|
+
the configured interval.
|
|
27
|
+
|
|
28
|
+
There is no validation error, no warning in workflow run logs, and no annotation
|
|
29
|
+
on the workflow YAML file. The workflow appears correctly configured in the GitHub
|
|
30
|
+
UI, but runs execute every 5 minutes regardless of the specified granularity.
|
|
31
|
+
|
|
32
|
+
This limitation is enforced server-side by GitHub's scheduler and cannot be
|
|
33
|
+
overridden via any workflow configuration. It applies to all hosted runners and
|
|
34
|
+
self-hosted runners triggered via GitHub's scheduler.
|
|
35
|
+
|
|
36
|
+
Additionally, even at the 5-minute minimum, scheduled workflows on busy
|
|
37
|
+
repositories may be delayed further during periods of high GitHub Actions load.
|
|
38
|
+
Under heavy load, schedules are queued and run as capacity allows — not
|
|
39
|
+
necessarily at the exact scheduled time. The schedule event documentation notes
|
|
40
|
+
that schedules may be delayed "up to hours" when load is high.
|
|
41
|
+
|
|
42
|
+
Common developer mental model mismatch: developers migrating from cron daemons
|
|
43
|
+
(systemd, cron, AWS EventBridge) where sub-minute schedules are routine expect
|
|
44
|
+
GitHub Actions to support the same granularity.
|
|
45
|
+
fix: |
|
|
46
|
+
Use a minimum interval of 5 minutes in your cron expression — replace */1,
|
|
47
|
+
*/2, */3, or */4 in the minutes field with */5.
|
|
48
|
+
|
|
49
|
+
If sub-5-minute polling or execution is required, use an external trigger
|
|
50
|
+
mechanism instead of on.schedule:
|
|
51
|
+
|
|
52
|
+
- External scheduler (AWS EventBridge, Azure Logic Apps, GCP Cloud Scheduler)
|
|
53
|
+
that calls the workflow_dispatch REST API
|
|
54
|
+
- A self-hosted runner with a local cron daemon (systemd timer, Linux cron)
|
|
55
|
+
that invokes the GitHub API to dispatch the workflow
|
|
56
|
+
- Move the time-sensitive logic out of GitHub Actions entirely and run it
|
|
57
|
+
in a dedicated service that can be triggered at any frequency
|
|
58
|
+
fix_code:
|
|
59
|
+
- language: yaml
|
|
60
|
+
label: "Minimum supported interval — every 5 minutes"
|
|
61
|
+
code: |
|
|
62
|
+
on:
|
|
63
|
+
schedule:
|
|
64
|
+
# Minimum supported interval is every 5 minutes
|
|
65
|
+
- cron: '*/5 * * * *'
|
|
66
|
+
# The following are silently capped to every 5 minutes:
|
|
67
|
+
# - cron: '*/1 * * * *'
|
|
68
|
+
# - cron: '*/2 * * * *'
|
|
69
|
+
# - cron: '*/3 * * * *'
|
|
70
|
+
# - cron: '*/4 * * * *'
|
|
71
|
+
- language: yaml
|
|
72
|
+
label: "workflow_dispatch trigger for external scheduler integration"
|
|
73
|
+
code: |
|
|
74
|
+
# Trigger via REST API from an external scheduler at any frequency:
|
|
75
|
+
# POST https://api.github.com/repos/OWNER/REPO/actions/workflows/WORKFLOW.yml/dispatches
|
|
76
|
+
# Body: {"ref": "main"}
|
|
77
|
+
on:
|
|
78
|
+
workflow_dispatch:
|
|
79
|
+
inputs:
|
|
80
|
+
triggered_by:
|
|
81
|
+
description: 'Trigger source identifier'
|
|
82
|
+
type: string
|
|
83
|
+
default: 'external-scheduler'
|
|
84
|
+
|
|
85
|
+
jobs:
|
|
86
|
+
scheduled-task:
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
steps:
|
|
89
|
+
- name: Run task
|
|
90
|
+
run: echo "Triggered by ${{ inputs.triggered_by }}"
|
|
91
|
+
prevention:
|
|
92
|
+
- "Always use */5 or higher in the cron minutes field for on.schedule expressions"
|
|
93
|
+
- "Add a comment next to every cron expression documenting the intended frequency and the 5-minute minimum"
|
|
94
|
+
- "Test schedule behavior by temporarily adding an on.push trigger during development rather than waiting for scheduled runs"
|
|
95
|
+
- "For sub-5-minute needs, design the solution with an external scheduler calling workflow_dispatch from the start — do not plan to reduce the interval later"
|
|
96
|
+
docs:
|
|
97
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule"
|
|
98
|
+
label: "GitHub Docs — Schedule event (on.schedule)"
|
|
99
|
+
- url: "https://stackoverflow.com/questions/63612471/run-github-actions-every-x-minutes-cron-job"
|
|
100
|
+
label: "Stack Overflow — Run GitHub Actions every X minutes (highly voted)"
|
|
101
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule"
|
|
102
|
+
label: "GitHub Docs — Note on schedule delays under high load"
|