@htekdev/actions-debugger 1.0.112 → 1.0.114
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-corrupt-on-cancel-during-restore-save-always.yml +136 -0
- package/errors/caching-artifacts/restore-keys-asterisk-literal-not-glob.yml +107 -0
- package/errors/concurrency-timing/pull-request-review-shared-concurrency-cancels-ci.yml +131 -0
- package/errors/known-unsolved/github-script-esm-not-supported.yml +111 -0
- package/errors/known-unsolved/job-outputs-string-only-no-array-object.yml +142 -0
- package/errors/permissions-auth/oidc-immutable-sub-claim-new-repo-trust-policy-mismatch.yml +122 -0
- package/errors/permissions-auth/permissions-auth-064.yml +122 -0
- package/errors/permissions-auth/permissions-auth-065.yml +97 -0
- package/errors/permissions-auth/permissions-auth-066.yml +129 -0
- package/errors/runner-environment/arc-kubernetes-checkout-circular-json-container-hook.yml +101 -0
- package/errors/runner-environment/cache-restore-windows-runner-silent-crash.yml +130 -0
- package/errors/runner-environment/git-248-fetch-tags-shallow-clone-regression.yml +100 -0
- package/errors/runner-environment/javascript-actions-alpine-arm64-not-supported.yml +121 -0
- package/errors/runner-environment/runner-environment-185.yml +88 -0
- package/errors/runner-environment/runner-environment-186.yml +95 -0
- package/errors/runner-environment/runner-environment-187.yml +90 -0
- package/errors/runner-environment/runner-environment-188.yml +96 -0
- package/errors/runner-environment/runner-environment-191.yml +147 -0
- package/errors/runner-environment/runner-environment-192.yml +144 -0
- package/errors/runner-environment/runner-environment-193.yml +136 -0
- package/errors/runner-environment/runner-environment-194.yml +86 -0
- package/errors/silent-failures/checkout-v6-clean-false-deletes-workspace-on-repo-change.yml +119 -0
- package/errors/silent-failures/queue-max-silently-ignored-with-cancel-in-progress.yml +109 -0
- package/errors/silent-failures/silent-failures-102.yml +141 -0
- package/errors/silent-failures/silent-failures-104.yml +119 -0
- package/errors/yaml-syntax/yaml-syntax-068.yml +137 -0
- package/errors/yaml-syntax/yaml-syntax-069.yml +118 -0
- package/package.json +1 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
id: silent-failures-105
|
|
2
|
+
title: 'checkout@v6 clean: false still deletes workspace when remote URL changes between checkouts'
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- checkout
|
|
7
|
+
- clean
|
|
8
|
+
- workspace
|
|
9
|
+
- multi-checkout
|
|
10
|
+
- v6
|
|
11
|
+
- remote-url
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'Deleting the contents of.*work.*\n.*Initializing the repository'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'clean:\s*false[\s\S]{0,500}Deleting the contents of'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'git config --local --get remote\.origin\.url[\s\S]{0,200}Deleting the contents'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- 'Deleting the contents of ''/home/runner/work/myrepo/myrepo'''
|
|
21
|
+
- 'Deleting the contents of ''/home/runner/work/_temp/...'
|
|
22
|
+
root_cause: |
|
|
23
|
+
actions/checkout's `clean: false` option prevents `git clean -ffdx` (removing untracked
|
|
24
|
+
files) but does NOT prevent the action from deleting and reinitializing the entire
|
|
25
|
+
workspace directory when it detects that the existing `remote.origin.url` does not match
|
|
26
|
+
the target repository being checked out.
|
|
27
|
+
|
|
28
|
+
When multiple checkout steps run in the same job and the second checkout targets a
|
|
29
|
+
different repository (different org, different repo name, or different URL), the action
|
|
30
|
+
reads the current `git config --local --get remote.origin.url`, compares it against the
|
|
31
|
+
target repository URL, and — on mismatch — deletes the entire workspace directory before
|
|
32
|
+
reinitializing. This deletion happens regardless of `clean: false`.
|
|
33
|
+
|
|
34
|
+
Common scenarios that trigger this:
|
|
35
|
+
1. First step checks out a support/config repo (sparse-checkout of a different repo for
|
|
36
|
+
shared config), then a second step checks out the main repo with `clean: false`.
|
|
37
|
+
2. Checking out the main repo first, then a second checkout of a different repo for
|
|
38
|
+
reading shared scripts, assuming `clean: false` preserves the main workspace.
|
|
39
|
+
3. Re-using runner workspace across jobs via `clean: false` where the prior job checked
|
|
40
|
+
out a different repository.
|
|
41
|
+
|
|
42
|
+
The log shows the silent deletion in plain text ("Deleting the contents of '...'") but
|
|
43
|
+
users commonly miss it because the step still succeeds and subsequent steps may not
|
|
44
|
+
immediately error if they only use the freshly-checked-out content.
|
|
45
|
+
|
|
46
|
+
checkout@v5 has the same behavior — this is not a v6 regression. It is documented as
|
|
47
|
+
expected behavior but is frequently unexpected by users.
|
|
48
|
+
fix: |
|
|
49
|
+
Option 1 (recommended): Specify `path:` to checkout each repository into a distinct
|
|
50
|
+
subdirectory, preventing the remote URL mismatch from triggering deletion.
|
|
51
|
+
|
|
52
|
+
Option 2: Reverse the order — checkout the main repo last so the deletion (if any)
|
|
53
|
+
happens to the support repo's files rather than the main workspace.
|
|
54
|
+
|
|
55
|
+
Option 3: Accept that `clean: false` cannot preserve workspace contents across
|
|
56
|
+
checkouts of different repositories sharing the same path, and restructure the workflow
|
|
57
|
+
to avoid this pattern.
|
|
58
|
+
|
|
59
|
+
Option 4: For reading shared scripts/config from another repo, use curl/gh api to fetch
|
|
60
|
+
specific files rather than a full checkout step.
|
|
61
|
+
fix_code:
|
|
62
|
+
- language: yaml
|
|
63
|
+
label: 'Use path: to isolate each checkout to its own directory'
|
|
64
|
+
code: |
|
|
65
|
+
jobs:
|
|
66
|
+
build:
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
steps:
|
|
69
|
+
# Checkout support config into a subdirectory
|
|
70
|
+
- name: Checkout shared config
|
|
71
|
+
uses: actions/checkout@v6
|
|
72
|
+
with:
|
|
73
|
+
repository: myorg/shared-config
|
|
74
|
+
ref: v1
|
|
75
|
+
sparse-checkout: |
|
|
76
|
+
nginx/nginx.conf
|
|
77
|
+
path: .shared-config # <-- isolated path, no URL conflict
|
|
78
|
+
|
|
79
|
+
# Checkout main repo into workspace root (or another path)
|
|
80
|
+
- name: Checkout source
|
|
81
|
+
uses: actions/checkout@v6
|
|
82
|
+
with:
|
|
83
|
+
fetch-depth: 0
|
|
84
|
+
# clean: false is now safe — different paths, no URL mismatch
|
|
85
|
+
|
|
86
|
+
- name: Use shared config
|
|
87
|
+
run: cp .shared-config/nginx/nginx.conf ./nginx.conf
|
|
88
|
+
|
|
89
|
+
- language: yaml
|
|
90
|
+
label: 'Fetch individual files without a full checkout to avoid URL conflict'
|
|
91
|
+
code: |
|
|
92
|
+
jobs:
|
|
93
|
+
build:
|
|
94
|
+
runs-on: ubuntu-latest
|
|
95
|
+
steps:
|
|
96
|
+
- uses: actions/checkout@v6
|
|
97
|
+
with:
|
|
98
|
+
fetch-depth: 0
|
|
99
|
+
|
|
100
|
+
# Fetch a single file from another repo without a second checkout
|
|
101
|
+
- name: Fetch shared nginx config
|
|
102
|
+
env:
|
|
103
|
+
GH_TOKEN: ${{ github.token }}
|
|
104
|
+
run: |
|
|
105
|
+
gh api repos/myorg/shared-config/contents/nginx/nginx.conf \
|
|
106
|
+
--jq '.content' | base64 -d > ./nginx.conf
|
|
107
|
+
|
|
108
|
+
prevention:
|
|
109
|
+
- 'Use path: on every checkout step when multiple repositories are checked out in the same job'
|
|
110
|
+
- 'Do not rely on clean: false to preserve workspace content across checkouts of different repositories'
|
|
111
|
+
- 'Watch for "Deleting the contents of..." lines in checkout step logs — this confirms workspace was reset even with clean: false'
|
|
112
|
+
- 'When using sparse-checkout for a support repo followed by a main repo checkout, always isolate them into separate path: directories'
|
|
113
|
+
docs:
|
|
114
|
+
- url: 'https://github.com/actions/checkout/issues/2348'
|
|
115
|
+
label: 'actions/checkout#2348 — v6 clean: false still deletes workspace files from prior checkout (Feb 2026)'
|
|
116
|
+
- url: 'https://github.com/actions/checkout#usage'
|
|
117
|
+
label: 'actions/checkout — clean input documentation'
|
|
118
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/storing-workflow-data-as-artifacts'
|
|
119
|
+
label: 'Storing workflow data — alternative to multi-repo checkout for sharing files'
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
id: silent-failures-103
|
|
2
|
+
title: "concurrency queue: max silently ignored when cancel-in-progress: true is also set"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- queue
|
|
8
|
+
- cancel-in-progress
|
|
9
|
+
- silent-failure
|
|
10
|
+
- deployment
|
|
11
|
+
- serialization
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'This run has been cancelled'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'Canceling since a higher priority waiting run was found'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'queue:\s*max'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- "This run has been cancelled."
|
|
21
|
+
- "Canceling since a higher priority waiting run was found"
|
|
22
|
+
root_cause: |
|
|
23
|
+
GitHub Actions introduced `queue: max` in May 2026 as a way to allow up to 100
|
|
24
|
+
pending runs to wait in a concurrency group instead of being cancelled and
|
|
25
|
+
replaced. Adding `queue: max` to an existing concurrency block without removing
|
|
26
|
+
`cancel-in-progress: true` results in the `queue: max` setting being silently
|
|
27
|
+
ignored — no validation error is raised, no warning is emitted.
|
|
28
|
+
|
|
29
|
+
The concurrency group continues to cancel pending runs exactly as it did before.
|
|
30
|
+
The developer believes their deployment queue is now buffering up to 100 runs,
|
|
31
|
+
but every third commit or concurrent PR merge still cancels the previously queued
|
|
32
|
+
run, dropping deployments silently.
|
|
33
|
+
|
|
34
|
+
The language services editor plugin (VS Code Actions extension) does report a
|
|
35
|
+
lint error for this combination, but:
|
|
36
|
+
- Not all teams have the extension installed or enabled.
|
|
37
|
+
- The Actions UI and `gh` CLI do not surface the conflict at run time.
|
|
38
|
+
- The workflow file passes schema validation and runs without a reported error.
|
|
39
|
+
|
|
40
|
+
The practical symptom is identical to having no `queue: max` at all: runs are
|
|
41
|
+
still cancelled, the queue never grows beyond one pending run, and deployments
|
|
42
|
+
are dropped during high-frequency push periods — exactly the problem `queue: max`
|
|
43
|
+
was supposed to solve.
|
|
44
|
+
fix: |
|
|
45
|
+
Remove `cancel-in-progress: true` (or omit it entirely, since `false` is the
|
|
46
|
+
default) when using `queue: max`. These two options are mutually exclusive:
|
|
47
|
+
|
|
48
|
+
- `cancel-in-progress: true` — cancel the pending run when a newer run arrives.
|
|
49
|
+
- `queue: max` — hold up to 100 pending runs in order.
|
|
50
|
+
|
|
51
|
+
If you need both semantics (cancel old pending runs for feature branches but queue
|
|
52
|
+
for main), split into separate concurrency group expressions per ref:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
concurrency:
|
|
56
|
+
group: deploy-${{ github.ref }}
|
|
57
|
+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
|
58
|
+
# Do NOT add queue: max when cancel-in-progress may be true
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For the main branch where ordered deploys matter, use `queue: max` alone:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
concurrency:
|
|
65
|
+
group: deploy-production
|
|
66
|
+
queue: max
|
|
67
|
+
# cancel-in-progress must be omitted or set to false
|
|
68
|
+
```
|
|
69
|
+
fix_code:
|
|
70
|
+
- language: yaml
|
|
71
|
+
label: "WRONG — queue: max silently ignored when cancel-in-progress: true"
|
|
72
|
+
code: |
|
|
73
|
+
concurrency:
|
|
74
|
+
group: deploy-${{ github.repository }}-${{ github.ref }}
|
|
75
|
+
cancel-in-progress: true # ← PROBLEM: negates queue: max
|
|
76
|
+
queue: max # ← silently ignored, no error shown
|
|
77
|
+
- language: yaml
|
|
78
|
+
label: "CORRECT — use queue: max without cancel-in-progress"
|
|
79
|
+
code: |
|
|
80
|
+
concurrency:
|
|
81
|
+
group: deploy-${{ github.repository }}-${{ github.ref }}
|
|
82
|
+
queue: max # ← up to 100 pending runs queued
|
|
83
|
+
# cancel-in-progress is false by default — omit it entirely
|
|
84
|
+
- language: yaml
|
|
85
|
+
label: "ADVANCED — cancel-in-progress for branches, queue for main"
|
|
86
|
+
code: |
|
|
87
|
+
concurrency:
|
|
88
|
+
group: deploy-${{ github.repository }}-${{ github.ref }}
|
|
89
|
+
# Dynamic cancel: cancel feature branch runs (fast feedback), queue
|
|
90
|
+
# main branch deploys (preserve ordering).
|
|
91
|
+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
|
92
|
+
# Note: queue: max cannot be combined with cancel-in-progress.
|
|
93
|
+
# For main branch serialization without cancel, omit cancel-in-progress
|
|
94
|
+
# and rely on queue: max in a separate workflow targeting only main.
|
|
95
|
+
prevention:
|
|
96
|
+
- "When adding `queue: max` to an existing concurrency block, always audit the
|
|
97
|
+
block for a `cancel-in-progress: true` setting and remove it."
|
|
98
|
+
- "Install the GitHub Actions VS Code extension — it reports a lint error for
|
|
99
|
+
`queue: max` + `cancel-in-progress: true` combinations before you push."
|
|
100
|
+
- "After enabling `queue: max`, verify it works by triggering two rapid pushes
|
|
101
|
+
and confirming both runs appear in the Actions UI as 'Queued' rather than one
|
|
102
|
+
being cancelled."
|
|
103
|
+
docs:
|
|
104
|
+
- url: "https://github.blog/changelog/2026-05-07-github-actions-concurrency-groups-now-allow-larger-queues/"
|
|
105
|
+
label: "GitHub Changelog: GitHub Actions concurrency groups now allow larger queues (May 7, 2026)"
|
|
106
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/using-concurrency"
|
|
107
|
+
label: "Using concurrency — GitHub Actions documentation"
|
|
108
|
+
- url: "https://github.com/actions/languageservices/pull/355"
|
|
109
|
+
label: "actions/languageservices#355: Add queue property to concurrency, validate queue+cancel-in-progress conflict"
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
id: silent-failures-102
|
|
2
|
+
title: 'Matrix include: Property Boolean Values Coerced to Strings — Conditional Jobs Silently Misbehave'
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- matrix
|
|
7
|
+
- include
|
|
8
|
+
- boolean
|
|
9
|
+
- string-coercion
|
|
10
|
+
- if-condition
|
|
11
|
+
- fromJSON
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'matrix\.[a-z_]+\s*==\s*(?:true|false)\b'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'if:\s*\$\{\{\s*matrix\.[a-z_]+\s*\}\}'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "if: ${{ matrix.enabled }}"
|
|
19
|
+
- "if: ${{ matrix.enabled == false }}"
|
|
20
|
+
- "if: ${{ matrix.deploy == true }}"
|
|
21
|
+
root_cause: |
|
|
22
|
+
All matrix property values — including those injected via `include:` entries — are coerced to
|
|
23
|
+
**strings** before expression evaluation at runtime. A matrix property configured as:
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
strategy:
|
|
27
|
+
matrix:
|
|
28
|
+
include:
|
|
29
|
+
- os: ubuntu-latest
|
|
30
|
+
enabled: false
|
|
31
|
+
- os: windows-latest
|
|
32
|
+
enabled: true
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
produces `matrix.enabled` equal to the string `"false"` or `"true"`, not the boolean
|
|
36
|
+
`false` / `true`. This creates two distinct silent failure modes:
|
|
37
|
+
|
|
38
|
+
1. `if: ${{ matrix.enabled }}` — the string `"false"` is **truthy** in GitHub Actions expression
|
|
39
|
+
syntax (non-empty string = true). The job/step **always runs** even when the intent is to skip
|
|
40
|
+
entries where `enabled: false`.
|
|
41
|
+
|
|
42
|
+
2. `if: ${{ matrix.enabled == false }}` — compares a string against a boolean. In GitHub
|
|
43
|
+
Actions expression syntax, `"false" == false` evaluates to `false` (type mismatch). The
|
|
44
|
+
condition **always evaluates to false**, silently skipping every include entry regardless
|
|
45
|
+
of the configured value.
|
|
46
|
+
|
|
47
|
+
In both cases the workflow completes without errors or warnings. Only the observable runtime
|
|
48
|
+
behavior is wrong: jobs that should be skipped always run, or jobs that should run are
|
|
49
|
+
silently skipped.
|
|
50
|
+
|
|
51
|
+
This is distinct from composite action boolean input coercion (silent-failures-004), which
|
|
52
|
+
covers `inputs.*` properties. Matrix properties have no `type:` annotation — they are always
|
|
53
|
+
strings at expression evaluation time.
|
|
54
|
+
fix: |
|
|
55
|
+
Use `fromJSON()` to parse the matrix property string into a native boolean before comparison:
|
|
56
|
+
|
|
57
|
+
- `if: ${{ fromJSON(matrix.enabled) }}` ✅ — `fromJSON("false")` → boolean `false` (falsy), skips correctly
|
|
58
|
+
- `if: ${{ fromJSON(matrix.enabled) == false }}` ✅ — correct boolean comparison
|
|
59
|
+
- `if: ${{ matrix.enabled }}` ❌ — string `"false"` is truthy, job always runs
|
|
60
|
+
- `if: ${{ matrix.enabled == false }}` ❌ — string vs boolean comparison, always evaluates to false
|
|
61
|
+
|
|
62
|
+
The same `fromJSON()` pattern applies to steps inside the matrix job:
|
|
63
|
+
```yaml
|
|
64
|
+
steps:
|
|
65
|
+
- name: Deploy step
|
|
66
|
+
if: ${{ fromJSON(matrix.deploy) }}
|
|
67
|
+
run: ./deploy.sh
|
|
68
|
+
```
|
|
69
|
+
fix_code:
|
|
70
|
+
- language: yaml
|
|
71
|
+
label: 'Correct: fromJSON() converts string to native boolean'
|
|
72
|
+
code: |
|
|
73
|
+
jobs:
|
|
74
|
+
build:
|
|
75
|
+
strategy:
|
|
76
|
+
matrix:
|
|
77
|
+
include:
|
|
78
|
+
- os: ubuntu-latest
|
|
79
|
+
enabled: true
|
|
80
|
+
deploy: false
|
|
81
|
+
- os: windows-latest
|
|
82
|
+
enabled: false
|
|
83
|
+
deploy: false
|
|
84
|
+
runs-on: ${{ matrix.os }}
|
|
85
|
+
# ✅ Correct: fromJSON() parses "false" → boolean false (falsy)
|
|
86
|
+
if: ${{ fromJSON(matrix.enabled) }}
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/checkout@v4
|
|
89
|
+
|
|
90
|
+
- name: Deploy (conditional step)
|
|
91
|
+
# ✅ Correct: fromJSON() for step-level boolean matrix property
|
|
92
|
+
if: ${{ fromJSON(matrix.deploy) }}
|
|
93
|
+
run: echo "Deploying on ${{ matrix.os }}"
|
|
94
|
+
|
|
95
|
+
- language: yaml
|
|
96
|
+
label: 'Wrong: string "false" is truthy — job always runs'
|
|
97
|
+
code: |
|
|
98
|
+
jobs:
|
|
99
|
+
build:
|
|
100
|
+
strategy:
|
|
101
|
+
matrix:
|
|
102
|
+
include:
|
|
103
|
+
- os: ubuntu-latest
|
|
104
|
+
enabled: true
|
|
105
|
+
- os: windows-latest
|
|
106
|
+
enabled: false
|
|
107
|
+
runs-on: ${{ matrix.os }}
|
|
108
|
+
# ❌ Wrong: matrix.enabled is the string "false", which is truthy
|
|
109
|
+
if: ${{ matrix.enabled }}
|
|
110
|
+
steps:
|
|
111
|
+
- run: echo "This always runs even when enabled: false"
|
|
112
|
+
|
|
113
|
+
- language: yaml
|
|
114
|
+
label: 'Wrong: string vs boolean comparison always false'
|
|
115
|
+
code: |
|
|
116
|
+
jobs:
|
|
117
|
+
build:
|
|
118
|
+
runs-on: ubuntu-latest
|
|
119
|
+
strategy:
|
|
120
|
+
matrix:
|
|
121
|
+
include:
|
|
122
|
+
- name: job-a
|
|
123
|
+
skip: false
|
|
124
|
+
- name: job-b
|
|
125
|
+
skip: true
|
|
126
|
+
# ❌ Wrong: "false" == false is always false in Actions expressions
|
|
127
|
+
if: ${{ matrix.skip == false }}
|
|
128
|
+
steps:
|
|
129
|
+
- run: echo "This never runs for any include entry"
|
|
130
|
+
prevention:
|
|
131
|
+
- 'Always wrap boolean matrix property references in fromJSON() — e.g., if: ${{ fromJSON(matrix.enabled) }}'
|
|
132
|
+
- 'Add a test matrix entry with the boolean set to false and verify the job is actually skipped before relying on the condition in production'
|
|
133
|
+
- 'Consider using string sentinel values (e.g., skip: "yes"/"no") and comparing with == to avoid the boolean coercion ambiguity entirely'
|
|
134
|
+
- 'Document in the workflow that all matrix properties are runtime strings — reviewers often assume boolean values remain boolean'
|
|
135
|
+
docs:
|
|
136
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-a-matrix-for-your-jobs'
|
|
137
|
+
label: 'Using a matrix for your jobs — GitHub Actions docs'
|
|
138
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#fromjson'
|
|
139
|
+
label: 'fromJSON() expression function — GitHub Actions docs'
|
|
140
|
+
- url: 'https://stackoverflow.com/questions/77059002/how-to-use-matrix-variables-to-conditionally-run-jobs-in-a-github-actions-workfl'
|
|
141
|
+
label: 'Stack Overflow Q77059002 — matrix boolean coercion and fromJSON() fix'
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
id: silent-failures-104
|
|
2
|
+
title: '`github.event.inputs.X` Returns Empty String (Not Declared Default) for `on: schedule` and Other Non-Dispatch Triggers — Use `inputs.X` Instead'
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- github-event-inputs
|
|
7
|
+
- inputs-context
|
|
8
|
+
- schedule
|
|
9
|
+
- workflow-dispatch
|
|
10
|
+
- multi-trigger
|
|
11
|
+
- empty-string
|
|
12
|
+
- default-value
|
|
13
|
+
- context
|
|
14
|
+
- boolean-input
|
|
15
|
+
patterns:
|
|
16
|
+
- regex: 'github\.event\.inputs\.[a-zA-Z_][a-zA-Z0-9_-]*'
|
|
17
|
+
flags: 'g'
|
|
18
|
+
- regex: 'on:\s*\n(?:.*\n)*?\s+schedule:'
|
|
19
|
+
flags: 'im'
|
|
20
|
+
error_messages:
|
|
21
|
+
- "Deploy target is empty — expected a non-empty value from github.event.inputs.environment"
|
|
22
|
+
- "Error: Input required and not supplied"
|
|
23
|
+
- "github.event.inputs.dry_run was '' but expected 'false'"
|
|
24
|
+
root_cause: |
|
|
25
|
+
Multi-trigger workflows that combine `on: schedule` (or `on: push`, `on: pull_request`,
|
|
26
|
+
etc.) with `on: workflow_dispatch` commonly read input values via
|
|
27
|
+
`${{ github.event.inputs.X }}`. This silently produces wrong results for all
|
|
28
|
+
non-dispatch runs because:
|
|
29
|
+
|
|
30
|
+
- **`github.event.inputs.X`** is populated ONLY when the workflow is triggered via
|
|
31
|
+
`workflow_dispatch`. For every other event type — including `schedule`, `push`,
|
|
32
|
+
and `pull_request` — `github.event.inputs` is null or an empty object. Accessing a
|
|
33
|
+
property returns `""` (empty string). The `default:` declared in the `inputs:` block
|
|
34
|
+
is NEVER applied through this context.
|
|
35
|
+
|
|
36
|
+
- **`inputs.X`** correctly returns the declared `default:` value for non-dispatch
|
|
37
|
+
triggers, and the actual provided value for dispatch-triggered runs.
|
|
38
|
+
|
|
39
|
+
**Silent failure patterns:**
|
|
40
|
+
|
|
41
|
+
1. **Boolean gate inverted on schedule** — `if: github.event.inputs.dry_run == 'false'`
|
|
42
|
+
evaluates to `false` on schedule runs (because `"" != "false"`), so deployment steps
|
|
43
|
+
that should run on the nightly schedule are silently skipped.
|
|
44
|
+
|
|
45
|
+
2. **Non-empty guard always fails** — `if: github.event.inputs.target != ''` is always
|
|
46
|
+
`false` on schedule even when the intended behavior is to run with the default target.
|
|
47
|
+
|
|
48
|
+
3. **String comparison breaks** — `${{ github.event.inputs.environment == 'staging' }}`
|
|
49
|
+
is `false` on schedule because the empty string does not match any environment name.
|
|
50
|
+
|
|
51
|
+
Unlike `inputs.X`, `github.event.inputs.X` has no concept of a fallback default — it
|
|
52
|
+
returns `""` for every non-dispatch event.
|
|
53
|
+
|
|
54
|
+
**Distinct from sf-077** (which covers `workflow_call` — where `github.event.inputs`
|
|
55
|
+
is null because reusable workflows use the `inputs` context, not `github.event.inputs`).
|
|
56
|
+
**Distinct from sf-072** (which covers the `inputs.X` context being empty on non-dispatch,
|
|
57
|
+
not the `github.event.inputs.X` context).
|
|
58
|
+
fix: |
|
|
59
|
+
Replace all `github.event.inputs.X` references with `inputs.X` in any workflow that
|
|
60
|
+
uses multiple triggers. The `inputs` context is populated for `workflow_dispatch` and
|
|
61
|
+
`workflow_call` events, and returns the declared `default:` value for all other triggers.
|
|
62
|
+
fix_code:
|
|
63
|
+
- language: yaml
|
|
64
|
+
label: "Broken — github.event.inputs.X ignores defaults on schedule runs"
|
|
65
|
+
code: |
|
|
66
|
+
on:
|
|
67
|
+
schedule:
|
|
68
|
+
- cron: '0 2 * * *'
|
|
69
|
+
workflow_dispatch:
|
|
70
|
+
inputs:
|
|
71
|
+
dry_run:
|
|
72
|
+
type: boolean
|
|
73
|
+
default: false
|
|
74
|
+
environment:
|
|
75
|
+
type: string
|
|
76
|
+
default: production
|
|
77
|
+
|
|
78
|
+
jobs:
|
|
79
|
+
deploy:
|
|
80
|
+
steps:
|
|
81
|
+
# ❌ On schedule: dry_run="" (not "false"), environment="" (not "production")
|
|
82
|
+
- run: echo "Env=${{ github.event.inputs.environment }}"
|
|
83
|
+
# ❌ This if-condition is always false on schedule — step silently skipped!
|
|
84
|
+
- if: github.event.inputs.dry_run == 'false'
|
|
85
|
+
run: ./deploy.sh --env ${{ github.event.inputs.environment }}
|
|
86
|
+
- language: yaml
|
|
87
|
+
label: "Fixed — use inputs.X which applies declared defaults on all non-dispatch runs"
|
|
88
|
+
code: |
|
|
89
|
+
on:
|
|
90
|
+
schedule:
|
|
91
|
+
- cron: '0 2 * * *'
|
|
92
|
+
workflow_dispatch:
|
|
93
|
+
inputs:
|
|
94
|
+
dry_run:
|
|
95
|
+
type: boolean
|
|
96
|
+
default: false
|
|
97
|
+
environment:
|
|
98
|
+
type: string
|
|
99
|
+
default: production
|
|
100
|
+
|
|
101
|
+
jobs:
|
|
102
|
+
deploy:
|
|
103
|
+
steps:
|
|
104
|
+
# ✓ On schedule: dry_run="false", environment="production" (defaults applied)
|
|
105
|
+
- run: echo "Env=${{ inputs.environment }}"
|
|
106
|
+
# ✓ Correctly runs on schedule (dry_run defaults to "false")
|
|
107
|
+
- if: inputs.dry_run == 'false'
|
|
108
|
+
run: ./deploy.sh --env ${{ inputs.environment }}
|
|
109
|
+
prevention:
|
|
110
|
+
- "Always use `inputs.X` (not `github.event.inputs.X`) in any workflow with more than one trigger — `inputs.X` applies declared defaults for non-dispatch runs"
|
|
111
|
+
- "Audit all `github.event.inputs.*` references in workflows that also include `on: schedule`, `on: push`, or `on: pull_request` triggers"
|
|
112
|
+
- "Add a debug step in the workflow to log `inputs.*` values on each run — catching empty-input regressions before they cause silent deploy failures"
|
|
113
|
+
docs:
|
|
114
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/contexts#inputs-context"
|
|
115
|
+
label: "GitHub Actions inputs context — recommended approach for reading workflow inputs"
|
|
116
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/contexts#context-availability"
|
|
117
|
+
label: "Context availability — github.event.inputs is only set for workflow_dispatch triggers"
|
|
118
|
+
- url: "https://github.com/orgs/community/discussions"
|
|
119
|
+
label: "GitHub Community Discussions — multi-trigger workflows with workflow_dispatch inputs"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
id: yaml-syntax-068
|
|
2
|
+
title: 'Composite Action run: Steps Use Caller Workspace as Working Directory — github.action_path Required for Bundled Scripts'
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- composite-action
|
|
7
|
+
- github-action-path
|
|
8
|
+
- working-directory
|
|
9
|
+
- relative-path
|
|
10
|
+
- script
|
|
11
|
+
- file-not-found
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'run:\s*\./[^\s]+'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'No such file or directory.*\./'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'bash:.*\./.*: No such file or directory'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- "bash: ./scripts/build.sh: No such file or directory"
|
|
21
|
+
- "/bin/bash: ./entrypoint.sh: No such file or directory"
|
|
22
|
+
- "Error: Process completed with exit code 127."
|
|
23
|
+
- "bash: line 1: ./setup.sh: No such file or directory"
|
|
24
|
+
root_cause: |
|
|
25
|
+
When a composite action (whether local `uses: ./` or published `uses: org/action@v1`) runs a
|
|
26
|
+
`run:` step, the **working directory is the caller's workspace** (`github.workspace`), not the
|
|
27
|
+
action's own directory. Any relative path like `run: ./scripts/build.sh` resolves against the
|
|
28
|
+
caller repository root — not the composite action's repository.
|
|
29
|
+
|
|
30
|
+
This affects both published composite actions (referenced as `uses: org/my-action@v1`) and
|
|
31
|
+
local composite actions stored inside the same repository. Developers commonly write:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
# In action.yml of org/my-action
|
|
35
|
+
runs:
|
|
36
|
+
using: composite
|
|
37
|
+
steps:
|
|
38
|
+
- name: Run bundled script
|
|
39
|
+
run: ./scripts/build.sh # ❌ Resolves against CALLER workspace, not action directory
|
|
40
|
+
shell: bash
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
When a caller uses `uses: org/my-action@v1`, `./scripts/build.sh` resolves to the caller's
|
|
44
|
+
`$GITHUB_WORKSPACE/scripts/build.sh`, which does not exist — producing "No such file or
|
|
45
|
+
directory" or exit code 127.
|
|
46
|
+
|
|
47
|
+
**Contrast with JavaScript/Docker actions:** In those action types, the action executes in its
|
|
48
|
+
own context. Composite actions are different — their steps are injected into the caller's
|
|
49
|
+
job environment and run with the caller's working directory.
|
|
50
|
+
|
|
51
|
+
The correct way to reference files bundled inside a composite action is to use
|
|
52
|
+
`${{ github.action_path }}`, which always points to the directory containing the action's
|
|
53
|
+
`action.yml` file, regardless of where the action is called from.
|
|
54
|
+
fix: |
|
|
55
|
+
Replace relative paths in composite action `run:` steps with `${{ github.action_path }}/`:
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
# In action.yml
|
|
59
|
+
runs:
|
|
60
|
+
using: composite
|
|
61
|
+
steps:
|
|
62
|
+
- name: Run bundled script
|
|
63
|
+
run: ${{ github.action_path }}/scripts/build.sh # ✅ Always resolves to action directory
|
|
64
|
+
shell: bash
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If the script must be made executable, add a `chmod` step using `github.action_path`:
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
steps:
|
|
71
|
+
- name: Make script executable
|
|
72
|
+
run: chmod +x ${{ github.action_path }}/scripts/build.sh
|
|
73
|
+
shell: bash
|
|
74
|
+
|
|
75
|
+
- name: Run bundled script
|
|
76
|
+
run: ${{ github.action_path }}/scripts/build.sh
|
|
77
|
+
shell: bash
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`github.action_path` is always set to the directory of the currently executing action's
|
|
81
|
+
`action.yml`, even for deeply nested composite actions calling other composite actions.
|
|
82
|
+
fix_code:
|
|
83
|
+
- language: yaml
|
|
84
|
+
label: 'Correct: github.action_path for bundled scripts'
|
|
85
|
+
code: |
|
|
86
|
+
# In action.yml of your composite action (org/my-action)
|
|
87
|
+
name: 'My Action'
|
|
88
|
+
description: 'Does something useful'
|
|
89
|
+
runs:
|
|
90
|
+
using: composite
|
|
91
|
+
steps:
|
|
92
|
+
- name: Make script executable
|
|
93
|
+
run: chmod +x ${{ github.action_path }}/scripts/build.sh
|
|
94
|
+
shell: bash
|
|
95
|
+
|
|
96
|
+
- name: Run bundled build script
|
|
97
|
+
# ✅ github.action_path always points to the action's directory
|
|
98
|
+
run: ${{ github.action_path }}/scripts/build.sh
|
|
99
|
+
shell: bash
|
|
100
|
+
|
|
101
|
+
- name: Run inline Python from action directory
|
|
102
|
+
run: python ${{ github.action_path }}/tools/generate.py
|
|
103
|
+
shell: bash
|
|
104
|
+
|
|
105
|
+
- language: yaml
|
|
106
|
+
label: 'Wrong: relative path resolves against caller workspace'
|
|
107
|
+
code: |
|
|
108
|
+
# In action.yml of your composite action (org/my-action)
|
|
109
|
+
name: 'My Action'
|
|
110
|
+
runs:
|
|
111
|
+
using: composite
|
|
112
|
+
steps:
|
|
113
|
+
- name: Run bundled script
|
|
114
|
+
# ❌ Wrong: ./scripts/build.sh resolves against the CALLER's $GITHUB_WORKSPACE
|
|
115
|
+
# Fails with: bash: ./scripts/build.sh: No such file or directory
|
|
116
|
+
run: ./scripts/build.sh
|
|
117
|
+
shell: bash
|
|
118
|
+
|
|
119
|
+
# ❌ Also wrong: explicitly using github.workspace points to caller repo root
|
|
120
|
+
- name: Run script using workspace
|
|
121
|
+
run: ${{ github.workspace }}/scripts/build.sh
|
|
122
|
+
shell: bash
|
|
123
|
+
|
|
124
|
+
prevention:
|
|
125
|
+
- 'Never use relative paths (./path) or github.workspace in composite action run: steps to reference the action''s own bundled files — always use github.action_path'
|
|
126
|
+
- 'Test composite actions by calling them from a separate test repository, not just the same repository where they are defined — relative paths that work locally (same repo) silently break when called externally'
|
|
127
|
+
- 'Add a step to verify the script exists at the expected path as the first step of your composite action during development: run: ls ${{ github.action_path }}/scripts/'
|
|
128
|
+
- 'Composite actions that call other composite actions each get their own github.action_path — do not pass it between actions as an input'
|
|
129
|
+
docs:
|
|
130
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/contexts#github-context'
|
|
131
|
+
label: 'github.action_path context — GitHub Actions docs'
|
|
132
|
+
- url: 'https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-composite-action'
|
|
133
|
+
label: 'Creating a composite action — GitHub Actions docs'
|
|
134
|
+
- url: 'https://github.com/actions/runner/issues/1348'
|
|
135
|
+
label: 'actions/runner#1348 — Local composite actions always relative to top level repository'
|
|
136
|
+
- url: 'https://stackoverflow.com/questions/77033208/github-action-composite-type-not-working-in-other-repositories-due-to-missing'
|
|
137
|
+
label: 'Stack Overflow — Composite action not working in other repositories due to missing action files'
|