@htekdev/actions-debugger 1.0.0 → 1.0.2

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.
Files changed (58) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +108 -108
  3. package/errors/_schema.json +89 -89
  4. package/errors/caching-artifacts/artifact-storage-quota-exceeded.yml +118 -0
  5. package/errors/caching-artifacts/cache-miss.yml +56 -56
  6. package/errors/caching-artifacts/cache-save-cancelled-job.yml +82 -0
  7. package/errors/caching-artifacts/cache-v3-to-v4-breaking-changes.yml +95 -0
  8. package/errors/caching-artifacts/cross-repo-artifacts-not-supported.yml +102 -0
  9. package/errors/caching-artifacts/upload-artifact-no-files-found.yml +92 -0
  10. package/errors/caching-artifacts/upload-artifact-v4-breaking.yml +67 -67
  11. package/errors/concurrency-timing/cancel-in-progress-deploy-drops.yml +97 -0
  12. package/errors/concurrency-timing/jobs-cancelled-unexpectedly.yml +60 -60
  13. package/errors/concurrency-timing/skipped-needs-cascade.yml +103 -0
  14. package/errors/concurrency-timing/workflow-run-conclusion-unchecked.yml +100 -0
  15. package/errors/known-unsolved/composite-input-env-vars-missing.yml +91 -0
  16. package/errors/known-unsolved/composite-nested-outputs-null.yml +101 -0
  17. package/errors/known-unsolved/no-dynamic-secret-access.yml +111 -0
  18. package/errors/known-unsolved/no-step-level-rerun.yml +94 -0
  19. package/errors/known-unsolved/no-step-retry.yml +53 -53
  20. package/errors/known-unsolved/workflow-rerun-limit.yml +101 -0
  21. package/errors/permissions-auth/checkout-submodule-private-auth.yml +91 -0
  22. package/errors/permissions-auth/fork-pr-secrets-unavailable.yml +97 -0
  23. package/errors/permissions-auth/gcp-oidc-workload-identity-misconfigured.yml +130 -0
  24. package/errors/permissions-auth/github-token-403.yml +64 -64
  25. package/errors/permissions-auth/github-token-protected-branch-push.yml +109 -0
  26. package/errors/permissions-auth/oidc-aws-failure.yml +85 -85
  27. package/errors/permissions-auth/oidc-azure-subject-mismatch.yml +91 -0
  28. package/errors/runner-environment/disk-space.yml +57 -57
  29. package/errors/runner-environment/docker-buildx-not-setup.yml +106 -0
  30. package/errors/runner-environment/macos-homebrew-path.yml +90 -0
  31. package/errors/runner-environment/node-runtime-deprecation.yml +56 -56
  32. package/errors/runner-environment/node20-to-node24-migration.yml +118 -0
  33. package/errors/runner-environment/npm-ci-lockfile-mismatch.yml +112 -0
  34. package/errors/runner-environment/self-hosted-stale-toolcache.yml +73 -0
  35. package/errors/runner-environment/setup-node-version-file-missing.yml +105 -0
  36. package/errors/runner-environment/windows-execution-policy.yml +83 -0
  37. package/errors/silent-failures/add-mask-no-retroactive-masking.yml +75 -0
  38. package/errors/silent-failures/composite-boolean-inputs-as-strings.yml +110 -0
  39. package/errors/silent-failures/conditional-output-null-downstream.yml +82 -0
  40. package/errors/silent-failures/continue-on-error-masks-failure.yml +86 -0
  41. package/errors/silent-failures/github-token-no-trigger.yml +57 -57
  42. package/errors/silent-failures/reusable-workflow-env-secrets-empty.yml +90 -0
  43. package/errors/silent-failures/scheduled-workflow-disabled.yml +59 -59
  44. package/errors/silent-failures/sparse-checkout-sticky-cone-mode.yml +120 -0
  45. package/errors/triggers/cron-schedule-late.yml +59 -59
  46. package/errors/triggers/pull-request-target-rce-risk.yml +117 -0
  47. package/errors/triggers/workflow-not-triggering.yml +60 -60
  48. package/errors/triggers/workflow-run-default-branch-requirement.yml +78 -0
  49. package/errors/yaml-syntax/anchors-not-supported.yml +95 -0
  50. package/errors/yaml-syntax/dynamic-matrix-fromjson-failure.yml +99 -0
  51. package/errors/yaml-syntax/if-always-true.yml +52 -52
  52. package/errors/yaml-syntax/missing-expression-wrapper.yml +67 -0
  53. package/errors/yaml-syntax/needs-indirect-outputs.yml +91 -0
  54. package/errors/yaml-syntax/reusable-workflow-missing-output-declaration.yml +140 -0
  55. package/errors/yaml-syntax/secrets-in-if.yml +55 -55
  56. package/errors/yaml-syntax/unexpected-yaml-key.yml +69 -69
  57. package/errors/yaml-syntax/working-directory-ignored-on-uses.yml +66 -0
  58. package/package.json +70 -67
