@htekdev/actions-debugger 1.0.40 → 1.0.42
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/setup-node-npm-cache-monorepo-lockfile-not-found.yml +118 -0
- package/errors/concurrency-timing/cancel-in-progress-queued-run-status-never-posts.yml +111 -0
- package/errors/concurrency-timing/step-timeout-not-supported-job-holds-runner.yml +101 -0
- package/errors/concurrency-timing/workflow-dispatch-push-shared-concurrency-silent-cancel.yml +104 -0
- package/errors/runner-environment/runner-workspace-vs-github-workspace-parent-directory.yml +100 -0
- package/errors/silent-failures/actions-runner-debug-wrong-secret-name.yml +90 -0
- package/errors/silent-failures/github-env-vars-not-shared-across-jobs.yml +96 -0
- package/errors/silent-failures/matrix-exclude-value-mismatch-silently-ignored.yml +96 -0
- package/package.json +1 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
id: caching-artifacts-032
|
|
2
|
+
title: 'setup-node cache: npm silently skips caching in monorepos — lockfile not at workspace root'
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- setup-node
|
|
7
|
+
- npm
|
|
8
|
+
- cache
|
|
9
|
+
- monorepo
|
|
10
|
+
- lockfile
|
|
11
|
+
- cache-dependency-path
|
|
12
|
+
- silent-failure
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: "Warning: No file found for: package-lock\\.json"
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'No file found for.*No cache will be (saved|restored)'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
- regex: 'cache-dependency-path.*not found|No lockfile found'
|
|
19
|
+
flags: 'i'
|
|
20
|
+
error_messages:
|
|
21
|
+
- "Warning: No file found for: package-lock.json"
|
|
22
|
+
- "Warning: No file found for: yarn.lock"
|
|
23
|
+
- "Warning: No file found for: pnpm-lock.yaml"
|
|
24
|
+
- "No file found for: package-lock.json. No cache will be saved."
|
|
25
|
+
root_cause: |
|
|
26
|
+
actions/setup-node with cache: 'npm' (or 'yarn' / 'pnpm') searches for the
|
|
27
|
+
package manager lockfile at the workspace root ($GITHUB_WORKSPACE) by default.
|
|
28
|
+
|
|
29
|
+
In monorepos where the lockfile lives in a subdirectory (apps/frontend/,
|
|
30
|
+
packages/api/, etc.), or when the workflow uses working-directory: to change
|
|
31
|
+
the build context, setup-node logs a warning and continues WITHOUT configuring
|
|
32
|
+
a cache. The step exits 0, making this a silent failure.
|
|
33
|
+
|
|
34
|
+
The result: npm ci (or yarn install / pnpm install) downloads all dependencies
|
|
35
|
+
from the network on every workflow run, as though no cache were configured.
|
|
36
|
+
Build times are identical to runs with no cache setting at all.
|
|
37
|
+
|
|
38
|
+
The warning message ("No file found for: package-lock.json") is easily missed
|
|
39
|
+
in long step logs and does not fail the step, so developers often go weeks
|
|
40
|
+
without noticing the cache was never active.
|
|
41
|
+
|
|
42
|
+
The same behavior applies when:
|
|
43
|
+
- yarn.lock is not at workspace root
|
|
44
|
+
- pnpm-lock.yaml is not at workspace root
|
|
45
|
+
- Multiple lockfiles exist across packages (only the first match is used
|
|
46
|
+
unless cache-dependency-path is explicitly set to a glob)
|
|
47
|
+
fix: |
|
|
48
|
+
Use the cache-dependency-path input to specify the path to the lockfile
|
|
49
|
+
relative to the workspace root. Supports glob patterns for monorepos.
|
|
50
|
+
|
|
51
|
+
Single package in subdirectory:
|
|
52
|
+
cache-dependency-path: 'frontend/package-lock.json'
|
|
53
|
+
|
|
54
|
+
Multiple lockfiles across a monorepo:
|
|
55
|
+
cache-dependency-path: '**/package-lock.json'
|
|
56
|
+
|
|
57
|
+
Using a glob creates a combined cache key from all matched lockfiles. Any
|
|
58
|
+
change to any package lockfile invalidates the shared cache — this is
|
|
59
|
+
correct behavior for a monorepo where cross-package installs are common.
|
|
60
|
+
fix_code:
|
|
61
|
+
- language: yaml
|
|
62
|
+
label: 'Single package in subdirectory — point cache-dependency-path at lockfile'
|
|
63
|
+
code: |
|
|
64
|
+
jobs:
|
|
65
|
+
build:
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
|
|
70
|
+
- uses: actions/setup-node@v4
|
|
71
|
+
with:
|
|
72
|
+
node-version: '22'
|
|
73
|
+
cache: 'npm'
|
|
74
|
+
# WRONG (omitted): setup-node looks at $GITHUB_WORKSPACE/package-lock.json
|
|
75
|
+
# and silently skips caching when not found there
|
|
76
|
+
|
|
77
|
+
# CORRECT: path relative to $GITHUB_WORKSPACE
|
|
78
|
+
cache-dependency-path: 'frontend/package-lock.json'
|
|
79
|
+
|
|
80
|
+
- name: Install dependencies
|
|
81
|
+
working-directory: frontend
|
|
82
|
+
run: npm ci
|
|
83
|
+
- language: yaml
|
|
84
|
+
label: 'Monorepo — cache all packages using glob pattern'
|
|
85
|
+
code: |
|
|
86
|
+
jobs:
|
|
87
|
+
build:
|
|
88
|
+
runs-on: ubuntu-latest
|
|
89
|
+
steps:
|
|
90
|
+
- uses: actions/checkout@v4
|
|
91
|
+
|
|
92
|
+
- uses: actions/setup-node@v4
|
|
93
|
+
with:
|
|
94
|
+
node-version: '22'
|
|
95
|
+
cache: 'npm'
|
|
96
|
+
# Glob matches all package-lock.json files anywhere in the repo
|
|
97
|
+
# Combined hash from all matched lockfiles forms the cache key
|
|
98
|
+
cache-dependency-path: '**/package-lock.json'
|
|
99
|
+
|
|
100
|
+
- name: Install root dependencies
|
|
101
|
+
run: npm ci
|
|
102
|
+
|
|
103
|
+
- name: Install frontend dependencies
|
|
104
|
+
working-directory: packages/frontend
|
|
105
|
+
run: npm ci
|
|
106
|
+
prevention:
|
|
107
|
+
- 'Always set cache-dependency-path when the lockfile is not in the repository root'
|
|
108
|
+
- 'Use **/package-lock.json glob in monorepos to cover all packages with a single cache configuration'
|
|
109
|
+
- 'Verify caching is active by checking setup-node logs for "Cache restored successfully" or "Cache saved"'
|
|
110
|
+
- 'Check for "No file found for: package-lock.json" warnings as early signal that caching is silently disabled'
|
|
111
|
+
- 'When using working-directory: on install steps, ensure cache-dependency-path is also adjusted to match'
|
|
112
|
+
docs:
|
|
113
|
+
- url: 'https://github.com/actions/setup-node#caching-global-packages-data'
|
|
114
|
+
label: 'actions/setup-node: Caching global packages data — cache-dependency-path input'
|
|
115
|
+
- url: 'https://github.com/actions/setup-node/issues/530'
|
|
116
|
+
label: 'actions/setup-node#530: cache: npm silently skips in monorepos without cache-dependency-path'
|
|
117
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows'
|
|
118
|
+
label: 'GitHub Docs: Caching dependencies to speed up workflows — lockfile path configuration'
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
id: concurrency-timing-027
|
|
2
|
+
title: 'Queued run cancelled by cancel-in-progress before any job starts — required status check never posts, PR permanently blocked'
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- cancel-in-progress
|
|
8
|
+
- required-status-check
|
|
9
|
+
- branch-protection
|
|
10
|
+
- pr-blocked
|
|
11
|
+
- status-check
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'This run was cancelled because another run in the same concurrency group'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'Waiting for.*status.*reported|Expected.*Waiting'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "This run was cancelled because another run in the same concurrency group is in progress."
|
|
19
|
+
- "Waiting for status to be reported"
|
|
20
|
+
root_cause: |
|
|
21
|
+
When cancel-in-progress: true cancels a workflow run that was queued but had
|
|
22
|
+
not yet started any job, GitHub does NOT post a status (pending, cancelled, or
|
|
23
|
+
failure) to the commit SHA. The run silently disappears from the run list.
|
|
24
|
+
|
|
25
|
+
Branch protection rules that require a specific status check (e.g., "CI / test")
|
|
26
|
+
only observe statuses that were posted. A run cancelled before its first job
|
|
27
|
+
starts never posts any status — not even "pending".
|
|
28
|
+
|
|
29
|
+
Impact on PRs with high push frequency:
|
|
30
|
+
1. Developer opens PR, pushes commit A — run starts, posts "pending"
|
|
31
|
+
2. Developer pushes fix commit B — run for A is cancelled, run for B is queued
|
|
32
|
+
3. Developer pushes commit C before B's run starts — B's run cancelled (never
|
|
33
|
+
started), run for C queued
|
|
34
|
+
4. Run for C finally starts and posts statuses
|
|
35
|
+
5. But if C is also cancelled before starting, the commit has NO status
|
|
36
|
+
6. Branch protection shows the required check as "Expected" forever
|
|
37
|
+
7. PR cannot be merged — the Merge button stays disabled indefinitely
|
|
38
|
+
|
|
39
|
+
This condition is self-healing if a new commit is pushed (starting a fresh
|
|
40
|
+
run that won't be cancelled), but in rapid-push scenarios the window persists
|
|
41
|
+
for many minutes and developers mistakenly believe the CI is broken.
|
|
42
|
+
fix: |
|
|
43
|
+
Option 1 — Queue instead of cancel (safest):
|
|
44
|
+
concurrency:
|
|
45
|
+
group: "${{ github.workflow }}-${{ github.ref }}"
|
|
46
|
+
cancel-in-progress: false
|
|
47
|
+
|
|
48
|
+
Runs queue behind one another; every commit eventually gets a status posted.
|
|
49
|
+
|
|
50
|
+
Option 2 — Status anchor job:
|
|
51
|
+
Add a minimal first job that completes instantly. Its "queued" + "in_progress"
|
|
52
|
+
status is posted to the commit SHA immediately, ensuring GitHub registers the
|
|
53
|
+
run before any cancellation can remove the status.
|
|
54
|
+
|
|
55
|
+
Option 3 — GitHub Actions recommended pattern:
|
|
56
|
+
Use cancel-in-progress: true for the expensive test jobs only, and have a
|
|
57
|
+
separate fast-posting job that always runs (not subject to concurrency group).
|
|
58
|
+
fix_code:
|
|
59
|
+
- language: yaml
|
|
60
|
+
label: 'Queue runs instead of cancelling — every commit gets a status'
|
|
61
|
+
code: |
|
|
62
|
+
on:
|
|
63
|
+
pull_request:
|
|
64
|
+
|
|
65
|
+
concurrency:
|
|
66
|
+
# Queue instead of cancel — latest run waits, but always posts status
|
|
67
|
+
group: '${{ github.workflow }}-${{ github.ref }}'
|
|
68
|
+
cancel-in-progress: false
|
|
69
|
+
|
|
70
|
+
jobs:
|
|
71
|
+
test:
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/checkout@v4
|
|
75
|
+
- run: npm test
|
|
76
|
+
- language: yaml
|
|
77
|
+
label: 'Status anchor job — posts status immediately before cancel window'
|
|
78
|
+
code: |
|
|
79
|
+
on:
|
|
80
|
+
pull_request:
|
|
81
|
+
|
|
82
|
+
concurrency:
|
|
83
|
+
group: '${{ github.workflow }}-${{ github.ref }}'
|
|
84
|
+
cancel-in-progress: true # OK — anchor ensures status is posted first
|
|
85
|
+
|
|
86
|
+
jobs:
|
|
87
|
+
# Minimal job: completes in seconds, ensuring a status is recorded on the SHA
|
|
88
|
+
anchor:
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
steps:
|
|
91
|
+
- run: echo "CI registered for ${{ github.sha }}"
|
|
92
|
+
|
|
93
|
+
# Expensive test job that is safe to cancel
|
|
94
|
+
test:
|
|
95
|
+
needs: anchor
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
steps:
|
|
98
|
+
- uses: actions/checkout@v4
|
|
99
|
+
- run: npm test
|
|
100
|
+
prevention:
|
|
101
|
+
- 'Avoid cancel-in-progress: true on workflows whose jobs are required status checks for branch protection'
|
|
102
|
+
- 'Use cancel-in-progress: false with queuing semantics when PR mergeability must be preserved on every commit'
|
|
103
|
+
- 'Add a lightweight anchor job as the first required job to ensure a status posts before any cancel window closes'
|
|
104
|
+
- 'Monitor PRs stuck with Expected required checks — check whether recent commits had all their runs cancelled before starting'
|
|
105
|
+
docs:
|
|
106
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/using-concurrency'
|
|
107
|
+
label: 'GitHub Actions: Using concurrency — cancel-in-progress behavior'
|
|
108
|
+
- url: 'https://github.com/orgs/community/discussions/21280'
|
|
109
|
+
label: 'GitHub Community: Required status check never posts when run cancelled before start (120+ reactions)'
|
|
110
|
+
- url: 'https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-status-checks-before-merging'
|
|
111
|
+
label: 'GitHub Docs: Required status checks — how commit statuses are evaluated for branch protection'
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
id: concurrency-timing-025
|
|
2
|
+
title: 'No step-level timeout — a hung step holds the runner slot for up to 6 hours'
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: limitation
|
|
5
|
+
tags:
|
|
6
|
+
- timeout
|
|
7
|
+
- hung-step
|
|
8
|
+
- runner-slot
|
|
9
|
+
- step
|
|
10
|
+
- limitation
|
|
11
|
+
- self-hosted-runner
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'The runner has received a shutdown signal'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'Error: The process.*timed out after \d+ minutes'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'Canceling since the workflow was cancelled'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- "The runner has received a shutdown signal. This can happen when the runner service is stopped, or a manually started runner is canceled."
|
|
21
|
+
- "Error: The process '/usr/bin/bash' failed with exit code 124"
|
|
22
|
+
- "Canceling since the workflow was cancelled."
|
|
23
|
+
root_cause: |
|
|
24
|
+
GitHub Actions supports timeout-minutes at the job level only. There is no
|
|
25
|
+
per-step timeout. A single run: step that hangs (network call blocked, test
|
|
26
|
+
suite deadlocked, subprocess waiting on stdin) holds the entire job until
|
|
27
|
+
the job-level timeout fires.
|
|
28
|
+
|
|
29
|
+
The default job timeout is 6 hours (360 minutes) for GitHub-hosted runners
|
|
30
|
+
and 35 days (unlimited in practice) for self-hosted runners. A single hung
|
|
31
|
+
step therefore silently consumes 6 hours of runner minutes and one complete
|
|
32
|
+
runner slot before GitHub kills the job.
|
|
33
|
+
|
|
34
|
+
Runner slot starvation is the secondary effect: while the hung job occupies a
|
|
35
|
+
runner, queued jobs wait. On self-hosted runners with limited capacity one
|
|
36
|
+
hung step can block an entire team's CI queue indefinitely.
|
|
37
|
+
|
|
38
|
+
This is a documented platform limitation tracked in actions/runner#1120
|
|
39
|
+
(220+ reactions, open since 2020) with no scheduled fix date.
|
|
40
|
+
fix: |
|
|
41
|
+
Apply one of these workarounds depending on operating system:
|
|
42
|
+
|
|
43
|
+
Linux/macOS: Wrap the command with the system timeout utility (seconds):
|
|
44
|
+
run: timeout 300 ./integration-test.sh
|
|
45
|
+
|
|
46
|
+
Cross-platform: Use curl/Invoke-RestMethod built-in timeout options for
|
|
47
|
+
network calls, and test-runner native timeouts for test suites.
|
|
48
|
+
|
|
49
|
+
Always set timeout-minutes explicitly on every job to establish a hard upper
|
|
50
|
+
bound regardless of which step hangs.
|
|
51
|
+
fix_code:
|
|
52
|
+
- language: yaml
|
|
53
|
+
label: 'Set explicit job timeout and use OS timeout for individual steps'
|
|
54
|
+
code: |
|
|
55
|
+
jobs:
|
|
56
|
+
build:
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
timeout-minutes: 30 # explicit ceiling — never rely on 6h default
|
|
59
|
+
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/checkout@v4
|
|
62
|
+
|
|
63
|
+
# Linux/macOS: system timeout (in seconds)
|
|
64
|
+
- name: Run integration tests
|
|
65
|
+
run: timeout 180 ./scripts/integration-test.sh
|
|
66
|
+
|
|
67
|
+
# Network call with built-in timeout
|
|
68
|
+
- name: Fetch external resource
|
|
69
|
+
run: |
|
|
70
|
+
curl --max-time 60 --retry 3 https://api.example.com/data -o data.json
|
|
71
|
+
- language: yaml
|
|
72
|
+
label: 'Self-hosted runner — conservative timeout prevents slot starvation'
|
|
73
|
+
code: |
|
|
74
|
+
jobs:
|
|
75
|
+
deploy:
|
|
76
|
+
runs-on: [self-hosted, production]
|
|
77
|
+
timeout-minutes: 45 # critical on self-hosted — hung jobs block all runners
|
|
78
|
+
|
|
79
|
+
steps:
|
|
80
|
+
- name: Deploy with timeout guard
|
|
81
|
+
shell: pwsh
|
|
82
|
+
run: |
|
|
83
|
+
$job = Start-Job { ./deploy.ps1 }
|
|
84
|
+
if (-not (Wait-Job $job -Timeout 120)) {
|
|
85
|
+
Stop-Job $job
|
|
86
|
+
throw "Deploy script timed out after 120s"
|
|
87
|
+
}
|
|
88
|
+
Receive-Job $job
|
|
89
|
+
prevention:
|
|
90
|
+
- 'Always set explicit timeout-minutes on every job — never rely on the 6-hour GitHub-hosted default'
|
|
91
|
+
- 'Use OS-level timeout utilities (timeout on Linux/macOS) for individual network calls and scripts'
|
|
92
|
+
- 'Configure test-runner-native timeouts for test suites — they are more granular than job timeouts'
|
|
93
|
+
- 'Monitor runner queue depth on self-hosted runners — sudden growth often signals a hung step holding a slot'
|
|
94
|
+
- 'Set timeout-minutes: 5 on jobs you know should complete quickly to catch regressions early'
|
|
95
|
+
docs:
|
|
96
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes'
|
|
97
|
+
label: 'GitHub Actions: jobs.<id>.timeout-minutes — job-level timeout (no step-level equivalent)'
|
|
98
|
+
- url: 'https://github.com/actions/runner/issues/1120'
|
|
99
|
+
label: 'actions/runner#1120: Feature request — step-level timeout (220+ reactions, open since 2020)'
|
|
100
|
+
- url: 'https://docs.github.com/en/actions/administering-github-actions/usage-limits-billing-and-administration#usage-limits'
|
|
101
|
+
label: 'GitHub Actions usage limits — default job timeout values per runner type'
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
id: concurrency-timing-026
|
|
2
|
+
title: 'workflow_dispatch run silently cancelled when push to same branch shares the concurrency group'
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- workflow-dispatch
|
|
8
|
+
- cancel-in-progress
|
|
9
|
+
- push
|
|
10
|
+
- manual-trigger
|
|
11
|
+
- silent-cancel
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'This run was cancelled because another run in the same concurrency group'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'Canceling run due to.*concurrency group'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'This run has been canceled'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- "This run was cancelled because another run in the same concurrency group is in progress."
|
|
21
|
+
- "Canceling run due to a newer request for the same concurrency group."
|
|
22
|
+
- "This run has been canceled."
|
|
23
|
+
root_cause: |
|
|
24
|
+
A common concurrency pattern groups runs by workflow name and ref:
|
|
25
|
+
|
|
26
|
+
concurrency:
|
|
27
|
+
group: "${{ github.workflow }}-${{ github.ref }}"
|
|
28
|
+
cancel-in-progress: true
|
|
29
|
+
|
|
30
|
+
workflow_dispatch and push events on the same branch share identical
|
|
31
|
+
github.workflow and github.ref values, so they are placed in the same
|
|
32
|
+
concurrency slot.
|
|
33
|
+
|
|
34
|
+
A developer manually triggers a workflow_dispatch run (e.g., to deploy to
|
|
35
|
+
staging, run a migration, or kick off a release). While it is running, any
|
|
36
|
+
push to that branch — including a small documentation fix or revert — creates
|
|
37
|
+
a new run in the same concurrency slot and immediately cancels the in-progress
|
|
38
|
+
manual dispatch with no notification.
|
|
39
|
+
|
|
40
|
+
The developer discovers this only when checking on the deployment minutes
|
|
41
|
+
later to find it was cancelled mid-run. The automatic push run that replaced
|
|
42
|
+
it may be completely irrelevant to the manual operation.
|
|
43
|
+
fix: |
|
|
44
|
+
Include github.event_name in the concurrency group key to give workflow_dispatch
|
|
45
|
+
and push events separate concurrency slots:
|
|
46
|
+
|
|
47
|
+
concurrency:
|
|
48
|
+
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}"
|
|
49
|
+
cancel-in-progress: true
|
|
50
|
+
|
|
51
|
+
workflow_dispatch runs now share a slot only with other workflow_dispatch runs
|
|
52
|
+
on the same branch, and push runs share a slot only with other push runs.
|
|
53
|
+
|
|
54
|
+
For deployment or migration workflows where even manual-vs-manual cancellation
|
|
55
|
+
is undesirable, use cancel-in-progress: false and let runs queue.
|
|
56
|
+
fix_code:
|
|
57
|
+
- language: yaml
|
|
58
|
+
label: 'Include event_name in concurrency group — isolates manual from automatic triggers'
|
|
59
|
+
code: |
|
|
60
|
+
on:
|
|
61
|
+
push:
|
|
62
|
+
branches: [main]
|
|
63
|
+
workflow_dispatch:
|
|
64
|
+
|
|
65
|
+
concurrency:
|
|
66
|
+
# event_name isolates push and workflow_dispatch into separate concurrency slots
|
|
67
|
+
group: '${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name }}'
|
|
68
|
+
cancel-in-progress: true
|
|
69
|
+
|
|
70
|
+
jobs:
|
|
71
|
+
deploy:
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/checkout@v4
|
|
75
|
+
- run: ./deploy.sh
|
|
76
|
+
- language: yaml
|
|
77
|
+
label: 'Separate workflow file for manual operations — no cancel-in-progress'
|
|
78
|
+
code: |
|
|
79
|
+
# deploy-manual.yml — only triggered by workflow_dispatch
|
|
80
|
+
on:
|
|
81
|
+
workflow_dispatch:
|
|
82
|
+
inputs:
|
|
83
|
+
environment:
|
|
84
|
+
description: 'Target environment'
|
|
85
|
+
required: true
|
|
86
|
+
type: choice
|
|
87
|
+
options: [staging, production]
|
|
88
|
+
|
|
89
|
+
# No shared concurrency group with push workflows
|
|
90
|
+
concurrency:
|
|
91
|
+
group: 'manual-deploy-${{ github.event.inputs.environment }}'
|
|
92
|
+
# cancel-in-progress defaults to false — manual deploys queue, not cancel
|
|
93
|
+
prevention:
|
|
94
|
+
- 'Always include github.event_name in concurrency group keys for workflows triggered by both push and workflow_dispatch'
|
|
95
|
+
- 'Use separate workflow files for manual deployment operations to avoid shared concurrency with automated triggers'
|
|
96
|
+
- 'Add a summary step or Telegram/Slack notification in the cleanup phase to alert when a run is cancelled mid-execution'
|
|
97
|
+
- 'Audit all concurrency group patterns when adding workflow_dispatch to an existing automated workflow'
|
|
98
|
+
docs:
|
|
99
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/using-concurrency'
|
|
100
|
+
label: 'GitHub Actions: Using concurrency — group patterns and cancel-in-progress'
|
|
101
|
+
- url: 'https://github.com/orgs/community/discussions/5435'
|
|
102
|
+
label: 'GitHub Community: workflow_dispatch cancelled by push with same concurrency group'
|
|
103
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#github-context'
|
|
104
|
+
label: 'GitHub context: github.event_name property'
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
id: runner-environment-098
|
|
2
|
+
title: "runner.workspace is the parent of the repo root — not the checked-out repo"
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- runner.workspace
|
|
7
|
+
- github.workspace
|
|
8
|
+
- path
|
|
9
|
+
- working-directory
|
|
10
|
+
- context
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'runner\.workspace'
|
|
13
|
+
flags: i
|
|
14
|
+
- regex: '\$\{\{\s*runner\.workspace\s*\}\}'
|
|
15
|
+
flags: i
|
|
16
|
+
error_messages:
|
|
17
|
+
- "No such file or directory"
|
|
18
|
+
- "Error: Path does not exist"
|
|
19
|
+
- "ENOENT: no such file or directory"
|
|
20
|
+
root_cause: |
|
|
21
|
+
Two similarly-named contexts point to DIFFERENT directories on GitHub-hosted
|
|
22
|
+
runners:
|
|
23
|
+
|
|
24
|
+
github.workspace = /home/runner/work/REPO/REPO ← the checked-out repo root
|
|
25
|
+
runner.workspace = /home/runner/work/REPO ← one level ABOVE the repo root
|
|
26
|
+
|
|
27
|
+
runner.workspace is the "runner work directory" that CONTAINS the repo clone
|
|
28
|
+
folder, not the repo itself. The official documentation describes this distinction,
|
|
29
|
+
but the names are confusingly similar and many developers assume runner.workspace
|
|
30
|
+
is equivalent to "where my code is" — which is github.workspace.
|
|
31
|
+
|
|
32
|
+
Consequences:
|
|
33
|
+
- working-directory: ${{ runner.workspace }} silently points one level above
|
|
34
|
+
the repo, causing file-not-found errors on relative paths inside the repo.
|
|
35
|
+
- Path-building expressions like runner.workspace + '/src' resolve to a
|
|
36
|
+
path that doesn't exist.
|
|
37
|
+
- The error surfaces as "No such file or directory" or a missing file, not as
|
|
38
|
+
a context resolution failure, making the root cause hard to identify.
|
|
39
|
+
|
|
40
|
+
On some self-hosted runner configurations both paths may resolve to the same
|
|
41
|
+
directory, masking the bug until the workflow runs on a GitHub-hosted runner.
|
|
42
|
+
|
|
43
|
+
Source: GitHub Actions contexts documentation; GitHub Community/15327;
|
|
44
|
+
Stack Overflow path confusion questions with 100K+ combined views.
|
|
45
|
+
fix: |
|
|
46
|
+
Replace runner.workspace with github.workspace (or the $GITHUB_WORKSPACE
|
|
47
|
+
environment variable) when the intent is to reference the checked-out repo root.
|
|
48
|
+
|
|
49
|
+
Use runner.workspace only when intentionally targeting the parent directory
|
|
50
|
+
that contains the repo clone — for example, creating a sibling directory for
|
|
51
|
+
build artifacts that should live outside the repo tree.
|
|
52
|
+
fix_code:
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: "Use github.workspace for the repo root"
|
|
55
|
+
code: |
|
|
56
|
+
jobs:
|
|
57
|
+
build:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v4
|
|
61
|
+
|
|
62
|
+
# ❌ WRONG — runner.workspace is one level ABOVE the repo
|
|
63
|
+
# - name: Wrong path
|
|
64
|
+
# working-directory: ${{ runner.workspace }}
|
|
65
|
+
|
|
66
|
+
# ✅ CORRECT — github.workspace is the repo root
|
|
67
|
+
- name: List repo files
|
|
68
|
+
run: ls ${{ github.workspace }}
|
|
69
|
+
|
|
70
|
+
# ✅ ALSO CORRECT — $GITHUB_WORKSPACE env var is identical
|
|
71
|
+
- name: Build from repo root
|
|
72
|
+
working-directory: ${{ github.workspace }}
|
|
73
|
+
run: ls .
|
|
74
|
+
|
|
75
|
+
# ✅ Intentional use of runner.workspace — sibling dir outside repo
|
|
76
|
+
- name: Create output dir alongside repo
|
|
77
|
+
run: mkdir ${{ runner.workspace }}/build-output
|
|
78
|
+
- language: yaml
|
|
79
|
+
label: "Directory layout reference"
|
|
80
|
+
code: |
|
|
81
|
+
# GitHub-hosted runner path layout:
|
|
82
|
+
#
|
|
83
|
+
# /home/runner/work/
|
|
84
|
+
# └── my-repo/ ← runner.workspace (${{ runner.workspace }})
|
|
85
|
+
# └── my-repo/ ← github.workspace (${{ github.workspace }})
|
|
86
|
+
# ├── src/
|
|
87
|
+
# ├── package.json
|
|
88
|
+
# └── .github/
|
|
89
|
+
#
|
|
90
|
+
# $GITHUB_WORKSPACE == ${{ github.workspace }} (same value, two access methods)
|
|
91
|
+
prevention:
|
|
92
|
+
- "Always use github.workspace (or $GITHUB_WORKSPACE) to reference the checked-out repo root"
|
|
93
|
+
- "Reserve runner.workspace for intentional parent-directory operations such as sibling build directories"
|
|
94
|
+
- "When debugging path issues, print both: echo $GITHUB_WORKSPACE and echo ${{ runner.workspace }}"
|
|
95
|
+
- "Self-hosted runner paths may differ from GitHub-hosted runners — use named contexts rather than hardcoded absolute paths"
|
|
96
|
+
docs:
|
|
97
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#github-context"
|
|
98
|
+
label: "GitHub Actions — github context (workspace)"
|
|
99
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#runner-context"
|
|
100
|
+
label: "GitHub Actions — runner context (workspace)"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
id: silent-failures-045
|
|
2
|
+
title: "ACTIONS_RUNNER_DEBUG / ACTIONS_STEP_DEBUG typo — debug logging silently disabled"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- debug
|
|
7
|
+
- ACTIONS_RUNNER_DEBUG
|
|
8
|
+
- ACTIONS_STEP_DEBUG
|
|
9
|
+
- secrets
|
|
10
|
+
- diagnostic
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'RUNNER_DEBUG'
|
|
13
|
+
flags: ''
|
|
14
|
+
- regex: 'ACTIONS_DEBUG'
|
|
15
|
+
flags: i
|
|
16
|
+
error_messages: []
|
|
17
|
+
root_cause: |
|
|
18
|
+
GitHub Actions debug logging is controlled by two specific repository secrets
|
|
19
|
+
(or repository variables since October 2023):
|
|
20
|
+
|
|
21
|
+
ACTIONS_RUNNER_DEBUG = "true" — runner diagnostic logs (runner.diag.log,
|
|
22
|
+
Worker_*.log files attached to the run as a downloadable zip)
|
|
23
|
+
ACTIONS_STEP_DEBUG = "true" — verbose step-level debug output shown
|
|
24
|
+
inline in each step log section
|
|
25
|
+
|
|
26
|
+
When developers set secrets with incorrect names such as RUNNER_DEBUG, DEBUG,
|
|
27
|
+
ACTIONS_DEBUG, or ENABLE_DEBUG, GitHub silently ignores them. The workflow
|
|
28
|
+
runs normally with no indication that debug logging was never activated.
|
|
29
|
+
The developer sees only standard log output and assumes there is nothing more
|
|
30
|
+
to inspect.
|
|
31
|
+
|
|
32
|
+
A secondary failure mode: setting these as workflow-level env: variables has
|
|
33
|
+
no effect — debug logging requires them as repository secrets or repository
|
|
34
|
+
variables, not env: entries.
|
|
35
|
+
|
|
36
|
+
Source: GitHub Docs — Enabling debug logging; GitHub Community recurring
|
|
37
|
+
reports of debug logs not appearing despite secrets being set.
|
|
38
|
+
fix: |
|
|
39
|
+
Set repository secrets or repository variables with the EXACT names:
|
|
40
|
+
ACTIONS_RUNNER_DEBUG (value: true)
|
|
41
|
+
ACTIONS_STEP_DEBUG (value: true)
|
|
42
|
+
|
|
43
|
+
Path: Repository Settings → Secrets and variables → Actions → New repository secret.
|
|
44
|
+
|
|
45
|
+
These can also be set at the organization level to apply across all repos.
|
|
46
|
+
|
|
47
|
+
Runner diagnostic logs from ACTIONS_RUNNER_DEBUG are available as a
|
|
48
|
+
downloadable zip file attached to the workflow run — not shown inline.
|
|
49
|
+
Step debug logs from ACTIONS_STEP_DEBUG appear inline in each step as
|
|
50
|
+
collapsed "##[debug]" lines.
|
|
51
|
+
fix_code:
|
|
52
|
+
- language: yaml
|
|
53
|
+
label: "workflow_dispatch input to enable debug for a single run without changing secrets"
|
|
54
|
+
code: |
|
|
55
|
+
on:
|
|
56
|
+
workflow_dispatch:
|
|
57
|
+
inputs:
|
|
58
|
+
debug_enabled:
|
|
59
|
+
type: boolean
|
|
60
|
+
description: 'Enable step debug logging for this run'
|
|
61
|
+
default: false
|
|
62
|
+
|
|
63
|
+
jobs:
|
|
64
|
+
build:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- name: Enable step debug mode
|
|
68
|
+
if: ${{ inputs.debug_enabled }}
|
|
69
|
+
run: echo "ACTIONS_STEP_DEBUG=true" >> $GITHUB_ENV
|
|
70
|
+
|
|
71
|
+
- name: Build step (verbose when debug enabled)
|
|
72
|
+
run: echo "Build running"
|
|
73
|
+
- language: yaml
|
|
74
|
+
label: "Check which debug secrets are active"
|
|
75
|
+
code: |
|
|
76
|
+
- name: Show debug context
|
|
77
|
+
run: |
|
|
78
|
+
echo "Runner debug: $ACTIONS_RUNNER_DEBUG"
|
|
79
|
+
echo "Step debug: $ACTIONS_STEP_DEBUG"
|
|
80
|
+
# If these print empty string, the secrets are not set or named incorrectly
|
|
81
|
+
prevention:
|
|
82
|
+
- "Use exactly 'ACTIONS_RUNNER_DEBUG' and 'ACTIONS_STEP_DEBUG' as the secret names — no other names work"
|
|
83
|
+
- "Set these as repository secrets or repository variables, not as env: in workflow YAML"
|
|
84
|
+
- "ACTIONS_RUNNER_DEBUG logs are in a separate zip download — check the run's uploaded artifacts section"
|
|
85
|
+
- "Both secrets can be enabled simultaneously; they control independent output streams"
|
|
86
|
+
docs:
|
|
87
|
+
- url: "https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/troubleshooting-workflows/enabling-debug-logging"
|
|
88
|
+
label: "GitHub Docs — Enabling debug logging"
|
|
89
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables"
|
|
90
|
+
label: "GitHub Docs — Storing information in variables"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
id: silent-failures-047
|
|
2
|
+
title: "GITHUB_ENV variables are job-scoped — not shared with downstream needs: jobs"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- GITHUB_ENV
|
|
7
|
+
- environment-variables
|
|
8
|
+
- job-outputs
|
|
9
|
+
- cross-job
|
|
10
|
+
- silent-failure
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'GITHUB_ENV'
|
|
13
|
+
flags: ''
|
|
14
|
+
- regex: 'echo\s+".+=.+"\s*>>\s*\$GITHUB_ENV'
|
|
15
|
+
flags: i
|
|
16
|
+
error_messages: []
|
|
17
|
+
root_cause: |
|
|
18
|
+
Environment variables written to $GITHUB_ENV are scoped to the CURRENT JOB
|
|
19
|
+
only. They persist across all subsequent steps within that job, but are NOT
|
|
20
|
+
propagated to any other job — including jobs that depend on the producing
|
|
21
|
+
job via needs:.
|
|
22
|
+
|
|
23
|
+
This is a silent failure because:
|
|
24
|
+
1. The producing step exits 0 with no warning
|
|
25
|
+
2. The downstream job runs normally with no error message
|
|
26
|
+
3. The variable evaluates to empty string in the downstream job
|
|
27
|
+
4. Steps that depend on the value produce wrong results or silently skip
|
|
28
|
+
based on the empty string condition
|
|
29
|
+
|
|
30
|
+
Common scenario (silently broken):
|
|
31
|
+
Job A: echo "VERSION=1.2.3" >> $GITHUB_ENV
|
|
32
|
+
Job B (needs: job-a): echo $VERSION → prints empty string
|
|
33
|
+
|
|
34
|
+
The correct mechanism for sharing values across job boundaries is job outputs
|
|
35
|
+
combined with the needs context. GITHUB_ENV is appropriate only for values
|
|
36
|
+
that need to persist across multiple steps within the same job.
|
|
37
|
+
|
|
38
|
+
Source: GitHub Docs — Passing information between jobs; GitHub Community and
|
|
39
|
+
Stack Overflow cross-job environment variable questions.
|
|
40
|
+
fix: |
|
|
41
|
+
To pass a value from one job to a downstream job:
|
|
42
|
+
|
|
43
|
+
1. Write the value to $GITHUB_OUTPUT (not $GITHUB_ENV) in the producing step
|
|
44
|
+
2. Declare the step output in the producing job's outputs: block
|
|
45
|
+
3. Reference the value in the consuming job via ${{ needs.job-id.outputs.key }}
|
|
46
|
+
|
|
47
|
+
Continue using $GITHUB_ENV when the value only needs to be available to later
|
|
48
|
+
steps within the same job.
|
|
49
|
+
fix_code:
|
|
50
|
+
- language: yaml
|
|
51
|
+
label: "Correct cross-job value sharing via GITHUB_OUTPUT and job outputs"
|
|
52
|
+
code: |
|
|
53
|
+
jobs:
|
|
54
|
+
build:
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
outputs:
|
|
57
|
+
# Expose the step output as a job output
|
|
58
|
+
version: ${{ steps.get-version.outputs.version }}
|
|
59
|
+
steps:
|
|
60
|
+
- id: get-version
|
|
61
|
+
# Write to GITHUB_OUTPUT for cross-job sharing, not GITHUB_ENV
|
|
62
|
+
run: echo "version=1.2.3" >> $GITHUB_OUTPUT
|
|
63
|
+
|
|
64
|
+
deploy:
|
|
65
|
+
needs: build
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
steps:
|
|
68
|
+
- name: Use version from build job
|
|
69
|
+
run: echo "Deploying version ${{ needs.build.outputs.version }}"
|
|
70
|
+
- language: yaml
|
|
71
|
+
label: "GITHUB_ENV is correct for within-job cross-step sharing"
|
|
72
|
+
code: |
|
|
73
|
+
jobs:
|
|
74
|
+
build:
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
steps:
|
|
77
|
+
- name: Set version for this job's steps
|
|
78
|
+
run: echo "VERSION=1.2.3" >> $GITHUB_ENV
|
|
79
|
+
|
|
80
|
+
- name: Step 2 in same job — $VERSION is available
|
|
81
|
+
run: echo "Building $VERSION"
|
|
82
|
+
|
|
83
|
+
- name: Step 3 in same job — $VERSION is still available
|
|
84
|
+
run: echo "Packaging $VERSION"
|
|
85
|
+
|
|
86
|
+
# $VERSION is NOT available in any other job
|
|
87
|
+
prevention:
|
|
88
|
+
- "Use GITHUB_OUTPUT + job outputs: for any value that must cross a job boundary"
|
|
89
|
+
- "Use GITHUB_ENV only for values shared between steps within the same job"
|
|
90
|
+
- "Add a verification step in the consuming job that echoes the value and fails if empty"
|
|
91
|
+
- "Avoid setting both GITHUB_ENV and GITHUB_OUTPUT for the same value — use GITHUB_OUTPUT as the single source of truth"
|
|
92
|
+
docs:
|
|
93
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs"
|
|
94
|
+
label: "GitHub Docs — Passing information between jobs"
|
|
95
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables"
|
|
96
|
+
label: "GitHub Docs — Storing information in variables (GITHUB_ENV)"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
id: silent-failures-046
|
|
2
|
+
title: "matrix.exclude silently ignored when value doesn't exactly match matrix dimension"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- matrix
|
|
7
|
+
- exclude
|
|
8
|
+
- strategy
|
|
9
|
+
- configuration
|
|
10
|
+
- silent-failure
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'exclude:'
|
|
13
|
+
flags: ''
|
|
14
|
+
- regex: 'strategy:\s*\n\s*matrix:'
|
|
15
|
+
flags: im
|
|
16
|
+
error_messages: []
|
|
17
|
+
root_cause: |
|
|
18
|
+
GitHub Actions matrix exclude: uses exact string equality. An exclude entry
|
|
19
|
+
removes a combination only when ALL specified key-value pairs exactly match
|
|
20
|
+
a generated matrix combination. If any pair fails to match, the combination
|
|
21
|
+
is silently kept.
|
|
22
|
+
|
|
23
|
+
Three common silent-failure patterns:
|
|
24
|
+
|
|
25
|
+
1. Value substring mismatch:
|
|
26
|
+
matrix: {os: [ubuntu-latest, windows-latest]}
|
|
27
|
+
exclude: [{os: ubuntu}] # 'ubuntu' != 'ubuntu-latest' → ignored
|
|
28
|
+
|
|
29
|
+
2. Key not present in matrix dimensions:
|
|
30
|
+
matrix: {os: [ubuntu-latest], node: [18, 20]}
|
|
31
|
+
exclude: [{os: ubuntu-latest, version: 18}] # 'version' not a matrix key → ignored
|
|
32
|
+
|
|
33
|
+
3. Type mismatch in some expression contexts:
|
|
34
|
+
matrix: {node: [18, 20]}
|
|
35
|
+
exclude: [{node: "18"}] # string "18" may not equal integer 18
|
|
36
|
+
|
|
37
|
+
GitHub produces no warning when an exclude entry matches zero combinations.
|
|
38
|
+
The unwanted job continues to run, consuming CI minutes with no indication
|
|
39
|
+
that the exclude rule was ineffective.
|
|
40
|
+
|
|
41
|
+
Source: GitHub Docs matrix strategy; GitHub Community/26957; Stack Overflow
|
|
42
|
+
questions about matrix exclude not working as expected.
|
|
43
|
+
fix: |
|
|
44
|
+
Use the exact string that appears in your matrix definition when writing
|
|
45
|
+
exclude entries. Run a matrix debug step to confirm the actual values
|
|
46
|
+
GitHub generates before relying on exclusions.
|
|
47
|
+
|
|
48
|
+
For complex exclusion logic involving substring matching or multiple
|
|
49
|
+
conditions, use the if: condition on the job instead of exclude:.
|
|
50
|
+
The if: condition can use contains(), startsWith(), and other expression
|
|
51
|
+
functions that exact-match exclude cannot.
|
|
52
|
+
fix_code:
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: "Correct exclude — exact value match required"
|
|
55
|
+
code: |
|
|
56
|
+
jobs:
|
|
57
|
+
test:
|
|
58
|
+
strategy:
|
|
59
|
+
matrix:
|
|
60
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
61
|
+
node: [18, 20, 22]
|
|
62
|
+
exclude:
|
|
63
|
+
# Value must match EXACTLY what appears in the matrix list above
|
|
64
|
+
- os: windows-latest # ✅ exact match
|
|
65
|
+
node: 18
|
|
66
|
+
# Incorrect examples (silently ignored):
|
|
67
|
+
# - os: windows # ❌ 'windows' != 'windows-latest'
|
|
68
|
+
# - os: windows-latest
|
|
69
|
+
# version: 18 # ❌ 'version' not a matrix dimension key
|
|
70
|
+
runs-on: ${{ matrix.os }}
|
|
71
|
+
steps:
|
|
72
|
+
- name: Debug matrix combination
|
|
73
|
+
run: echo '${{ toJSON(matrix) }}'
|
|
74
|
+
- language: yaml
|
|
75
|
+
label: "Alternative — use if: for flexible exclusion"
|
|
76
|
+
code: |
|
|
77
|
+
jobs:
|
|
78
|
+
test:
|
|
79
|
+
# Use if: when you need startsWith / contains logic
|
|
80
|
+
if: >-
|
|
81
|
+
!(matrix.os == 'windows-latest' && matrix.node == 18)
|
|
82
|
+
strategy:
|
|
83
|
+
matrix:
|
|
84
|
+
os: [ubuntu-latest, windows-latest]
|
|
85
|
+
node: [18, 20]
|
|
86
|
+
runs-on: ${{ matrix.os }}
|
|
87
|
+
prevention:
|
|
88
|
+
- "Use the exact strings from your matrix definition in exclude entries — copy-paste, do not retype"
|
|
89
|
+
- "Add a debug step printing toJSON(matrix) to verify which combinations GitHub actually generates"
|
|
90
|
+
- "Use if: conditions for substring matching, contains(), or multi-condition exclusion logic"
|
|
91
|
+
- "Test matrix changes on a branch and review the job list before merging to confirm excluded combinations are gone"
|
|
92
|
+
docs:
|
|
93
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow#excluding-matrix-configurations"
|
|
94
|
+
label: "GitHub Docs — Excluding matrix configurations"
|
|
95
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/using-conditions-to-control-job-execution"
|
|
96
|
+
label: "GitHub Docs — Using conditions to control job execution"
|
package/package.json
CHANGED