@htekdev/actions-debugger 1.0.10 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/errors/caching-artifacts/cache-cross-os-archive-missing.yml +120 -0
- package/errors/known-unsolved/matrix-outputs-last-writer-wins.yml +137 -0
- package/errors/known-unsolved/uses-key-no-expression-support.yml +134 -0
- package/errors/runner-environment/checkout-persist-credentials-false-git-auth.yml +124 -0
- package/errors/runner-environment/windows-default-shell-powershell-not-bash.yml +137 -0
- package/errors/silent-failures/checkout-fetch-depth-shallow-clone-breaks-history.yml +104 -0
- package/errors/silent-failures/pull-request-ref-is-merge-ref.yml +134 -0
- package/errors/triggers/push-paths-filter-bypassed-by-tag.yml +123 -0
- package/errors/triggers/workflow-dispatch-filters-silently-ignored.yml +131 -0
- package/errors/yaml-syntax/matrix-include-extra-standalone-jobs.yml +136 -0
- package/package.json +1 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
id: caching-artifacts-013
|
|
2
|
+
title: "Cross-OS Cache Miss — enableCrossOsArchive Not Set"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- cross-os
|
|
8
|
+
- windows
|
|
9
|
+
- linux
|
|
10
|
+
- cache-miss
|
|
11
|
+
- enableCrossOsArchive
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "Cache not found for input keys"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "enableCrossOsArchive.*false"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "cache miss.*windows"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "tar.*posix.*failed"
|
|
20
|
+
flags: "i"
|
|
21
|
+
error_messages:
|
|
22
|
+
- "Cache not found for input keys: <key>"
|
|
23
|
+
- "enableCrossOsArchive: false"
|
|
24
|
+
- "Cache saved successfully"
|
|
25
|
+
- "Post job cleanup."
|
|
26
|
+
root_cause: |
|
|
27
|
+
`actions/cache` uses different archive formats depending on the runner OS:
|
|
28
|
+
- Linux/macOS: GNU tar (gnutar) with zstd compression
|
|
29
|
+
- Windows: BSD tar bundled with Git for Windows (`C:\Program Files\Git\usr\bin\tar.exe`)
|
|
30
|
+
|
|
31
|
+
When a cache is created on Linux or macOS, the archive is in GNU tar format. When
|
|
32
|
+
Windows attempts to restore it (or vice versa), the different tar implementation
|
|
33
|
+
fails to decompress the archive, resulting in a silent cache miss.
|
|
34
|
+
|
|
35
|
+
The `enableCrossOsArchive` option (default: `false`) controls whether the cache is
|
|
36
|
+
stored in a cross-platform-compatible format. Setting it to `true` forces GNU tar
|
|
37
|
+
format on all platforms, enabling cache sharing across OSes.
|
|
38
|
+
|
|
39
|
+
This is particularly common in monorepos where:
|
|
40
|
+
- Node modules or package caches are written by a Linux job and expected to be
|
|
41
|
+
restored by a Windows job in the same workflow run
|
|
42
|
+
- A "seed cache" job runs on Linux and downstream jobs run on Windows
|
|
43
|
+
- The matrix includes multiple OS values sharing the same cache key
|
|
44
|
+
|
|
45
|
+
Tracked in actions/cache#1275 (Cache miss on Windows despite a successful
|
|
46
|
+
cache-write) — confirmed fixed by setting `enableCrossOsArchive: true`.
|
|
47
|
+
fix: |
|
|
48
|
+
Add `enableCrossOsArchive: true` to ALL cache steps (both the write and the restore
|
|
49
|
+
steps) that need to share caches across different OS runners.
|
|
50
|
+
|
|
51
|
+
IMPORTANT: The option must be set consistently on both the writing and reading jobs.
|
|
52
|
+
Setting it only on one side will not work.
|
|
53
|
+
|
|
54
|
+
If true cross-OS caching is not needed, ensure each OS creates its own native cache
|
|
55
|
+
by including `${{ runner.os }}` in the cache key.
|
|
56
|
+
fix_code:
|
|
57
|
+
- language: yaml
|
|
58
|
+
label: "Enable cross-OS archive on cache steps that share between Linux and Windows"
|
|
59
|
+
code: |
|
|
60
|
+
- name: Cache node modules (cross-OS compatible)
|
|
61
|
+
uses: actions/cache@v4
|
|
62
|
+
with:
|
|
63
|
+
path: node_modules
|
|
64
|
+
key: node-${{ hashFiles('**/package-lock.json') }}
|
|
65
|
+
enableCrossOsArchive: true # Required for Linux↔Windows cache sharing
|
|
66
|
+
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: "OS-specific cache keys (alternative — each OS gets its own cache)"
|
|
69
|
+
code: |
|
|
70
|
+
- name: Cache dependencies (OS-scoped key — no cross-OS needed)
|
|
71
|
+
uses: actions/cache@v4
|
|
72
|
+
with:
|
|
73
|
+
path: ~/.cache/pip
|
|
74
|
+
# Include runner.os so each OS writes its own cache entry
|
|
75
|
+
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
|
76
|
+
restore-keys: |
|
|
77
|
+
${{ runner.os }}-pip-
|
|
78
|
+
|
|
79
|
+
- language: yaml
|
|
80
|
+
label: "Matrix workflow with consistent enableCrossOsArchive on all jobs"
|
|
81
|
+
code: |
|
|
82
|
+
jobs:
|
|
83
|
+
seed-cache:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
- name: Seed shared cache
|
|
88
|
+
uses: actions/cache@v4
|
|
89
|
+
with:
|
|
90
|
+
path: node_modules
|
|
91
|
+
key: node-${{ hashFiles('**/package-lock.json') }}
|
|
92
|
+
enableCrossOsArchive: true
|
|
93
|
+
|
|
94
|
+
build:
|
|
95
|
+
needs: seed-cache
|
|
96
|
+
strategy:
|
|
97
|
+
matrix:
|
|
98
|
+
os: [ubuntu-latest, windows-latest]
|
|
99
|
+
runs-on: ${{ matrix.os }}
|
|
100
|
+
steps:
|
|
101
|
+
- uses: actions/checkout@v4
|
|
102
|
+
- name: Restore shared cache
|
|
103
|
+
uses: actions/cache@v4
|
|
104
|
+
with:
|
|
105
|
+
path: node_modules
|
|
106
|
+
key: node-${{ hashFiles('**/package-lock.json') }}
|
|
107
|
+
enableCrossOsArchive: true # Must match the write job
|
|
108
|
+
|
|
109
|
+
prevention:
|
|
110
|
+
- "Always include `runner.os` in your cache key unless you explicitly need cross-OS sharing."
|
|
111
|
+
- "When cross-OS sharing IS required, set `enableCrossOsArchive: true` on every step that reads or writes the shared cache."
|
|
112
|
+
- "Treat cross-OS cache sharing as an explicit opt-in, not the default."
|
|
113
|
+
- "Test cache restoration on all target OSes during initial workflow setup."
|
|
114
|
+
docs:
|
|
115
|
+
- url: "https://github.com/actions/cache/blob/main/README.md"
|
|
116
|
+
label: "actions/cache README: enableCrossOsArchive option"
|
|
117
|
+
- url: "https://github.com/actions/cache/blob/main/tips-and-workarounds.md"
|
|
118
|
+
label: "actions/cache: Tips and workarounds (cross-OS section)"
|
|
119
|
+
- url: "https://github.com/actions/cache/issues/1275"
|
|
120
|
+
label: "actions/cache#1275: Cache miss on Windows despite a successful cache-write"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
id: known-unsolved-011
|
|
2
|
+
title: "Matrix Job Outputs Are Non-Deterministic — Only Last Completed Job's Value Is Used"
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- matrix
|
|
7
|
+
- outputs
|
|
8
|
+
- non-deterministic
|
|
9
|
+
- race-condition
|
|
10
|
+
- jobs-outputs
|
|
11
|
+
- strategy
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "jobs\\.\\w+\\.outputs\\.\\w+"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "matrix.*output.*overwrite"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "only.*last.*matrix.*job.*output"
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "outputs from matrix returns only the last value"
|
|
21
|
+
- "matrix job output overwritten by later-completing job"
|
|
22
|
+
root_cause: |
|
|
23
|
+
GitHub Actions does not support aggregating outputs across matrix jobs. When a job uses
|
|
24
|
+
`strategy: matrix:` and defines `outputs:`, all matrix instances write to the SAME
|
|
25
|
+
output key. The value that survives is whichever matrix job completes LAST — and
|
|
26
|
+
completion order is non-deterministic.
|
|
27
|
+
|
|
28
|
+
Example: a 3-element matrix writing `result` as an output. Job [A, B, C] may complete
|
|
29
|
+
in any order — the downstream job receives only the output from whichever finished last.
|
|
30
|
+
This can silently produce wrong results that vary between runs.
|
|
31
|
+
|
|
32
|
+
The underlying limitation: `jobs.<job_id>.outputs` maps to a single scalar value
|
|
33
|
+
per key, with no way to collect values from all matrix instances into a structured
|
|
34
|
+
result. The runner simply last-writes-wins with no conflict detection.
|
|
35
|
+
|
|
36
|
+
This is a long-standing limitation tracked in actions/runner#1835 (opened April 2022)
|
|
37
|
+
and actions/runner#2477. A partial improvement in runner v2.303.0 added `$matrix`
|
|
38
|
+
context to outputs expressions, but it is still not generally available and has known
|
|
39
|
+
bugs (runner#2499: workflow won't start with the new syntax on older runners).
|
|
40
|
+
|
|
41
|
+
Source: actions/runner#1835 (Outputs from matrix returns only the last value)
|
|
42
|
+
Source: Stack Overflow #70287603 (Dynamic outputs for job with strategy.matrix)
|
|
43
|
+
fix: |
|
|
44
|
+
There is no native GitHub Actions solution for collecting all matrix job outputs into
|
|
45
|
+
a structured result. Use one of these workarounds:
|
|
46
|
+
|
|
47
|
+
Workaround 1 — Write to artifacts, aggregate downstream:
|
|
48
|
+
Each matrix job writes its result to a uniquely-named artifact file. A downstream
|
|
49
|
+
aggregator job downloads all artifacts and processes them.
|
|
50
|
+
|
|
51
|
+
Workaround 2 — Encode outputs in artifact JSON, then use as dynamic matrix:
|
|
52
|
+
Popularized by the `matrix-output` marketplace action. Each job appends a JSON row
|
|
53
|
+
to an artifact, and a reporting job downloads and merges them.
|
|
54
|
+
|
|
55
|
+
Workaround 3 — Use reusable workflows with known job indices:
|
|
56
|
+
Reusable workflows allow accessing specific job outputs via the caller's
|
|
57
|
+
`jobs.<reusable_workflow_job>.outputs` context.
|
|
58
|
+
|
|
59
|
+
Workaround 4 — Consolidate into a single non-matrix job:
|
|
60
|
+
If the matrix outputs must be consumed, consider restructuring to run the work
|
|
61
|
+
sequentially in one job or use a scripted loop.
|
|
62
|
+
fix_code:
|
|
63
|
+
- language: yaml
|
|
64
|
+
label: "Workaround: write output to artifact file, aggregate in downstream job"
|
|
65
|
+
code: |
|
|
66
|
+
jobs:
|
|
67
|
+
build:
|
|
68
|
+
strategy:
|
|
69
|
+
matrix:
|
|
70
|
+
component: [api, web, worker]
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
steps:
|
|
73
|
+
- name: Build component
|
|
74
|
+
run: |
|
|
75
|
+
# ... build logic ...
|
|
76
|
+
echo "build_status=success" >> result-${{ matrix.component }}.txt
|
|
77
|
+
|
|
78
|
+
- name: Upload result artifact
|
|
79
|
+
uses: actions/upload-artifact@v4
|
|
80
|
+
with:
|
|
81
|
+
name: result-${{ matrix.component }}
|
|
82
|
+
path: result-${{ matrix.component }}.txt
|
|
83
|
+
|
|
84
|
+
aggregate:
|
|
85
|
+
needs: build
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
steps:
|
|
88
|
+
- name: Download all results
|
|
89
|
+
uses: actions/download-artifact@v4
|
|
90
|
+
with:
|
|
91
|
+
pattern: result-*
|
|
92
|
+
merge-multiple: true
|
|
93
|
+
|
|
94
|
+
- name: Aggregate results
|
|
95
|
+
run: |
|
|
96
|
+
for f in result-*.txt; do
|
|
97
|
+
echo "=== $f ==="
|
|
98
|
+
cat "$f"
|
|
99
|
+
done
|
|
100
|
+
|
|
101
|
+
- language: yaml
|
|
102
|
+
label: "Anti-pattern: matrix outputs — only one value survives (last writer wins)"
|
|
103
|
+
code: |
|
|
104
|
+
# ❌ WRONG — only last-completing job's output is used by downstream-job
|
|
105
|
+
jobs:
|
|
106
|
+
build:
|
|
107
|
+
strategy:
|
|
108
|
+
matrix:
|
|
109
|
+
component: [api, web, worker]
|
|
110
|
+
runs-on: ubuntu-latest
|
|
111
|
+
outputs:
|
|
112
|
+
status: ${{ steps.build.outputs.status }} # ← non-deterministic!
|
|
113
|
+
steps:
|
|
114
|
+
- id: build
|
|
115
|
+
run: echo "status=done-${{ matrix.component }}" >> $GITHUB_OUTPUT
|
|
116
|
+
|
|
117
|
+
downstream-job:
|
|
118
|
+
needs: build
|
|
119
|
+
runs-on: ubuntu-latest
|
|
120
|
+
steps:
|
|
121
|
+
- run: echo "${{ needs.build.outputs.status }}"
|
|
122
|
+
# Prints ONE value (whichever matrix job finished last) — unpredictable
|
|
123
|
+
|
|
124
|
+
prevention:
|
|
125
|
+
- "Never rely on matrix job outputs for per-job values — outputs are not aggregated."
|
|
126
|
+
- "Use upload-artifact/download-artifact with unique names per matrix dimension to collect per-job results."
|
|
127
|
+
- "If a downstream job needs all matrix results, prefer artifact-based aggregation over job outputs."
|
|
128
|
+
- "Document in your workflow comments that matrix outputs are single-valued and non-deterministic."
|
|
129
|
+
docs:
|
|
130
|
+
- url: "https://github.com/actions/runner/issues/1835"
|
|
131
|
+
label: "actions/runner#1835: Outputs from matrix returns only the last value"
|
|
132
|
+
- url: "https://stackoverflow.com/questions/70287603/dynamic-outputs-for-job-with-strategy-matrix"
|
|
133
|
+
label: "Stack Overflow: Dynamic outputs for job with strategy.matrix"
|
|
134
|
+
- url: "https://github.com/marketplace/actions/matrix-output"
|
|
135
|
+
label: "Marketplace: matrix-output action (artifact-based aggregation workaround)"
|
|
136
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs"
|
|
137
|
+
label: "GitHub Docs: Passing information between jobs"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
id: known-unsolved-012
|
|
2
|
+
title: "uses: Key Does Not Support Expressions or Context Variables — Must Be a Static String"
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- uses
|
|
7
|
+
- expression
|
|
8
|
+
- context
|
|
9
|
+
- dynamic-action
|
|
10
|
+
- composite
|
|
11
|
+
- local-action
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "uses:\\s*\\.?/\\$\\{\\{"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "uses:\\s*['\"]?\\$\\{\\{"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "Unrecognized named-value.*uses"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "Invalid workflow file.*uses.*expression"
|
|
20
|
+
flags: "i"
|
|
21
|
+
error_messages:
|
|
22
|
+
- "Invalid workflow file: .github/workflows/ci.yml — uses: does not support expression syntax"
|
|
23
|
+
- "The 'uses' field must be a literal string and cannot contain expressions"
|
|
24
|
+
- "Unrecognized named-value: 'env' at position 1 within expression in uses:"
|
|
25
|
+
root_cause: |
|
|
26
|
+
GitHub Actions resolves all `uses:` references (actions and reusable workflows) BEFORE
|
|
27
|
+
the job starts executing — at workflow evaluation / job queue time. At that point,
|
|
28
|
+
no runtime contexts (env, inputs, github, matrix, steps, needs) are available.
|
|
29
|
+
|
|
30
|
+
As a result, you cannot use `${{ }}` expression syntax in a `uses:` key at any level:
|
|
31
|
+
- Step-level: `uses: ${{ env.MY_ACTION }}@${{ env.VERSION }}` ❌
|
|
32
|
+
- Local path with variable: `uses: ./${{ env.DIR }}/my-action` ❌
|
|
33
|
+
- Dynamic version pin: `uses: actions/setup-node@${{ inputs.node-version }}` ❌
|
|
34
|
+
- Reusable workflow: `uses: ${{ github.repository }}/.github/workflows/ci.yml@main` ❌
|
|
35
|
+
|
|
36
|
+
This is an architectural constraint: the runner must download and validate actions
|
|
37
|
+
(and reusable workflows) before any step code can run. Allowing runtime expressions
|
|
38
|
+
would break policy enforcement (org-level allowed-actions lists) and make workflow
|
|
39
|
+
graphs non-deterministic at parse time.
|
|
40
|
+
|
|
41
|
+
Note: `env:` and `with:` inputs to an action DO support expressions — only the `uses:`
|
|
42
|
+
key itself is restricted.
|
|
43
|
+
|
|
44
|
+
Source: actions/runner#1479 (Support context/matrix variables in steps of type 'uses')
|
|
45
|
+
Source: actions/runner#895 (Allow expressions in uses:)
|
|
46
|
+
Source: Stack Overflow #75373413 (Reference a variable in uses when pointing to a path)
|
|
47
|
+
fix: |
|
|
48
|
+
There is no native GitHub Actions solution for dynamic `uses:` values.
|
|
49
|
+
|
|
50
|
+
Option 1 — Hard-code the version or path (recommended for most cases):
|
|
51
|
+
Pin versions explicitly in the workflow file. Use Dependabot or Renovate to keep
|
|
52
|
+
action versions updated automatically.
|
|
53
|
+
|
|
54
|
+
Option 2 — Use a composite action or script as a dispatcher:
|
|
55
|
+
Create a wrapper composite action that accepts an input and uses `if:` conditions
|
|
56
|
+
to call different static actions based on the input value.
|
|
57
|
+
|
|
58
|
+
Option 3 — Use step-security/dynamic-uses (third-party workaround):
|
|
59
|
+
The `dynamic-uses` action by step-security resolves and invokes actions dynamically
|
|
60
|
+
at runtime. This works around the limitation but adds a third-party dependency.
|
|
61
|
+
|
|
62
|
+
Option 4 — Restructure to not need dynamic action references:
|
|
63
|
+
If you're trying to select between local action paths, check out the repo first and
|
|
64
|
+
use a scripted approach instead of composite action dispatch.
|
|
65
|
+
fix_code:
|
|
66
|
+
- language: yaml
|
|
67
|
+
label: "Anti-pattern — expressions in uses: cause parse error"
|
|
68
|
+
code: |
|
|
69
|
+
# ❌ WRONG — none of these are supported
|
|
70
|
+
steps:
|
|
71
|
+
- uses: ./${{ env.ACTION_DIR }}/my-action # ← parse error
|
|
72
|
+
- uses: actions/setup-node@${{ inputs.version }} # ← parse error
|
|
73
|
+
- uses: ${{ github.repository }}/.github/workflows/deploy.yml@main # ← error
|
|
74
|
+
|
|
75
|
+
- language: yaml
|
|
76
|
+
label: "Fix: hard-code the version, use Dependabot to keep it updated"
|
|
77
|
+
code: |
|
|
78
|
+
# ✅ CORRECT — static string in uses:
|
|
79
|
+
steps:
|
|
80
|
+
- uses: actions/setup-node@v4 # pin to major version
|
|
81
|
+
- uses: actions/setup-node@11.0.0 # or exact version
|
|
82
|
+
- uses: ./.github/actions/my-action # local action: always relative to repo root
|
|
83
|
+
|
|
84
|
+
# Keep versions current with Dependabot (.github/dependabot.yml):
|
|
85
|
+
# version: 2
|
|
86
|
+
# updates:
|
|
87
|
+
# - package-ecosystem: "github-actions"
|
|
88
|
+
# directory: "/"
|
|
89
|
+
# schedule:
|
|
90
|
+
# interval: "weekly"
|
|
91
|
+
|
|
92
|
+
- language: yaml
|
|
93
|
+
label: "Workaround: if-based dispatch when choosing between known static actions"
|
|
94
|
+
code: |
|
|
95
|
+
# ✅ Conditional dispatch using if: conditions on static uses:
|
|
96
|
+
steps:
|
|
97
|
+
- name: Setup Node (version from input)
|
|
98
|
+
uses: actions/setup-node@v4
|
|
99
|
+
with:
|
|
100
|
+
node-version: ${{ inputs.node-version }} # ← with: supports expressions
|
|
101
|
+
|
|
102
|
+
# If you truly need different actions based on input:
|
|
103
|
+
- name: Use action for staging
|
|
104
|
+
if: inputs.environment == 'staging'
|
|
105
|
+
uses: ./actions/deploy-staging # static path
|
|
106
|
+
|
|
107
|
+
- name: Use action for production
|
|
108
|
+
if: inputs.environment == 'production'
|
|
109
|
+
uses: ./actions/deploy-production # static path
|
|
110
|
+
|
|
111
|
+
- language: yaml
|
|
112
|
+
label: "Workaround: step-security/dynamic-uses third-party action"
|
|
113
|
+
code: |
|
|
114
|
+
# Third-party workaround — resolves expressions in uses: at runtime
|
|
115
|
+
steps:
|
|
116
|
+
- uses: step-security/dynamic-uses@v1
|
|
117
|
+
with:
|
|
118
|
+
uses: actions/setup-node@${{ inputs.node-version }}
|
|
119
|
+
with: '{ "node-version": 18 }'
|
|
120
|
+
|
|
121
|
+
prevention:
|
|
122
|
+
- "Always use static literal strings in `uses:` keys — no `${{ }}` expressions."
|
|
123
|
+
- "Use `with:` inputs (which DO support expressions) to pass dynamic values to actions."
|
|
124
|
+
- "Use Dependabot or Renovate to automate action version bumps so manual pinning stays current."
|
|
125
|
+
- "For environment-specific action selection, use `if:` conditions on separate steps with static `uses:` values."
|
|
126
|
+
docs:
|
|
127
|
+
- url: "https://github.com/actions/runner/issues/1479"
|
|
128
|
+
label: "actions/runner#1479: Support context/matrix variables in steps of type 'uses'"
|
|
129
|
+
- url: "https://github.com/actions/runner/issues/895"
|
|
130
|
+
label: "actions/runner#895: Allow expressions in uses: key"
|
|
131
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-pre-written-building-blocks-in-your-workflow"
|
|
132
|
+
label: "GitHub Docs: Using pre-written building blocks (actions)"
|
|
133
|
+
- url: "https://github.com/step-security/dynamic-uses"
|
|
134
|
+
label: "step-security/dynamic-uses: third-party workaround for dynamic uses:"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
id: runner-environment-032
|
|
2
|
+
title: "persist-credentials: false Breaks Subsequent Git Push / Authentication"
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- checkout
|
|
7
|
+
- persist-credentials
|
|
8
|
+
- git-push
|
|
9
|
+
- authentication
|
|
10
|
+
- credential-helper
|
|
11
|
+
- git-auto-commit
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "fatal:.*could not read Username.*No such device"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "fatal: Authentication failed for.*github\\.com"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "remote:.*Invalid username or password"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "persist-credentials.*false"
|
|
20
|
+
flags: "i"
|
|
21
|
+
- regex: "error: The requested URL returned error: 403"
|
|
22
|
+
flags: "i"
|
|
23
|
+
error_messages:
|
|
24
|
+
- "fatal: could not read Username for 'https://github.com': No such device or address"
|
|
25
|
+
- "fatal: Authentication failed for 'https://github.com/owner/repo.git/'"
|
|
26
|
+
- "remote: Invalid username or password."
|
|
27
|
+
- "error: The requested URL returned error: 403"
|
|
28
|
+
- "remote: Support for password authentication was removed"
|
|
29
|
+
root_cause: |
|
|
30
|
+
When `actions/checkout` runs with `persist-credentials: false`, it:
|
|
31
|
+
1. Checks out the repository
|
|
32
|
+
2. Explicitly **removes** the git credential helper that was configured for GITHUB_TOKEN auth
|
|
33
|
+
3. Leaves the working directory with no way to authenticate subsequent git operations
|
|
34
|
+
|
|
35
|
+
Any `git push`, `git pull`, or `git fetch` call that runs AFTER this checkout will
|
|
36
|
+
fail with an authentication error because there is no credential helper configured.
|
|
37
|
+
|
|
38
|
+
This pattern is often introduced deliberately for security (to avoid GITHUB_TOKEN
|
|
39
|
+
persistence), but developers forget that it also breaks any action or step that needs
|
|
40
|
+
to push changes back to the repository — such as:
|
|
41
|
+
- `stefanzweifel/git-auto-commit-action` (fails with "could not read Username")
|
|
42
|
+
- Manual `git push origin HEAD` steps
|
|
43
|
+
- Semantic-release, release-please, and other auto-committing tools
|
|
44
|
+
- Actions that amend, tag, or push version bumps
|
|
45
|
+
|
|
46
|
+
The issue is also triggered transitively: if a composite action internally calls
|
|
47
|
+
`actions/checkout` with `persist-credentials: false`, the credential helper is
|
|
48
|
+
removed from the runner's git config, breaking git auth for all subsequent steps.
|
|
49
|
+
|
|
50
|
+
Tracked across multiple issues: stefanzweifel/git-auto-commit-action#356,
|
|
51
|
+
stefanzweifel/git-auto-commit-action#397, anthropics/claude-code-action#1236.
|
|
52
|
+
fix: |
|
|
53
|
+
**Option 1 (simplest): Remove persist-credentials: false**
|
|
54
|
+
If you set it out of habit or from a template, just remove it. GITHUB_TOKEN credentials
|
|
55
|
+
stored by actions/checkout are scoped to the runner and do not persist beyond the
|
|
56
|
+
workflow run anyway.
|
|
57
|
+
|
|
58
|
+
**Option 2: Re-configure credentials after checkout**
|
|
59
|
+
If you need `persist-credentials: false` for security reasons (e.g., to prevent
|
|
60
|
+
GITHUB_TOKEN from being used by untrusted code), re-add credentials only for the
|
|
61
|
+
steps that need to push.
|
|
62
|
+
|
|
63
|
+
**Option 3: Use SSH instead of HTTPS**
|
|
64
|
+
Configure SSH deploy keys and use an SSH remote URL to avoid HTTPS credential
|
|
65
|
+
issues entirely.
|
|
66
|
+
fix_code:
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: "Remove persist-credentials: false (simplest fix)"
|
|
69
|
+
code: |
|
|
70
|
+
- uses: actions/checkout@v4
|
|
71
|
+
with:
|
|
72
|
+
# REMOVE this line — credentials are scoped to the runner by default
|
|
73
|
+
# persist-credentials: false
|
|
74
|
+
|
|
75
|
+
- name: Commit and push changes
|
|
76
|
+
run: |
|
|
77
|
+
git config user.name "github-actions[bot]"
|
|
78
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
79
|
+
git add .
|
|
80
|
+
git commit -m "chore: auto-update generated files [skip ci]"
|
|
81
|
+
git push
|
|
82
|
+
|
|
83
|
+
- language: yaml
|
|
84
|
+
label: "Re-configure credentials after persist-credentials: false (security-first workflows)"
|
|
85
|
+
code: |
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
with:
|
|
88
|
+
persist-credentials: false # Untrusted code runs between here and push
|
|
89
|
+
|
|
90
|
+
# ... run untrusted/third-party code here ...
|
|
91
|
+
|
|
92
|
+
- name: Re-configure GITHUB_TOKEN for push
|
|
93
|
+
run: |
|
|
94
|
+
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
|
|
95
|
+
# Now git push will work again
|
|
96
|
+
|
|
97
|
+
- name: Push changes
|
|
98
|
+
run: git push
|
|
99
|
+
|
|
100
|
+
- language: yaml
|
|
101
|
+
label: "Use PAT or app token to push (avoids GITHUB_TOKEN limitations)"
|
|
102
|
+
code: |
|
|
103
|
+
- uses: actions/checkout@v4
|
|
104
|
+
with:
|
|
105
|
+
token: ${{ secrets.MY_PAT }} # Use a PAT that has write access
|
|
106
|
+
persist-credentials: true # Default: true — keep this to allow push
|
|
107
|
+
|
|
108
|
+
- name: Push changes
|
|
109
|
+
run: git push
|
|
110
|
+
|
|
111
|
+
prevention:
|
|
112
|
+
- "Do not add `persist-credentials: false` unless you have a specific security reason (e.g., running untrusted fork code)."
|
|
113
|
+
- "If any step after `actions/checkout` needs to push commits or tags, ensure credentials are persisted or re-configured."
|
|
114
|
+
- "When using `git-auto-commit-action` or similar, check upstream docs for compatibility with `persist-credentials: false`."
|
|
115
|
+
- "Prefer `token: ${{ secrets.GITHUB_TOKEN }}` with `persist-credentials: true` over re-configuring remote URLs manually."
|
|
116
|
+
docs:
|
|
117
|
+
- url: "https://github.com/actions/checkout#usage"
|
|
118
|
+
label: "actions/checkout: persist-credentials input"
|
|
119
|
+
- url: "https://github.com/stefanzweifel/git-auto-commit-action/discussions/356"
|
|
120
|
+
label: "git-auto-commit-action#356: persist-credentials: false compatibility"
|
|
121
|
+
- url: "https://github.com/stefanzweifel/git-auto-commit-action/issues/397"
|
|
122
|
+
label: "git-auto-commit-action#397: fatal: could not read Username (checkout v5)"
|
|
123
|
+
- url: "https://github.com/anthropics/claude-code-action/issues/1236"
|
|
124
|
+
label: "claude-code-action#1236: fails when persist-credentials: false"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
id: runner-environment-033
|
|
2
|
+
title: "Windows Runner Default Shell Is PowerShell — Bash Scripts Silently Fail Without shell: bash"
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- windows
|
|
7
|
+
- shell
|
|
8
|
+
- bash
|
|
9
|
+
- powershell
|
|
10
|
+
- default-shell
|
|
11
|
+
- run-step
|
|
12
|
+
- cross-platform
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: "shell:\\s*C:\\\\Windows\\\\system32\\\\bash\\.EXE"
|
|
15
|
+
flags: "i"
|
|
16
|
+
- regex: "The term '.*' is not recognized.*cmdlet.*function.*script"
|
|
17
|
+
flags: "i"
|
|
18
|
+
- regex: "pwsh.*noprofile.*noninteractive.*command"
|
|
19
|
+
flags: "i"
|
|
20
|
+
- regex: "bash.*error.*not.*recognized.*windows"
|
|
21
|
+
flags: "i"
|
|
22
|
+
error_messages:
|
|
23
|
+
- "The term 'set' is not recognized as the name of a cmdlet, function, script file"
|
|
24
|
+
- "Unrecognized token '||' in pipeline"
|
|
25
|
+
- "shell: C:\\Windows\\system32\\bash.EXE --noprofile --norc -e -o pipefail {0}: No such file or directory"
|
|
26
|
+
- "export: The term 'export' is not recognized as the name of a cmdlet"
|
|
27
|
+
root_cause: |
|
|
28
|
+
GitHub Actions uses a different default shell depending on the runner OS:
|
|
29
|
+
- Linux (ubuntu-*): bash
|
|
30
|
+
- macOS (macos-*): bash
|
|
31
|
+
- Windows (windows-*): PowerShell (pwsh)
|
|
32
|
+
|
|
33
|
+
When you write `run:` steps using bash syntax (e.g., `export VAR=value`, `set -e`,
|
|
34
|
+
`||`, `&&`, heredocs, `$(...)` subshells) without explicitly specifying `shell: bash`,
|
|
35
|
+
those steps will execute under PowerShell on Windows runners and fail with confusing
|
|
36
|
+
errors.
|
|
37
|
+
|
|
38
|
+
This is especially problematic in:
|
|
39
|
+
- Matrix workflows that include both Linux and Windows OS variants
|
|
40
|
+
- Composite actions where the shell is not explicitly set per step
|
|
41
|
+
- Reusable workflows called from both Linux and Windows jobs
|
|
42
|
+
- Migration from Linux-only workflows to cross-platform
|
|
43
|
+
|
|
44
|
+
A secondary issue: on Windows runners, if `C:\Windows\System32\bash.exe` exists
|
|
45
|
+
(WSL stub), specifying `shell: bash` may invoke the WSL stub instead of Git Bash
|
|
46
|
+
(`C:\Program Files\Git\bin\bash.EXE`). This causes "No such file or directory"
|
|
47
|
+
errors because the WSL stub requires a Linux distribution to be installed.
|
|
48
|
+
Since runner-images#12646, this is a known issue with no GitHub-side fix.
|
|
49
|
+
|
|
50
|
+
Source: actions/runner#1328 (Unable to run bash scripts on Windows runner)
|
|
51
|
+
Source: runner-images#12646 (Windows WSL bash.exe stub causes shell: bash failures)
|
|
52
|
+
fix: |
|
|
53
|
+
Always explicitly set `shell: bash` on any `run:` step that uses bash syntax.
|
|
54
|
+
|
|
55
|
+
For cross-platform matrix workflows, there are three approaches:
|
|
56
|
+
|
|
57
|
+
1. Explicit shell per step — add `shell: bash` to every bash step.
|
|
58
|
+
|
|
59
|
+
2. Workflow-level default — set `defaults.run.shell: bash` to apply bash to ALL
|
|
60
|
+
run steps in the workflow (be consistent — all steps must then use bash syntax).
|
|
61
|
+
|
|
62
|
+
3. Job-level default — set `defaults.run.shell: bash` at the job level to scope it.
|
|
63
|
+
|
|
64
|
+
If you need PowerShell on Windows AND bash on Linux in the same matrix, use a
|
|
65
|
+
matrix-aware condition to selectively set the shell, or split into separate jobs.
|
|
66
|
+
fix_code:
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: "Fix: explicit shell: bash on individual steps"
|
|
69
|
+
code: |
|
|
70
|
+
jobs:
|
|
71
|
+
test:
|
|
72
|
+
strategy:
|
|
73
|
+
matrix:
|
|
74
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
75
|
+
runs-on: ${{ matrix.os }}
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/checkout@v4
|
|
78
|
+
|
|
79
|
+
# ✅ Always specify shell: bash for bash scripts
|
|
80
|
+
- name: Run build script
|
|
81
|
+
shell: bash # required on Windows — default is PowerShell
|
|
82
|
+
run: |
|
|
83
|
+
export BUILD_ENV=production
|
|
84
|
+
set -euo pipefail
|
|
85
|
+
./scripts/build.sh
|
|
86
|
+
|
|
87
|
+
- language: yaml
|
|
88
|
+
label: "Fix: workflow-level default shell (applies to all run steps)"
|
|
89
|
+
code: |
|
|
90
|
+
name: Cross-Platform CI
|
|
91
|
+
|
|
92
|
+
defaults:
|
|
93
|
+
run:
|
|
94
|
+
shell: bash # override default — all run steps use bash on all OS
|
|
95
|
+
|
|
96
|
+
jobs:
|
|
97
|
+
test:
|
|
98
|
+
strategy:
|
|
99
|
+
matrix:
|
|
100
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
101
|
+
runs-on: ${{ matrix.os }}
|
|
102
|
+
steps:
|
|
103
|
+
- uses: actions/checkout@v4
|
|
104
|
+
- name: Run tests
|
|
105
|
+
run: |
|
|
106
|
+
# This runs in bash on all platforms
|
|
107
|
+
./scripts/run-tests.sh
|
|
108
|
+
|
|
109
|
+
- language: yaml
|
|
110
|
+
label: "Anti-pattern: bash syntax without shell: bash on Windows fails"
|
|
111
|
+
code: |
|
|
112
|
+
# ❌ WRONG on Windows — default shell is PowerShell, bash syntax breaks
|
|
113
|
+
jobs:
|
|
114
|
+
build:
|
|
115
|
+
runs-on: windows-latest
|
|
116
|
+
steps:
|
|
117
|
+
- name: Set env
|
|
118
|
+
run: |
|
|
119
|
+
export DEPLOY_ENV=production # ← ERROR: 'export' not recognized
|
|
120
|
+
echo "env: $DEPLOY_ENV" # ← ERROR: bash variable expansion
|
|
121
|
+
ls -la # ← ERROR: ls -la not valid in pwsh
|
|
122
|
+
|
|
123
|
+
prevention:
|
|
124
|
+
- "Always specify `shell: bash` (or `shell: pwsh`) explicitly on every `run:` step in cross-platform workflows."
|
|
125
|
+
- "Use `defaults.run.shell: bash` at the workflow or job level to reduce repetition."
|
|
126
|
+
- "Test all matrix OS variants in CI — don't assume Linux behavior translates to Windows."
|
|
127
|
+
- "For PowerShell-specific steps on Windows, use `shell: pwsh` explicitly rather than relying on the default."
|
|
128
|
+
- "Avoid relying on the runner's WSL bash stub on Windows — it requires a WSL distribution installed."
|
|
129
|
+
docs:
|
|
130
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/setting-a-default-shell-and-working-directory"
|
|
131
|
+
label: "GitHub Docs: Setting a default shell and working directory"
|
|
132
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell"
|
|
133
|
+
label: "GitHub Docs: jobs.<job_id>.steps[*].shell"
|
|
134
|
+
- url: "https://github.com/actions/runner/issues/1328"
|
|
135
|
+
label: "actions/runner#1328: Unable to run bash scripts on Windows runner"
|
|
136
|
+
- url: "https://github.com/actions/runner-images/issues/12646"
|
|
137
|
+
label: "runner-images#12646: Windows WSL bash.exe stub causes shell: bash failures"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
id: silent-failures-013
|
|
2
|
+
title: "Shallow Clone (fetch-depth: 1) Silently Breaks Git History Operations"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- checkout
|
|
7
|
+
- fetch-depth
|
|
8
|
+
- shallow-clone
|
|
9
|
+
- git-describe
|
|
10
|
+
- changelog
|
|
11
|
+
- versioning
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "fatal:.*No names found.*cannot describe anything"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "fatal:.*no tag can describe"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "git describe.*failed with exit code 128"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "fetch-depth.*1"
|
|
20
|
+
flags: "i"
|
|
21
|
+
error_messages:
|
|
22
|
+
- "fatal: No names found, cannot describe anything."
|
|
23
|
+
- "fatal: no tag can describe ''"
|
|
24
|
+
- "error: process completed with exit code 128"
|
|
25
|
+
- "git describe --tags --abbrev=0 failed"
|
|
26
|
+
root_cause: |
|
|
27
|
+
`actions/checkout` defaults to `fetch-depth: 1`, which creates a shallow clone containing
|
|
28
|
+
only the most recent commit. This means:
|
|
29
|
+
- No commit history beyond the latest commit is available
|
|
30
|
+
- No tags are fetched (unless `fetch-tags: true` is set separately)
|
|
31
|
+
- Git operations that need history context fail silently or produce wrong results
|
|
32
|
+
|
|
33
|
+
Affected operations include:
|
|
34
|
+
- `git describe --tags` — fails with "No names found, cannot describe anything"
|
|
35
|
+
- `git log --oneline HEAD~10..HEAD` — returns nothing or errors
|
|
36
|
+
- Semantic versioning tools (semantic-release, standard-version, release-please)
|
|
37
|
+
- Changelog generators that diff HEAD against previous tags
|
|
38
|
+
- `git rev-list --count HEAD` — returns "1" instead of full commit count
|
|
39
|
+
- GitVersion, MinVer, and similar tag-based version calculators
|
|
40
|
+
|
|
41
|
+
The failure is silent in many cases: the step appears to succeed, but produces an
|
|
42
|
+
empty string, "0.0.0", or a fallback version instead of the expected semver.
|
|
43
|
+
|
|
44
|
+
Tracked in actions/checkout#217 (RFC: fetch-depth: 1 and not cloning tags are dangerous
|
|
45
|
+
defaults) with 22 thumbs-up reactions.
|
|
46
|
+
fix: |
|
|
47
|
+
**Option 1 (recommended for most cases): Fetch full history**
|
|
48
|
+
Set `fetch-depth: 0` to fetch all commits and tags.
|
|
49
|
+
|
|
50
|
+
**Option 2: Fetch only enough history**
|
|
51
|
+
For large repos, fetch only the depth needed (e.g., last 50 commits). Use
|
|
52
|
+
`fetch-tags: true` (available in actions/checkout@v4) to fetch all tags without
|
|
53
|
+
the full commit history.
|
|
54
|
+
|
|
55
|
+
**Option 3: Unshallow after checkout**
|
|
56
|
+
Fetch the necessary history lazily with `git fetch --unshallow` or
|
|
57
|
+
`git fetch --tags --unshallow` as a subsequent step.
|
|
58
|
+
fix_code:
|
|
59
|
+
- language: yaml
|
|
60
|
+
label: "Full history checkout (simplest fix)"
|
|
61
|
+
code: |
|
|
62
|
+
- uses: actions/checkout@v4
|
|
63
|
+
with:
|
|
64
|
+
fetch-depth: 0 # 0 = full history; default 1 = shallow
|
|
65
|
+
|
|
66
|
+
- name: Generate changelog
|
|
67
|
+
run: git log --oneline $(git describe --tags --abbrev=0 @^)..@ --no-merges
|
|
68
|
+
|
|
69
|
+
- language: yaml
|
|
70
|
+
label: "Fetch tags only without full history (faster for large repos)"
|
|
71
|
+
code: |
|
|
72
|
+
- uses: actions/checkout@v4
|
|
73
|
+
with:
|
|
74
|
+
fetch-depth: 0 # Required for tag-based versioning
|
|
75
|
+
fetch-tags: true # Explicitly fetch all tags (actions/checkout v4.1.1+)
|
|
76
|
+
|
|
77
|
+
- language: yaml
|
|
78
|
+
label: "Unshallow lazily if full history is not needed upfront"
|
|
79
|
+
code: |
|
|
80
|
+
- uses: actions/checkout@v4
|
|
81
|
+
# fetch-depth: 1 (default — shallow clone)
|
|
82
|
+
|
|
83
|
+
- name: Fetch tags for versioning
|
|
84
|
+
run: |
|
|
85
|
+
git fetch --tags --force
|
|
86
|
+
# Or for full history:
|
|
87
|
+
# git fetch --unshallow
|
|
88
|
+
|
|
89
|
+
- name: Get version from tags
|
|
90
|
+
run: git describe --tags --abbrev=0
|
|
91
|
+
|
|
92
|
+
prevention:
|
|
93
|
+
- "Add `fetch-depth: 0` to any checkout step that precedes git history, tag, or versioning operations."
|
|
94
|
+
- "Set `fetch-tags: true` in actions/checkout@v4 when using tag-based versioning tools."
|
|
95
|
+
- "When using semantic-release, release-please, or GitVersion, always use `fetch-depth: 0`."
|
|
96
|
+
- "Test locally with `git clone --depth 1` to reproduce the shallow clone environment before debugging in CI."
|
|
97
|
+
- "Audit all checkout steps in release workflows — shallow clones are fine for build/test but break release automation."
|
|
98
|
+
docs:
|
|
99
|
+
- url: "https://github.com/actions/checkout#usage"
|
|
100
|
+
label: "actions/checkout: fetch-depth and fetch-tags inputs"
|
|
101
|
+
- url: "https://github.com/actions/checkout/issues/217"
|
|
102
|
+
label: "actions/checkout#217: RFC — fetch-depth: 1 and not cloning tags are dangerous defaults"
|
|
103
|
+
- url: "https://stackoverflow.com/questions/66349002/get-latest-tag-git-describe-tags-when-repo-is-cloned-with-depth-1"
|
|
104
|
+
label: "Stack Overflow: git describe fails when cloned with depth=1"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
id: silent-failures-014
|
|
2
|
+
title: "pull_request github.ref Is refs/pull/N/merge — Branch-Name Conditions Silently Evaluate to False"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- pull_request
|
|
7
|
+
- github-ref
|
|
8
|
+
- merge-ref
|
|
9
|
+
- branch-name
|
|
10
|
+
- if-condition
|
|
11
|
+
- head-ref
|
|
12
|
+
- base-ref
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: "github\\.ref\\s*==\\s*['\"]refs/heads/"
|
|
15
|
+
flags: "i"
|
|
16
|
+
- regex: "GITHUB_REF.*refs/pull/\\d+/merge"
|
|
17
|
+
flags: "i"
|
|
18
|
+
- regex: "github\\.ref.*startsWith.*refs/heads"
|
|
19
|
+
flags: "i"
|
|
20
|
+
error_messages:
|
|
21
|
+
- "refs/pull/123/merge"
|
|
22
|
+
- "Conditional check 'github.ref == refs/heads/main' evaluated to false on pull_request event"
|
|
23
|
+
- "GITHUB_REF=refs/pull/47/merge"
|
|
24
|
+
root_cause: |
|
|
25
|
+
When a workflow is triggered by the `pull_request` (or `pull_request_target`) event,
|
|
26
|
+
`github.ref` is set to `refs/pull/<number>/merge` — NOT the source branch name.
|
|
27
|
+
|
|
28
|
+
This `refs/pull/<N>/merge` is a synthetic Git reference created by GitHub representing
|
|
29
|
+
the prospective merge commit (the merge of the PR's head branch into the base branch).
|
|
30
|
+
It does not correspond to any named branch.
|
|
31
|
+
|
|
32
|
+
Developers commonly write conditions expecting `github.ref` to contain the branch name:
|
|
33
|
+
- `if: github.ref == 'refs/heads/feature-branch'` → always false on PR events
|
|
34
|
+
- `if: startsWith(github.ref, 'refs/heads/')` → always false on PR events
|
|
35
|
+
- `if: github.ref != 'refs/heads/main'` → always true on PR events
|
|
36
|
+
|
|
37
|
+
These conditions silently pass or silently skip — no error is reported. The job
|
|
38
|
+
simply runs (or doesn't run) with no indication that the ref check was wrong.
|
|
39
|
+
|
|
40
|
+
The correct context variables for PR events are:
|
|
41
|
+
- `github.head_ref` — the source branch name (e.g., "feature/my-branch")
|
|
42
|
+
- `github.base_ref` — the target branch name (e.g., "main")
|
|
43
|
+
- `github.event.pull_request.head.ref` — same as head_ref
|
|
44
|
+
- `github.event.pull_request.base.ref` — same as base_ref
|
|
45
|
+
|
|
46
|
+
Note: `github.head_ref` and `github.base_ref` are ONLY set for pull_request events.
|
|
47
|
+
For push events, use `github.ref_name` instead.
|
|
48
|
+
|
|
49
|
+
Source: Stack Overflow #68708792 (what is github.ref when merging PR to master)
|
|
50
|
+
Source: Stack Overflow #78162051 (obtain branch name in github action on pull_request)
|
|
51
|
+
Docs: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#github-context
|
|
52
|
+
fix: |
|
|
53
|
+
Use the correct context variable depending on what information you need:
|
|
54
|
+
|
|
55
|
+
For the PR source branch:
|
|
56
|
+
Use `github.head_ref` (not `github.ref`)
|
|
57
|
+
|
|
58
|
+
For the PR target branch:
|
|
59
|
+
Use `github.base_ref` (not `github.ref`)
|
|
60
|
+
|
|
61
|
+
For branch-scoped conditions that need to work across BOTH push and pull_request:
|
|
62
|
+
Use `github.ref_name` (gives branch/tag name without refs/heads/ prefix)
|
|
63
|
+
Or use `github.event_name` to gate the condition by event type
|
|
64
|
+
|
|
65
|
+
To check if a push or PR targets the main branch:
|
|
66
|
+
- Push: `github.ref == 'refs/heads/main'`
|
|
67
|
+
- PR: `github.base_ref == 'main'`
|
|
68
|
+
- Both: use separate conditions gated by `github.event_name`
|
|
69
|
+
fix_code:
|
|
70
|
+
- language: yaml
|
|
71
|
+
label: "Fix: use github.head_ref for PR source branch, base_ref for target"
|
|
72
|
+
code: |
|
|
73
|
+
jobs:
|
|
74
|
+
deploy:
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
steps:
|
|
77
|
+
# ❌ WRONG: github.ref on pull_request is refs/pull/N/merge — never matches heads/*
|
|
78
|
+
- name: Wrong branch check
|
|
79
|
+
if: github.ref == 'refs/heads/feature-branch' # always false on PR!
|
|
80
|
+
run: echo "this never runs on PR events"
|
|
81
|
+
|
|
82
|
+
# ✅ CORRECT: use head_ref for the source branch name
|
|
83
|
+
- name: Correct branch check
|
|
84
|
+
if: github.head_ref == 'feature-branch'
|
|
85
|
+
run: echo "this runs when PR source branch is feature-branch"
|
|
86
|
+
|
|
87
|
+
# ✅ CORRECT: use base_ref for the target branch name
|
|
88
|
+
- name: Check if targeting main
|
|
89
|
+
if: github.base_ref == 'main'
|
|
90
|
+
run: echo "this PR is targeting main"
|
|
91
|
+
|
|
92
|
+
- language: yaml
|
|
93
|
+
label: "Cross-event condition: works for both push and pull_request"
|
|
94
|
+
code: |
|
|
95
|
+
jobs:
|
|
96
|
+
conditional:
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
steps:
|
|
99
|
+
# Works for both push (github.ref_name) and PR (github.base_ref)
|
|
100
|
+
- name: Is this targeting/on main?
|
|
101
|
+
if: |
|
|
102
|
+
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
|
103
|
+
(github.event_name == 'pull_request' && github.base_ref == 'main')
|
|
104
|
+
run: echo "targeting or on main branch"
|
|
105
|
+
|
|
106
|
+
# Simpler: github.ref_name works for push; for PR it returns the merge ref number
|
|
107
|
+
# Best to keep push and pull_request logic separate with event_name guards
|
|
108
|
+
|
|
109
|
+
- language: yaml
|
|
110
|
+
label: "Debug step: print all relevant refs for troubleshooting"
|
|
111
|
+
code: |
|
|
112
|
+
- name: Debug refs
|
|
113
|
+
run: |
|
|
114
|
+
echo "event_name: ${{ github.event_name }}"
|
|
115
|
+
echo "ref: ${{ github.ref }}"
|
|
116
|
+
echo "ref_name: ${{ github.ref_name }}"
|
|
117
|
+
echo "head_ref: ${{ github.head_ref }}"
|
|
118
|
+
echo "base_ref: ${{ github.base_ref }}"
|
|
119
|
+
# On push/branch: ref=refs/heads/main, head_ref='', base_ref=''
|
|
120
|
+
# On pull_request: ref=refs/pull/47/merge, head_ref=feature-x, base_ref=main
|
|
121
|
+
|
|
122
|
+
prevention:
|
|
123
|
+
- "Never use `github.ref` to get the branch name in `pull_request` triggered workflows — it is not a branch ref."
|
|
124
|
+
- "Use `github.head_ref` for the PR source branch and `github.base_ref` for the target branch."
|
|
125
|
+
- "For conditions that span both push and pull_request events, gate on `github.event_name` first."
|
|
126
|
+
- "Add a debug step printing `github.ref`, `github.head_ref`, and `github.base_ref` when debugging trigger conditions."
|
|
127
|
+
- "Use `github.ref_name` (branch/tag name without prefix) for push events instead of stripping `refs/heads/` manually."
|
|
128
|
+
docs:
|
|
129
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#github-context"
|
|
130
|
+
label: "GitHub Docs: github context (ref, head_ref, base_ref, ref_name)"
|
|
131
|
+
- url: "https://stackoverflow.com/questions/68708792/what-is-github-ref-when-merging-pr-to-master"
|
|
132
|
+
label: "Stack Overflow: What is github.ref when merging PR to master?"
|
|
133
|
+
- url: "https://stackoverflow.com/questions/78162051/obtain-branch-name-in-github-action-on-pull-request"
|
|
134
|
+
label: "Stack Overflow: Obtain branch name in github action on pull request"
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
id: triggers-012
|
|
2
|
+
title: "Tag Pushes Bypass paths: Filter — Workflow Fires on Every Tag Regardless of Changed Files"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- push
|
|
7
|
+
- tags
|
|
8
|
+
- paths-filter
|
|
9
|
+
- tag-push
|
|
10
|
+
- trigger
|
|
11
|
+
- branches
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "on:\\s*\\n\\s*push:\\s*\\n\\s*(\\w[^\\n]*\\n\\s*)*paths:"
|
|
14
|
+
flags: "im"
|
|
15
|
+
- regex: "Triggered by refs/tags/"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "push.*paths.*tags.*ignored"
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "Run triggered by push to refs/tags/v1.0.0"
|
|
21
|
+
- "Evaluation skipped: tag push does not evaluate path filters"
|
|
22
|
+
root_cause: |
|
|
23
|
+
GitHub Actions evaluates `paths:` and `paths-ignore:` filters ONLY for branch pushes.
|
|
24
|
+
When a tag is pushed, path filters are completely bypassed — the workflow runs regardless
|
|
25
|
+
of which files were changed.
|
|
26
|
+
|
|
27
|
+
This means a workflow like:
|
|
28
|
+
|
|
29
|
+
on:
|
|
30
|
+
push:
|
|
31
|
+
paths:
|
|
32
|
+
- "src/**"
|
|
33
|
+
|
|
34
|
+
will fire on every tag push (e.g., `git push origin v1.2.3`) even if no files under
|
|
35
|
+
`src/` were modified in the tagged commit.
|
|
36
|
+
|
|
37
|
+
Additionally, if a workflow has `on: push:` with NO branches/tags filter, a tag push
|
|
38
|
+
produces a `push` webhook event that triggers the workflow. Developers expect tag pushes
|
|
39
|
+
to be governed by path filters, but the docs explicitly state path filters are not
|
|
40
|
+
evaluated for tag events.
|
|
41
|
+
|
|
42
|
+
Source: actions/runner#3933 (path filters not respected for tag pushes)
|
|
43
|
+
Source: Stack Overflow #76037078 (paths also triggered when pushing a new tag)
|
|
44
|
+
Docs: https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow#using-filters-to-target-specific-paths-for-pull-request-or-push-events
|
|
45
|
+
fix: |
|
|
46
|
+
If you want path-filtered push workflows to run ONLY on branch pushes (not tag pushes),
|
|
47
|
+
explicitly add a `tags-ignore: ["**"]` filter or restrict to branches explicitly.
|
|
48
|
+
|
|
49
|
+
Option A — Add `tags-ignore` to suppress all tag events:
|
|
50
|
+
on:
|
|
51
|
+
push:
|
|
52
|
+
paths:
|
|
53
|
+
- "src/**"
|
|
54
|
+
tags-ignore:
|
|
55
|
+
- "**"
|
|
56
|
+
|
|
57
|
+
Option B — Add an explicit `branches` filter (only branch pushes match):
|
|
58
|
+
on:
|
|
59
|
+
push:
|
|
60
|
+
branches:
|
|
61
|
+
- "**" # all branches
|
|
62
|
+
paths:
|
|
63
|
+
- "src/**"
|
|
64
|
+
|
|
65
|
+
Note: Option B is equivalent — defining `branches` or `tags` implicitly excludes the
|
|
66
|
+
other git ref type from triggering the workflow.
|
|
67
|
+
fix_code:
|
|
68
|
+
- language: yaml
|
|
69
|
+
label: "Add tags-ignore to prevent tag pushes from bypassing path filters"
|
|
70
|
+
code: |
|
|
71
|
+
on:
|
|
72
|
+
push:
|
|
73
|
+
# Path filter only works for branch pushes — add tags-ignore to prevent
|
|
74
|
+
# tag pushes from triggering this workflow unconditionally.
|
|
75
|
+
tags-ignore:
|
|
76
|
+
- "**"
|
|
77
|
+
paths:
|
|
78
|
+
- "src/**"
|
|
79
|
+
- "lib/**"
|
|
80
|
+
|
|
81
|
+
- language: yaml
|
|
82
|
+
label: "Restrict to all branches (implicitly excludes tags)"
|
|
83
|
+
code: |
|
|
84
|
+
on:
|
|
85
|
+
push:
|
|
86
|
+
branches:
|
|
87
|
+
- "**" # matches all branch pushes, ignores tag pushes
|
|
88
|
+
paths:
|
|
89
|
+
- "src/**"
|
|
90
|
+
|
|
91
|
+
- language: yaml
|
|
92
|
+
label: "Separate workflows for branch CI vs tag release"
|
|
93
|
+
code: |
|
|
94
|
+
# ci.yml — triggered by branch pushes with path filtering
|
|
95
|
+
on:
|
|
96
|
+
push:
|
|
97
|
+
branches:
|
|
98
|
+
- main
|
|
99
|
+
- "feature/**"
|
|
100
|
+
paths:
|
|
101
|
+
- "src/**"
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
# release.yml — triggered by version tags
|
|
105
|
+
on:
|
|
106
|
+
push:
|
|
107
|
+
tags:
|
|
108
|
+
- "v*.*.*"
|
|
109
|
+
|
|
110
|
+
prevention:
|
|
111
|
+
- "Never rely on `paths:` filters to suppress tag-push triggers — they are not evaluated for tags."
|
|
112
|
+
- "Always add an explicit `branches:` or `tags-ignore: [\"**\"]` when your workflow is path-filtered and meant only for code changes."
|
|
113
|
+
- "Keep branch-CI workflows and release/tag workflows in separate files to avoid trigger ambiguity."
|
|
114
|
+
- "Test your trigger logic with `act` locally — push a tag and verify the workflow does not fire unexpectedly."
|
|
115
|
+
docs:
|
|
116
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow#using-filters-to-target-specific-paths-for-pull-request-or-push-events"
|
|
117
|
+
label: "GitHub Docs: Using filters to target specific paths"
|
|
118
|
+
- url: "https://github.com/actions/runner/issues/3933"
|
|
119
|
+
label: "actions/runner#3933: Path filters not respected for tag pushes"
|
|
120
|
+
- url: "https://stackoverflow.com/questions/76037078/why-is-my-github-action-on-paths-also-triggered-when-pushing-a-new-tag"
|
|
121
|
+
label: "Stack Overflow: Why is paths: also triggered when pushing a new tag?"
|
|
122
|
+
- url: "https://stackoverflow.com/questions/70743715/how-do-i-configure-a-github-actions-workflow-so-it-does-not-run-on-a-tag-push"
|
|
123
|
+
label: "Stack Overflow: How to prevent workflow from running on tag push"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
id: triggers-011
|
|
2
|
+
title: "workflow_dispatch branches/paths Filters Silently Ignored or Cause Validation Error"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: warning
|
|
5
|
+
tags:
|
|
6
|
+
- workflow_dispatch
|
|
7
|
+
- branches
|
|
8
|
+
- paths
|
|
9
|
+
- filters
|
|
10
|
+
- trigger
|
|
11
|
+
- manual-dispatch
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "Unexpected value 'branches'"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "workflow_dispatch.*branches.*paths"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "The workflow is not valid.*Unexpected value"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "on\\.workflow_dispatch.*branches"
|
|
20
|
+
flags: "i"
|
|
21
|
+
error_messages:
|
|
22
|
+
- "The workflow is not valid. .github/workflows/deploy.yml (Line: X, Col: Y): Unexpected value 'branches'"
|
|
23
|
+
- "Unexpected value 'branches'"
|
|
24
|
+
- "Unexpected value 'tags'"
|
|
25
|
+
- "Unexpected value 'paths'"
|
|
26
|
+
root_cause: |
|
|
27
|
+
`workflow_dispatch` is a manual trigger — it runs when a user (or the API) explicitly
|
|
28
|
+
triggers the workflow. Because it is not event-driven by a push or pull request, the
|
|
29
|
+
`branches`, `paths`, `tags`, and `paths-ignore` filters that apply to push/pull_request
|
|
30
|
+
events are **not valid** for `workflow_dispatch`.
|
|
31
|
+
|
|
32
|
+
Two failure modes exist:
|
|
33
|
+
|
|
34
|
+
**Mode 1: Validation error (branches, tags)**
|
|
35
|
+
Adding `branches` or `tags` under `on.workflow_dispatch` now produces a schema
|
|
36
|
+
validation error: "Unexpected value 'branches'" or "Unexpected value 'tags'".
|
|
37
|
+
GitHub used to silently ignore these keys (pre-2022), leading to copy-paste templates
|
|
38
|
+
with these keys still floating around. The workflow may fail to queue at all.
|
|
39
|
+
|
|
40
|
+
**Mode 2: Silent ignore (paths)**
|
|
41
|
+
The `paths` filter under `on.workflow_dispatch` was silently ignored historically.
|
|
42
|
+
The workflow runs regardless of which files changed (or didn't change), because
|
|
43
|
+
workflow_dispatch has no file-change context to filter on.
|
|
44
|
+
|
|
45
|
+
Developers commonly copy a push-triggered workflow and add workflow_dispatch without
|
|
46
|
+
removing the push-specific filters, producing this mistake:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
on:
|
|
50
|
+
push:
|
|
51
|
+
branches: [main]
|
|
52
|
+
paths: ['src/**']
|
|
53
|
+
workflow_dispatch:
|
|
54
|
+
branches: [main] # ← INVALID for workflow_dispatch
|
|
55
|
+
paths: ['src/**'] # ← silently ignored for workflow_dispatch
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Tracked in github/docs#34884 ("documentation on workflow_dispatch is not
|
|
59
|
+
correct/complete") and blog post by Jon Gallant (2022): "workflow_dispatch never
|
|
60
|
+
supported branches, but GH silently ignored it."
|
|
61
|
+
fix: |
|
|
62
|
+
Remove `branches`, `paths`, `tags`, and `paths-ignore` from the `workflow_dispatch`
|
|
63
|
+
block entirely. These filters only apply to `push`, `pull_request`, and
|
|
64
|
+
`pull_request_target` triggers.
|
|
65
|
+
|
|
66
|
+
If you want manual dispatch to only be available on specific branches, use the
|
|
67
|
+
GitHub Actions UI branch selector at runtime — it allows choosing which branch to
|
|
68
|
+
run the workflow on during manual dispatch without needing a filter in the YAML.
|
|
69
|
+
|
|
70
|
+
If you need path-based conditional logic in a manually-triggered workflow, use
|
|
71
|
+
`dorny/paths-filter` or a custom `git diff` step to check which files changed after
|
|
72
|
+
the workflow starts.
|
|
73
|
+
fix_code:
|
|
74
|
+
- language: yaml
|
|
75
|
+
label: "Remove invalid filters from workflow_dispatch block"
|
|
76
|
+
code: |
|
|
77
|
+
on:
|
|
78
|
+
push:
|
|
79
|
+
branches: [main]
|
|
80
|
+
paths: ['src/**'] # ← valid here for push
|
|
81
|
+
workflow_dispatch:
|
|
82
|
+
# ← Do NOT add branches/paths/tags here — they are not supported
|
|
83
|
+
# Use inputs if you need runtime parameterization:
|
|
84
|
+
inputs:
|
|
85
|
+
environment:
|
|
86
|
+
description: "Target environment"
|
|
87
|
+
required: true
|
|
88
|
+
default: "staging"
|
|
89
|
+
type: choice
|
|
90
|
+
options: [staging, production]
|
|
91
|
+
|
|
92
|
+
- language: yaml
|
|
93
|
+
label: "Path-based conditional logic inside a manually-dispatched workflow"
|
|
94
|
+
code: |
|
|
95
|
+
on:
|
|
96
|
+
workflow_dispatch:
|
|
97
|
+
|
|
98
|
+
jobs:
|
|
99
|
+
check-and-deploy:
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
steps:
|
|
102
|
+
- uses: actions/checkout@v4
|
|
103
|
+
with:
|
|
104
|
+
fetch-depth: 2
|
|
105
|
+
|
|
106
|
+
- name: Check changed paths
|
|
107
|
+
id: filter
|
|
108
|
+
uses: dorny/paths-filter@v3
|
|
109
|
+
with:
|
|
110
|
+
filters: |
|
|
111
|
+
src:
|
|
112
|
+
- 'src/**'
|
|
113
|
+
|
|
114
|
+
- name: Deploy (only if src changed)
|
|
115
|
+
if: steps.filter.outputs.src == 'true'
|
|
116
|
+
run: echo "Deploying..."
|
|
117
|
+
|
|
118
|
+
prevention:
|
|
119
|
+
- "Never copy `branches`, `paths`, or `tags` filters from a `push` block into a `workflow_dispatch` block."
|
|
120
|
+
- "Treat `workflow_dispatch` as a parameter-based trigger, not a filter-based one — use `inputs` instead."
|
|
121
|
+
- "If the GitHub Actions linter flags 'Unexpected value branches', remove it from the workflow_dispatch block."
|
|
122
|
+
- "Use the branch selector in the GitHub Actions UI for branch scoping at runtime."
|
|
123
|
+
docs:
|
|
124
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_dispatch"
|
|
125
|
+
label: "GitHub Docs: workflow_dispatch event trigger"
|
|
126
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpathspaths-ignore"
|
|
127
|
+
label: "GitHub Docs: paths filter (push/pull_request only)"
|
|
128
|
+
- url: "https://github.com/github/docs/issues/34884"
|
|
129
|
+
label: "github/docs#34884: documentation on workflow_dispatch is not correct/complete"
|
|
130
|
+
- url: "https://blog.jongallant.com/2022/04/github-actions-failing-with-unexpected-value-branches"
|
|
131
|
+
label: "Jon Gallant: workflow_dispatch never supported branches (2022)"
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
id: yaml-syntax-017
|
|
2
|
+
title: "Matrix include Entry Without Matching Combination Creates Unexpected Standalone Jobs"
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: warning
|
|
5
|
+
tags:
|
|
6
|
+
- matrix
|
|
7
|
+
- include
|
|
8
|
+
- strategy
|
|
9
|
+
- extra-jobs
|
|
10
|
+
- standalone
|
|
11
|
+
- job-count
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "strategy.*matrix.*include"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "matrix.*include.*extra.*job"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "include.*os.*version"
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "strategy.matrix.include"
|
|
21
|
+
- "unexpected job combination in matrix"
|
|
22
|
+
root_cause: |
|
|
23
|
+
GitHub Actions matrix `include` entries serve two purposes:
|
|
24
|
+
1. **Add variables to existing combinations** — when the entry shares at least one
|
|
25
|
+
key-value pair with an existing matrix combination, it adds/overrides variables
|
|
26
|
+
for that specific combination only.
|
|
27
|
+
2. **Create new standalone jobs** — when the entry does NOT match any existing matrix
|
|
28
|
+
combination, GitHub Actions treats it as an entirely NEW matrix job on its own.
|
|
29
|
+
|
|
30
|
+
This second behavior surprises developers who expect `include` to only add metadata
|
|
31
|
+
to existing jobs. Example that creates an unexpected extra job:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
strategy:
|
|
35
|
+
matrix:
|
|
36
|
+
os: [ubuntu-latest, windows-latest]
|
|
37
|
+
node: [18, 20]
|
|
38
|
+
include:
|
|
39
|
+
- os: macos-latest # ← No matching {os: macos-latest} in matrix
|
|
40
|
+
node: 20
|
|
41
|
+
experimental: true # ← This creates a BRAND NEW 5th job, not expected
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The matrix produces: 4 jobs from combinations (ubuntu×18, ubuntu×20,
|
|
45
|
+
windows×18, windows×20) PLUS an extra 5th job for (macos-latest × 20 × experimental).
|
|
46
|
+
|
|
47
|
+
This can also cause subtle bugs when a typo in an `include` value fails to match
|
|
48
|
+
the existing combination, silently creating an extra duplicate-like job:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
include:
|
|
52
|
+
- os: ubuntu-latest # os key matches!
|
|
53
|
+
node: 18
|
|
54
|
+
runs-long: true # Variable added to ubuntu-latest×18 job ✓
|
|
55
|
+
- os: ubuntu # TYPO: doesn't match "ubuntu-latest" → new standalone job ✗
|
|
56
|
+
node: 18
|
|
57
|
+
experimental: true
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Tracked in github/docs#23322 ("Documentation for jobs matrix strategy seems incorrect").
|
|
61
|
+
fix: |
|
|
62
|
+
**To add variables to existing combinations:** Ensure at least one key in the
|
|
63
|
+
`include` entry exactly matches an existing combination value. The match is
|
|
64
|
+
case-sensitive.
|
|
65
|
+
|
|
66
|
+
**To intentionally add a new job:** This is valid behavior — just document it clearly
|
|
67
|
+
in the workflow and be aware of the extra job in your branch protection rules.
|
|
68
|
+
|
|
69
|
+
**To prevent accidental extra jobs:** Review the job count after adding include entries.
|
|
70
|
+
The total should be: (product of all matrix dimensions) + (number of include entries
|
|
71
|
+
that don't match any combination).
|
|
72
|
+
fix_code:
|
|
73
|
+
- language: yaml
|
|
74
|
+
label: "include entry that correctly adds a variable to an existing combination"
|
|
75
|
+
code: |
|
|
76
|
+
strategy:
|
|
77
|
+
matrix:
|
|
78
|
+
os: [ubuntu-latest, windows-latest]
|
|
79
|
+
node: [18, 20]
|
|
80
|
+
include:
|
|
81
|
+
# This MATCHES ubuntu-latest×18 — adds 'experimental: true' to that job only
|
|
82
|
+
- os: ubuntu-latest # ← must exactly match the matrix value
|
|
83
|
+
node: 18 # ← must exactly match the matrix value
|
|
84
|
+
experimental: true
|
|
85
|
+
|
|
86
|
+
# This MATCHES all ubuntu-latest jobs (node 18 AND 20)
|
|
87
|
+
# because os key matches and node is not specified
|
|
88
|
+
- os: ubuntu-latest
|
|
89
|
+
runs-slow: true # added to ubuntu-latest×18 AND ubuntu-latest×20
|
|
90
|
+
|
|
91
|
+
- language: yaml
|
|
92
|
+
label: "Intentional standalone job via include (extra OS not in base matrix)"
|
|
93
|
+
code: |
|
|
94
|
+
strategy:
|
|
95
|
+
matrix:
|
|
96
|
+
os: [ubuntu-latest, windows-latest]
|
|
97
|
+
node: [18, 20]
|
|
98
|
+
include:
|
|
99
|
+
# Intentional extra job — macos is not in the base matrix
|
|
100
|
+
# Documents clearly that this creates job #5 (macos×20)
|
|
101
|
+
- os: macos-latest
|
|
102
|
+
node: 20
|
|
103
|
+
experimental: true
|
|
104
|
+
# Total jobs: 4 (base combinations) + 1 (macos standalone) = 5
|
|
105
|
+
|
|
106
|
+
- language: yaml
|
|
107
|
+
label: "Verify total job count matches expectations with exclude"
|
|
108
|
+
code: |
|
|
109
|
+
strategy:
|
|
110
|
+
matrix:
|
|
111
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
112
|
+
node: [18, 20]
|
|
113
|
+
exclude:
|
|
114
|
+
# Exclude expensive macos×18 — not needed
|
|
115
|
+
- os: macos-latest
|
|
116
|
+
node: 18
|
|
117
|
+
include:
|
|
118
|
+
# Add experimental flag to ubuntu-latest×20 only
|
|
119
|
+
- os: ubuntu-latest
|
|
120
|
+
node: 20
|
|
121
|
+
experimental: true
|
|
122
|
+
# Total jobs: (3×2) - 1 excluded = 5 jobs; 0 new from include (it matches existing)
|
|
123
|
+
|
|
124
|
+
prevention:
|
|
125
|
+
- "Always verify the total expected job count after adding `include` entries."
|
|
126
|
+
- "Ensure `include` key-value pairs exactly match (case-sensitive) the base matrix values."
|
|
127
|
+
- "Use a comment in the workflow to document the expected total number of jobs."
|
|
128
|
+
- "If a typo causes an unexpected extra job, it will show up as a job with no matching `if:` context — watch for jobs that run but shouldn't."
|
|
129
|
+
- "Use `exclude` to remove specific combinations instead of relying solely on `include` overrides."
|
|
130
|
+
docs:
|
|
131
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow#expanding-or-adding-matrix-configurations"
|
|
132
|
+
label: "GitHub Docs: Expanding or adding matrix configurations with include"
|
|
133
|
+
- url: "https://github.com/github/docs/issues/23322"
|
|
134
|
+
label: "github/docs#23322: Documentation for jobs matrix strategy seems incorrect"
|
|
135
|
+
- url: "https://stackoverflow.com/questions/78821409/github-actions-matrix-job-running-despite-false-conditions"
|
|
136
|
+
label: "Stack Overflow: GitHub Actions matrix job running despite false conditions"
|
package/package.json
CHANGED