@@ -0,0 +1,73 @@
1
+ id: runner-environment-013
2
+ title: "Self-Hosted Runner Stale Tool Cache Serves Outdated Versions"
3
+ category: runner-environment
4
+ severity: silent-failure
5
+ tags:
6
+ - self-hosted
7
+ - tool-cache
8
+ - setup-node
9
+ - setup-python
10
+ - setup-java
11
+ - outdated
12
+ - toolcache
13
+ patterns:
14
+ - regex: "Found in cache"
15
+ flags: "i"
16
+ - regex: "Resolved .* from tool-cache"
17
+ flags: "i"
18
+ - regex: "Tool cache hit.*[0-9]+\\.[0-9]+"
19
+ flags: "i"
20
+ error_messages:
21
+ - "Found in cache @ /opt/hostedtoolcache/node/18.12.0/x64"
22
+ - "Resolved Node 18.12.0 from tool-cache"
23
+ root_cause: |
24
+ `actions/setup-node`, `actions/setup-python`, `actions/setup-java`, and similar setup
25
+ actions maintain a **tool cache** on the runner host. On GitHub-hosted runners this
26
+ cache is wiped between jobs (fresh VMs), but on **self-hosted runners** the cache
27
+ persists indefinitely across runs.
28
+
29
+ When a self-hosted runner's tool cache has a matching version range (e.g., `node-version: 18.x`)
30
+ it returns the cached version without downloading a fresh copy. If that cached version
31
+ contains a security vulnerability or a bug that was patched in a newer patch release,
32
+ workflows silently continue using the vulnerable version. There is no warning — the log
33
+ shows "Found in cache" and proceeds.
34
+
35
+ This also happens when teams pin to `18.x` intending to get the latest 18 minor but the
36
+ cache serves the version that was first downloaded months ago.
37
+ fix: |
38
+ Pin to exact versions in production workflows. Periodically purge the tool cache on
39
+ self-hosted runners. Use the `check-latest` input to force setup actions to verify
40
+ whether a newer patch release is available.
41
+ fix_code:
42
+ - language: yaml
43
+ label: "WRONG — loose version range silently serves cached old version"
44
+ code: |
45
+ steps:
46
+ - uses: actions/setup-node@v4
47
+ with:
48
+ node-version: 18 # resolves to whatever 18.x is in cache
49
+ - language: yaml
50
+ label: "CORRECT — pin exact version for reproducibility"
51
+ code: |
52
+ steps:
53
+ - uses: actions/setup-node@v4
54
+ with:
55
+ node-version: '18.20.4' # exact version, cache miss forces fresh download
56
+ - language: yaml
57
+ label: "CORRECT — use check-latest to force freshness check"
58
+ code: |
59
+ steps:
60
+ - uses: actions/setup-node@v4
61
+ with:
62
+ node-version: '18.x'
63
+ check-latest: true # verifies cache against upstream, downloads if newer available
64
+ prevention:
65
+ - "Pin to exact tool versions (`18.20.4` not `18.x`) on self-hosted runners to ensure cache hits are intentional."
66
+ - "Schedule periodic purges of `/opt/hostedtoolcache` (or Windows equivalent) on self-hosted runners."
67
+ - "Use `check-latest: true` on self-hosted runners when using semver ranges."
68
+ - "Document your self-hosted runner's tool cache contents and last purge date."
69
+ docs:
70
+ - url: "https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners"
71
+ label: "About self-hosted runners"
72
+ - url: "https://github.com/actions/setup-node#check-latest-version"
73
+ label: "actions/setup-node — check-latest input"
@@ -0,0 +1,105 @@
1
+ id: runner-environment-010
2
+ title: "actions/setup-node Fails When .nvmrc or node-version-file Is Missing"
3
+ category: runner-environment
4
+ severity: error
5
+ tags:
6
+ - setup-node
7
+ - nvmrc
8
+ - node-version
9
+ - node-version-file
10
+ - version-resolution
11
+ patterns:
12
+ - regex: "The specified node version file at.*does not exist"
13
+ flags: "i"
14
+ - regex: "Unable to find Node version '\\$\\{.*\\}'"
15
+ flags: "i"
16
+ - regex: "Unable to find Node version '.*' for platform"
17
+ flags: "i"
18
+ - regex: "Cache service responded with 422"
19
+ flags: "i"
20
+ error_messages:
21
+ - "The specified node version file at: /__w/actions/actions/.nvmrc does not exist"
22
+ - "##[error]Unable to find Node version '${NVMRC}' for platform linux and architecture x64."
23
+ - "##[error]Unable to find Node version '16.99.0' for platform linux and architecture x64."
24
+ - "Error: Cache service responded with 422"
25
+ root_cause: |
26
+ `actions/setup-node` supports three ways to specify a Node.js version:
27
+ `node-version` (inline), `node-version-file` (reads from a file), or auto-detection.
28
+ These fail in distinct ways:
29
+
30
+ 1. **Missing version file** (`node-version-file: '.nvmrc'` or `'.node-version'`):
31
+ If the file referenced by `node-version-file` does not exist on the runner at the
32
+ path provided, setup-node throws "The specified node version file … does not exist"
33
+ and the step fails. This commonly happens when the file exists in the repo root but
34
+ the workflow runs in a subdirectory, or when the checkout step runs after setup-node.
35
+
36
+ 2. **Unresolved environment variable** (`node-version: '${{ env.NVMRC }}'`):
37
+ Using a shell env var (e.g., `$NVMRC`) instead of an Actions expression
38
+ (`${{ env.NVMRC }}`) means the value is never substituted — setup-node receives a
39
+ literal `${NVMRC}` string, which is not a valid version, and fails with "Unable to
40
+ find Node version '${NVMRC}'". Similarly, if `env.NVMRC` is not set at the point
41
+ setup-node runs, the expression evaluates to empty string.
42
+
43
+ 3. **Non-existent or misspelled version** (`node-version: '16.99.0'`):
44
+ Requesting a version that GitHub's node distribution manifest does not list causes
45
+ "Unable to find Node version." This can happen after pinning to a patch version
46
+ that was later delisted.
47
+
48
+ 4. **Old setup-node version + cache service 422**:
49
+ Older setup-node versions (v1/v2) use a deprecated cache service API that now
50
+ returns HTTP 422. The fix is upgrading to setup-node@v3 or v4.
51
+ fix: |
52
+ 1. Ensure `actions/checkout` runs **before** any `actions/setup-node` step that reads
53
+ `node-version-file` — the file must exist on the runner when setup-node reads it.
54
+ 2. Use Actions expressions (`${{ env.VAR }}`) not shell syntax (`$VAR`) for
55
+ `node-version` values.
56
+ 3. For `.nvmrc`, prefer the `node-version-file: '.nvmrc'` parameter directly rather
57
+ than reading the file in a shell step and passing it to `node-version`.
58
+ 4. Upgrade to `actions/setup-node@v4` to avoid the deprecated cache service 422 error.
59
+ fix_code:
60
+ - language: yaml
61
+ label: "Correct order: checkout before setup-node with node-version-file"
62
+ code: |
63
+ steps:
64
+ - uses: actions/checkout@v4 # Must come BEFORE setup-node
65
+ - uses: actions/setup-node@v4
66
+ with:
67
+ node-version-file: '.nvmrc' # Reads .nvmrc from the checked-out repo
68
+ cache: 'npm'
69
+ - language: yaml
70
+ label: "Inline version — use Actions expression, not shell variable"
71
+ code: |
72
+ env:
73
+ NODE_VERSION: '20'
74
+
75
+ steps:
76
+ - uses: actions/checkout@v4
77
+ - uses: actions/setup-node@v4
78
+ with:
79
+ node-version: ${{ env.NODE_VERSION }} # ✅ Actions expression
80
+ # node-version: $NODE_VERSION # ❌ Shell syntax — not expanded
81
+ - language: yaml
82
+ label: "Read .nvmrc manually when node-version-file is insufficient"
83
+ code: |
84
+ steps:
85
+ - uses: actions/checkout@v4
86
+ - name: Read .nvmrc
87
+ id: nvmrc
88
+ run: echo "version=$(cat .nvmrc)" >> $GITHUB_OUTPUT
89
+ - uses: actions/setup-node@v4
90
+ with:
91
+ node-version: ${{ steps.nvmrc.outputs.version }}
92
+ prevention:
93
+ - "Always run `actions/checkout` before any step that reads files from the repository, including `node-version-file`."
94
+ - "Use `actions/setup-node@v4` — v1/v2 are deprecated and trigger cache service 422 errors."
95
+ - "Prefer `node-version-file: '.nvmrc'` over reading the file in a shell step to avoid shell escaping issues."
96
+ - "Pin to a Node.js major version (e.g., `20`) rather than a full SemVer patch to avoid version-not-found errors."
97
+ docs:
98
+ - url: "https://github.com/actions/setup-node"
99
+ label: "actions/setup-node — README and usage"
100
+ - url: "https://github.com/actions/setup-node/issues/613"
101
+ label: "setup-node#613 — node-version-file does not exist error"
102
+ - url: "https://github.com/actions/setup-node/issues/32"
103
+ label: "setup-node#32 — Using .nvmrc with env variable substitution"
104
+ - url: "https://github.com/actions/setup-node/issues/1275"
105
+ label: "setup-node#1275 — Cache service 422 — upgrade to v3/v4"
@@ -0,0 +1,83 @@
1
+ id: runner-environment-012
2
+ title: "Windows Runner PowerShell Execution Policy Blocks Custom Scripts"
3
+ category: runner-environment
4
+ severity: error
5
+ tags:
6
+ - windows
7
+ - powershell
8
+ - execution-policy
9
+ - scripts
10
+ - runner
11
+ - security
12
+ patterns:
13
+ - regex: "cannot be loaded because running scripts is disabled"
14
+ flags: "i"
15
+ - regex: "execution policy"
16
+ flags: "i"
17
+ - regex: "UnauthorizedAccess.*\\.ps1"
18
+ flags: "i"
19
+ - regex: "File .+\\.ps1 cannot be loaded"
20
+ flags: "i"
21
+ error_messages:
22
+ - "File C:\\...\\script.ps1 cannot be loaded because running scripts is disabled on this system."
23
+ - "UnauthorizedAccess: File C:\\...\\script.ps1 cannot be loaded because running scripts is disabled on this system. For more information, see about_Execution_Policies"
24
+ root_cause: |
25
+ Windows GitHub-hosted runners default to the `RemoteSigned` execution policy, which
26
+ requires that downloaded scripts be digitally signed. Scripts created inline during a
27
+ workflow or checked out from the repo can trigger this restriction depending on how
28
+ they are invoked, especially when called via `powershell.exe` directly or from a
29
+ third-party action that spawns a new PowerShell session with a more restrictive default.
30
+
31
+ This is particularly common when a workflow uses `shell: powershell` (legacy
32
+ `powershell.exe` v5) where the execution policy is stricter, or when a script file
33
+ is written to disk by a previous step and then invoked.
34
+ fix: |
35
+ Use `shell: pwsh` (PowerShell 7+) which defaults to `Bypass` execution policy in
36
+ GitHub Actions workflows. For `shell: powershell` sessions, prefix the script
37
+ invocation with `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass`.
38
+ fix_code:
39
+ - language: yaml
40
+ label: "WRONG — script fails with execution policy error"
41
+ code: |
42
+ jobs:
43
+ build:
44
+ runs-on: windows-latest
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+
48
+ - name: Run build script
49
+ shell: powershell
50
+ run: .\scripts\build.ps1 # may fail: execution policy
51
+ - language: yaml
52
+ label: "CORRECT — use pwsh (PowerShell 7) which defaults to Bypass"
53
+ code: |
54
+ jobs:
55
+ build:
56
+ runs-on: windows-latest
57
+ steps:
58
+ - uses: actions/checkout@v4
59
+
60
+ - name: Run build script
61
+ shell: pwsh # PowerShell 7+ — Bypass policy by default
62
+ run: .\scripts\build.ps1
63
+ - language: yaml
64
+ label: "CORRECT — explicitly bypass in legacy powershell session"
65
+ code: |
66
+ jobs:
67
+ build:
68
+ runs-on: windows-latest
69
+ steps:
70
+ - name: Run legacy PS script
71
+ shell: powershell
72
+ run: |
73
+ Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
74
+ .\scripts\build.ps1
75
+ prevention:
76
+ - "Prefer `shell: pwsh` over `shell: powershell` for all new Windows workflows — it's PowerShell 7+ and more permissive."
77
+ - "Avoid calling `.ps1` files from non-PowerShell shells (cmd, bash) — invoke them via `pwsh -File script.ps1` instead."
78
+ - "Test Windows workflows locally with the same shell version (`pwsh` vs `powershell`) before pushing."
79
+ docs:
80
+ - url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell"
81
+ label: "jobs.<job_id>.steps[*].shell"
82
+ - url: "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy"
83
+ label: "Set-ExecutionPolicy (Microsoft Learn)"
@@ -0,0 +1,75 @@
1
+ id: silent-failures-006
2
+ title: "add-mask Does Not Retroactively Redact Values Already Printed"
3
+ category: silent-failures
4
+ severity: silent-failure
5
+ tags:
6
+ - secrets
7
+ - masking
8
+ - add-mask
9
+ - security
10
+ - log-redaction
11
+ - sensitive-data
12
+ patterns:
13
+ - regex: "::add-mask::"
14
+ flags: "i"
15
+ - regex: "add_mask"
16
+ flags: "i"
17
+ error_messages:
18
+ - "Warning: Secret value was printed to log before masking was applied."
19
+ root_cause: |
20
+ The `add-mask` workflow command (`echo "::add-mask::$VALUE"`) tells the runner to
21
+ redact all future occurrences of `$VALUE` from log output. However, it only applies
22
+ **from the point it is issued forward** — any log lines already written before the
23
+ `add-mask` command are not retroactively redacted.
24
+
25
+ This creates a silent security failure when a step prints a sensitive value in an
26
+ earlier `run:` command and only calls `add-mask` later. The value appears in plain
27
+ text in the workflow log for the duration the log is retained (90 days by default).
28
+
29
+ A common pattern that triggers this: computing a token at the start of a long `run:`
30
+ script, printing it for debugging, and only masking it several lines later.
31
+ fix: |
32
+ Issue `add-mask` as the very first operation after obtaining a sensitive value —
33
+ before any `echo`, `cat`, or other command that might emit it. Prefer using
34
+ built-in `secrets.*` context (which is auto-masked) over dynamically computed
35
+ sensitive values wherever possible.
36
+ fix_code:
37
+ - language: yaml
38
+ label: "WRONG — value printed before add-mask is issued"
39
+ code: |
40
+ steps:
41
+ - name: Get dynamic token
42
+ run: |
43
+ TOKEN=$(curl -s https://api.example.com/token | jq -r .access_token)
44
+ echo "Got token: $TOKEN" # PRINTED IN PLAIN TEXT
45
+ echo "::add-mask::$TOKEN" # too late — above line already logged
46
+ echo "TOKEN=$TOKEN" >> $GITHUB_ENV
47
+ - language: yaml
48
+ label: "CORRECT — mask before any output"
49
+ code: |
50
+ steps:
51
+ - name: Get dynamic token
52
+ run: |
53
+ TOKEN=$(curl -s https://api.example.com/token | jq -r .access_token)
54
+ echo "::add-mask::$TOKEN" # mask FIRST
55
+ echo "TOKEN=$TOKEN" >> $GITHUB_ENV
56
+ echo "Token acquired and masked successfully" # no value, safe to log
57
+ - language: yaml
58
+ label: "CORRECT — use secrets context (auto-masked)"
59
+ code: |
60
+ steps:
61
+ - name: Use pre-configured secret
62
+ env:
63
+ API_TOKEN: ${{ secrets.API_TOKEN }} # automatically masked everywhere
64
+ run: |
65
+ curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com/data
66
+ prevention:
67
+ - "Always call `add-mask` as the very first line after obtaining a sensitive value."
68
+ - "Prefer `secrets.*` context values over dynamically fetched tokens where possible."
69
+ - "Never print sensitive values for debugging — use `echo 'Token acquired (masked)'` instead."
70
+ - "Audit workflows that call external APIs and store tokens in env vars for missing `add-mask`."
71
+ docs:
72
+ - url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#masking-a-value-in-a-log"
73
+ label: "Masking a value in a log"
74
+ - url: "https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions"
75
+ label: "Security hardening for GitHub Actions"
@@ -0,0 +1,110 @@
1
+ id: silent-failures-004
2
+ title: "Composite Action Boolean Inputs Treated as Strings — Conditions Always True or Always False"
3
+ category: silent-failures
4
+ severity: silent-failure
5
+ tags:
6
+ - composite-actions
7
+ - boolean
8
+ - inputs
9
+ - string-coercion
10
+ - if-condition
11
+ patterns:
12
+ - regex: "if:\\s*\\$\\{\\{\\s*inputs\\.[a-zA-Z_-]+\\s*\\}\\}"
13
+ flags: "i"
14
+ - regex: "inputs\\.[a-zA-Z_-]+\\s*==\\s*true(?!')"
15
+ flags: "i"
16
+ error_messages:
17
+ - "realRun==false"
18
+ - "Step was skipped because it is conditional"
19
+ root_cause: |
20
+ Composite actions store all inputs internally as strings regardless of the `type:` field declared in
21
+ `action.yml`. The `type: boolean` annotation is accepted by the YAML parser but has no runtime effect —
22
+ all values are coerced to strings before expressions are evaluated.
23
+
24
+ This creates two distinct silent failure modes:
25
+
26
+ 1. `if: ${{ inputs.enabled }}` — the string "false" is truthy in GitHub Actions expression syntax,
27
+ so the step ALWAYS runs even when the caller explicitly passes `enabled: false`.
28
+
29
+ 2. `if: ${{ inputs.enabled == true }}` — the string "true" never equals the boolean `true` in
30
+ expression syntax, so the step NEVER runs regardless of the input value.
31
+
32
+ In both cases no error is thrown, no warning is emitted, and the workflow succeeds. Only the
33
+ observable behavior is wrong (unexpected steps run or expected steps are skipped entirely).
34
+
35
+ This also affects composite action outputs: any output whose `value:` expression produces a boolean
36
+ (e.g., `${{ fromJSON(steps.x.outputs.flag) }}`) is silently coerced back to a string before it
37
+ leaves the composite action.
38
+ fix: |
39
+ Always compare composite action boolean inputs using string equality:
40
+
41
+ - `if: inputs.enabled == 'true'` ✅
42
+ - `if: ${{ inputs.enabled == 'true' }}` ✅
43
+ - `if: ${{ inputs.enabled }}` ❌ (always truthy — string "false" is truthy)
44
+ - `if: ${{ inputs.enabled == true }}` ❌ (never matches — string never equals boolean)
45
+
46
+ When passing a boolean workflow_dispatch input into a composite action, cast it to a string explicitly:
47
+
48
+ ```yaml
49
+ with:
50
+ enabled: ${{ inputs.enabled == true }} # evaluates to string 'true' or 'false'
51
+ ```
52
+ fix_code:
53
+ - language: yaml
54
+ label: "Use string comparison for boolean inputs inside composite actions"
55
+ code: |
56
+ # action.yml (composite action)
57
+ inputs:
58
+ enabled:
59
+ description: "Enable the feature"
60
+ required: false
61
+ default: "false"
62
+ # Note: type: boolean has no runtime effect in composite actions — default must be a string
63
+
64
+ runs:
65
+ using: composite
66
+ steps:
67
+ # ❌ WRONG — string "false" is truthy, step always runs
68
+ # - name: Feature step
69
+ # if: ${{ inputs.enabled }}
70
+
71
+ # ❌ WRONG — string "true" != boolean true, step never runs
72
+ # - name: Feature step
73
+ # if: ${{ inputs.enabled == true }}
74
+
75
+ # ✅ CORRECT — compare as string
76
+ - name: Feature step
77
+ if: inputs.enabled == 'true'
78
+ shell: bash
79
+ run: echo "Feature is enabled"
80
+ - language: yaml
81
+ label: "Cast boolean workflow_dispatch input before passing to composite action"
82
+ code: |
83
+ on:
84
+ workflow_dispatch:
85
+ inputs:
86
+ run-tests:
87
+ type: boolean
88
+ default: false
89
+
90
+ jobs:
91
+ build:
92
+ runs-on: ubuntu-latest
93
+ steps:
94
+ - uses: ./.github/actions/my-composite
95
+ with:
96
+ # Explicitly cast to string — inputs.run-tests is boolean at this scope
97
+ run-tests: ${{ inputs.run-tests == true }}
98
+ prevention:
99
+ - "Never rely on `type: boolean` in composite action inputs — it is parsed but not enforced at runtime."
100
+ - "Use string `'false'` not boolean `false` as default values in composite action input definitions."
101
+ - "Always compare composite action boolean inputs with `== 'true'` or `== 'false'` string comparison."
102
+ - "When passing boolean workflow inputs into composite actions, cast with `${{ inputs.flag == true }}`."
103
+ - "Document the string-only convention in the composite action README to prevent future contributors from introducing the bug."
104
+ docs:
105
+ - url: "https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs"
106
+ label: "Metadata syntax — inputs for composite actions"
107
+ - url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions"
108
+ label: "Evaluate expressions in workflows and actions — truthiness rules"
109
+ - url: "https://github.com/actions/runner/issues/2238"
110
+ label: "actions/runner#2238 — Boolean inputs not actually booleans in composite actions (112 reactions)"
@@ -0,0 +1,82 @@
1
+ id: silent-failures-007
2
+ title: "Step Output Only Written When Condition Passes — Downstream Reads Empty String"
3
+ category: silent-failures
4
+ severity: silent-failure
5
+ tags:
6
+ - outputs
7
+ - steps
8
+ - context
9
+ - conditionals
10
+ - GITHUB_OUTPUT
11
+ - empty-string
12
+ patterns:
13
+ - regex: "steps\\.[a-z_-]+\\.outputs\\.[a-z_-]+"
14
+ flags: "i"
15
+ - regex: "GITHUB_OUTPUT"
16
+ flags: "i"
17
+ error_messages:
18
+ - "Warning: Unexpected input(s) '', valid inputs are ['version']"
19
+ root_cause: |
20
+ A step output written to `$GITHUB_OUTPUT` (or the deprecated `set-output` command)
21
+ only exists if the step that writes it actually executed **and** reached the `echo`
22
+ statement. When a step is conditional (`if: github.event_name == 'push'`) and the
23
+ condition is false, the step is skipped entirely — no output is written.
24
+
25
+ Downstream steps that reference `${{ steps.<id>.outputs.<key> }}` receive an empty
26
+ string rather than an error, making the failure silent. The workflow continues, but
27
+ subsequent steps silently operate on empty values — potentially deploying with no
28
+ version tag, pushing an empty config, or skipping critical logic without any warning.
29
+ fix: |
30
+ Guard against empty outputs in consuming steps, or restructure so outputs are always
31
+ written unconditionally and logic is controlled by the value itself.
32
+ fix_code:
33
+ - language: yaml
34
+ label: "WRONG — output only written on push, silently empty on PR"
35
+ code: |
36
+ steps:
37
+ - name: Compute version
38
+ id: version
39
+ if: github.event_name == 'push'
40
+ run: echo "tag=v$(date +%Y%m%d%H%M%S)" >> $GITHUB_OUTPUT
41
+
42
+ - name: Build Docker image
43
+ run: |
44
+ # On pull_request, steps.version.outputs.tag is "" — image tagged as ""
45
+ docker build -t myapp:${{ steps.version.outputs.tag }} .
46
+ - language: yaml
47
+ label: "CORRECT — always write output, vary value by condition"
48
+ code: |
49
+ steps:
50
+ - name: Compute version
51
+ id: version
52
+ run: |
53
+ if [ "${{ github.event_name }}" = "push" ]; then
54
+ echo "tag=v$(date +%Y%m%d%H%M%S)" >> $GITHUB_OUTPUT
55
+ else
56
+ echo "tag=dev-${{ github.sha }}" >> $GITHUB_OUTPUT
57
+ fi
58
+
59
+ - name: Build Docker image
60
+ run: docker build -t myapp:${{ steps.version.outputs.tag }} .
61
+ - language: yaml
62
+ label: "CORRECT — guard against empty value in consuming step"
63
+ code: |
64
+ steps:
65
+ - name: Deploy
66
+ if: steps.version.outputs.tag != ''
67
+ run: ./deploy.sh ${{ steps.version.outputs.tag }}
68
+
69
+ - name: Warn if no version
70
+ if: steps.version.outputs.tag == ''
71
+ run: |
72
+ echo "::warning::No version tag output — skipping deploy"
73
+ exit 1
74
+ prevention:
75
+ - "Write a default/fallback value in the output step rather than skipping the entire step."
76
+ - "Validate critical outputs with `if: steps.<id>.outputs.<key> != ''` before consuming them."
77
+ - "Use `if: always()` or no condition on steps that must always emit outputs."
78
+ docs:
79
+ - url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs"
80
+ label: "Passing information between jobs"
81
+ - url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-output-parameter"
82
+ label: "Setting an output parameter"
@@ -0,0 +1,86 @@
1
+ id: silent-failures-005
2
+ title: "continue-on-error: true Masks Step Failures — Job Reports Success"
3
+ category: silent-failures
4
+ severity: silent-failure
5
+ tags:
6
+ - continue-on-error
7
+ - failure
8
+ - masking
9
+ - job-status
10
+ - outcome
11
+ patterns:
12
+ - regex: "continue-on-error"
13
+ flags: "i"
14
+ - regex: "steps\\.[a-z_-]+\\.outcome.*failure"
15
+ flags: "i"
16
+ error_messages:
17
+ - "##[error]Process completed with exit code 1."
18
+ - "Error: Process completed with exit code 1."
19
+ root_cause: |
20
+ When `continue-on-error: true` is set on a step, GitHub Actions marks the step's
21
+ `outcome` as `failure` but sets `conclusion` to `success` and allows the job to
22
+ continue. Critically, the **job itself reports success** even if one or more steps
23
+ failed — the failure is effectively swallowed.
24
+
25
+ This is frequently misunderstood: developers use `continue-on-error` to avoid blocking
26
+ a pipeline on non-critical steps, but they don't realise the overall job will show a
27
+ green checkmark even when those steps fail. Branch protection rules, required status
28
+ checks, and downstream `needs` jobs all see a passing job.
29
+
30
+ The `outcome` context (`steps.<id>.outcome`) still reports `failure`, but this is only
31
+ visible if explicitly checked in a later conditional.
32
+ fix: |
33
+ If you need a step to be non-blocking, check its outcome explicitly in a downstream
34
+ step and surface the failure in a way that's visible (job summary, annotation, or
35
+ dedicated reporting step). Do not rely on `continue-on-error` as a silent suppressor.
36
+ fix_code:
37
+ - language: yaml
38
+ label: "WRONG — failure silently swallowed, job shows green"
39
+ code: |
40
+ steps:
41
+ - name: Run flaky integration test
42
+ id: integration
43
+ continue-on-error: true
44
+ run: ./run-integration.sh
45
+
46
+ - name: Deploy
47
+ run: ./deploy.sh # runs even if integration test failed, job still green
48
+ - language: yaml
49
+ label: "CORRECT — check outcome and fail loudly if needed"
50
+ code: |
51
+ steps:
52
+ - name: Run integration test
53
+ id: integration
54
+ continue-on-error: true
55
+ run: ./run-integration.sh
56
+
57
+ - name: Report integration failure
58
+ if: steps.integration.outcome == 'failure'
59
+ run: |
60
+ echo "::error::Integration test failed — review before shipping"
61
+ echo "## ⚠️ Integration Test Failed" >> $GITHUB_STEP_SUMMARY
62
+ exit 1 # fail the job explicitly if the failure matters
63
+ - language: yaml
64
+ label: "CORRECT — use outcome in branch protection-aware way"
65
+ code: |
66
+ steps:
67
+ - name: Lint (non-blocking)
68
+ id: lint
69
+ continue-on-error: true
70
+ run: npm run lint
71
+
72
+ - name: Annotate lint result
73
+ if: always()
74
+ run: |
75
+ if [ "${{ steps.lint.outcome }}" = "failure" ]; then
76
+ echo "::warning::Linting failed but is non-blocking for this branch"
77
+ fi
78
+ prevention:
79
+ - "Treat `continue-on-error: true` as a visibility-reduction tool — always pair it with explicit outcome checking."
80
+ - "Add a final step that fails the job if any non-critical steps failed and the failure actually matters."
81
+ - "Prefer `allowed-failure` patterns at the matrix level for known-unstable configurations."
82
+ docs:
83
+ - url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepscontinue-on-error"
84
+ label: "continue-on-error"
85
+ - url: "https://docs.github.com/en/actions/learn-github-actions/expressions#steps-context"
86
+ label: "Steps context — outcome vs conclusion"