@htekdev/actions-debugger 1.0.56 → 1.0.58
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/caching-artifacts/caching-artifacts-040.yml +112 -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/permissions-auth/permissions-auth-042.yml +125 -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/runner-environment/runner-environment-118.yml +102 -0
- package/errors/silent-failures/silent-failures-057.yml +120 -0
- package/errors/silent-failures/silent-failures-058.yml +126 -0
- package/errors/silent-failures/silent-failures-059.yml +104 -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,112 @@
|
|
|
1
|
+
id: caching-artifacts-040
|
|
2
|
+
title: "actions/download-artifact@v4 cross-workflow download silently returns no artifacts without actions: read permission"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- download-artifact
|
|
7
|
+
- v4
|
|
8
|
+
- cross-workflow
|
|
9
|
+
- permissions
|
|
10
|
+
- actions-read
|
|
11
|
+
- run-id
|
|
12
|
+
- breaking-change
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'Unable to find any artifacts for the associated workflow'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'No artifacts found for the associated workflow run'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
- regex: 'run-id:.*\d+'
|
|
19
|
+
flags: 'i'
|
|
20
|
+
- regex: 'Resource not accessible by integration'
|
|
21
|
+
flags: 'i'
|
|
22
|
+
error_messages:
|
|
23
|
+
- "Unable to find any artifacts for the associated workflow"
|
|
24
|
+
- "No artifacts found for the associated workflow run"
|
|
25
|
+
- "Resource not accessible by integration"
|
|
26
|
+
- "Error: Artifact download failed: 403 Forbidden"
|
|
27
|
+
root_cause: |
|
|
28
|
+
actions/download-artifact@v4 introduced the ability to download artifacts produced
|
|
29
|
+
by a *different* workflow run (not just the current run) by specifying the `run-id`
|
|
30
|
+
input. This cross-workflow download requires the `actions: read` permission on the
|
|
31
|
+
GITHUB_TOKEN.
|
|
32
|
+
|
|
33
|
+
Workflows that do not explicitly declare `permissions: actions: read` will use the
|
|
34
|
+
default GITHUB_TOKEN permissions. In repositories where the default token permissions
|
|
35
|
+
are set to "read for all" at the org level, `actions` read may be granted by default
|
|
36
|
+
— but in repositories with restrictive default permissions or when only specific
|
|
37
|
+
permissions are declared in the workflow, `actions: read` is NOT automatically
|
|
38
|
+
included.
|
|
39
|
+
|
|
40
|
+
The misleading aspect is the error message: "Unable to find any artifacts for the
|
|
41
|
+
associated workflow" suggests the artifact does not exist, when in fact the issue is
|
|
42
|
+
a 403 permission denial. The action does not distinguish between "artifact not found"
|
|
43
|
+
and "access denied" in its error output.
|
|
44
|
+
|
|
45
|
+
This is a new permission requirement introduced in v4 that did not exist in v3
|
|
46
|
+
(which only supported downloading from the current workflow run and did not need
|
|
47
|
+
`actions` read access).
|
|
48
|
+
|
|
49
|
+
Common trigger scenario: a workflow that processes artifacts from a different trigger
|
|
50
|
+
(e.g., a deployment workflow that downloads build artifacts from a build workflow run)
|
|
51
|
+
is upgraded from download-artifact@v3 to @v4 and the `run-id` input is added — but
|
|
52
|
+
the required `permissions: actions: read` block is not added.
|
|
53
|
+
fix: |
|
|
54
|
+
Add `actions: read` to the permissions block of the job or workflow that uses
|
|
55
|
+
`actions/download-artifact@v4` with a `run-id` input referencing a different workflow.
|
|
56
|
+
|
|
57
|
+
If the workflow or job already has a `permissions` block, add `actions: read` to it.
|
|
58
|
+
If there is no `permissions` block, add one with the minimum required permissions
|
|
59
|
+
including `actions: read` and `contents: read`.
|
|
60
|
+
fix_code:
|
|
61
|
+
- language: yaml
|
|
62
|
+
label: "Add actions: read permission for cross-workflow artifact download"
|
|
63
|
+
code: |
|
|
64
|
+
jobs:
|
|
65
|
+
deploy:
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
permissions:
|
|
68
|
+
actions: read # Required for download-artifact@v4 with run-id
|
|
69
|
+
contents: read
|
|
70
|
+
steps:
|
|
71
|
+
- name: Download build artifacts from build workflow
|
|
72
|
+
uses: actions/download-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
name: build-output
|
|
75
|
+
run-id: ${{ github.event.inputs.build_run_id }}
|
|
76
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
77
|
+
- language: yaml
|
|
78
|
+
label: "Workflow-level permissions for cross-workflow download"
|
|
79
|
+
code: |
|
|
80
|
+
on:
|
|
81
|
+
workflow_dispatch:
|
|
82
|
+
inputs:
|
|
83
|
+
build_run_id:
|
|
84
|
+
description: 'Run ID of the build workflow'
|
|
85
|
+
required: true
|
|
86
|
+
type: string
|
|
87
|
+
|
|
88
|
+
permissions:
|
|
89
|
+
actions: read
|
|
90
|
+
contents: read
|
|
91
|
+
|
|
92
|
+
jobs:
|
|
93
|
+
deploy:
|
|
94
|
+
runs-on: ubuntu-latest
|
|
95
|
+
steps:
|
|
96
|
+
- uses: actions/download-artifact@v4
|
|
97
|
+
with:
|
|
98
|
+
name: dist
|
|
99
|
+
run-id: ${{ inputs.build_run_id }}
|
|
100
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
101
|
+
prevention:
|
|
102
|
+
- "Whenever `run-id` is added to a download-artifact@v4 step, immediately add `actions: read` to the job permissions block"
|
|
103
|
+
- "Do not rely on default token permissions for cross-workflow operations — always declare explicit permission blocks"
|
|
104
|
+
- "Test cross-workflow downloads in a feature branch before merging — the permission error is deterministic, not flaky"
|
|
105
|
+
- "Add `github-token: ${{ secrets.GITHUB_TOKEN }}` explicitly to download-artifact@v4 steps that use run-id, as it clarifies the token in use"
|
|
106
|
+
docs:
|
|
107
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/storing-workflow-data-as-artifacts#downloading-artifacts-from-a-previous-workflow-run"
|
|
108
|
+
label: "GitHub Docs — Downloading artifacts from a previous workflow run"
|
|
109
|
+
- url: "https://github.com/actions/download-artifact/releases/tag/v4.0.0"
|
|
110
|
+
label: "actions/download-artifact v4.0.0 release notes — cross-workflow download"
|
|
111
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token#defining-access-for-the-github_token-scopes"
|
|
112
|
+
label: "GitHub Docs — GITHUB_TOKEN permission scopes"
|
|
@@ -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"
|