@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.
- package/LICENSE +21 -21
- package/README.md +108 -108
- package/errors/_schema.json +89 -89
- package/errors/caching-artifacts/artifact-storage-quota-exceeded.yml +118 -0
- package/errors/caching-artifacts/cache-miss.yml +56 -56
- package/errors/caching-artifacts/cache-save-cancelled-job.yml +82 -0
- package/errors/caching-artifacts/cache-v3-to-v4-breaking-changes.yml +95 -0
- package/errors/caching-artifacts/cross-repo-artifacts-not-supported.yml +102 -0
- package/errors/caching-artifacts/upload-artifact-no-files-found.yml +92 -0
- package/errors/caching-artifacts/upload-artifact-v4-breaking.yml +67 -67
- package/errors/concurrency-timing/cancel-in-progress-deploy-drops.yml +97 -0
- package/errors/concurrency-timing/jobs-cancelled-unexpectedly.yml +60 -60
- package/errors/concurrency-timing/skipped-needs-cascade.yml +103 -0
- package/errors/concurrency-timing/workflow-run-conclusion-unchecked.yml +100 -0
- package/errors/known-unsolved/composite-input-env-vars-missing.yml +91 -0
- package/errors/known-unsolved/composite-nested-outputs-null.yml +101 -0
- package/errors/known-unsolved/no-dynamic-secret-access.yml +111 -0
- package/errors/known-unsolved/no-step-level-rerun.yml +94 -0
- package/errors/known-unsolved/no-step-retry.yml +53 -53
- package/errors/known-unsolved/workflow-rerun-limit.yml +101 -0
- package/errors/permissions-auth/checkout-submodule-private-auth.yml +91 -0
- package/errors/permissions-auth/fork-pr-secrets-unavailable.yml +97 -0
- package/errors/permissions-auth/gcp-oidc-workload-identity-misconfigured.yml +130 -0
- package/errors/permissions-auth/github-token-403.yml +64 -64
- package/errors/permissions-auth/github-token-protected-branch-push.yml +109 -0
- package/errors/permissions-auth/oidc-aws-failure.yml +85 -85
- package/errors/permissions-auth/oidc-azure-subject-mismatch.yml +91 -0
- package/errors/runner-environment/disk-space.yml +57 -57
- package/errors/runner-environment/docker-buildx-not-setup.yml +106 -0
- package/errors/runner-environment/macos-homebrew-path.yml +90 -0
- package/errors/runner-environment/node-runtime-deprecation.yml +56 -56
- package/errors/runner-environment/node20-to-node24-migration.yml +118 -0
- package/errors/runner-environment/npm-ci-lockfile-mismatch.yml +112 -0
- package/errors/runner-environment/self-hosted-stale-toolcache.yml +73 -0
- package/errors/runner-environment/setup-node-version-file-missing.yml +105 -0
- package/errors/runner-environment/windows-execution-policy.yml +83 -0
- package/errors/silent-failures/add-mask-no-retroactive-masking.yml +75 -0
- package/errors/silent-failures/composite-boolean-inputs-as-strings.yml +110 -0
- package/errors/silent-failures/conditional-output-null-downstream.yml +82 -0
- package/errors/silent-failures/continue-on-error-masks-failure.yml +86 -0
- package/errors/silent-failures/github-token-no-trigger.yml +57 -57
- package/errors/silent-failures/reusable-workflow-env-secrets-empty.yml +90 -0
- package/errors/silent-failures/scheduled-workflow-disabled.yml +59 -59
- package/errors/silent-failures/sparse-checkout-sticky-cone-mode.yml +120 -0
- package/errors/triggers/cron-schedule-late.yml +59 -59
- package/errors/triggers/pull-request-target-rce-risk.yml +117 -0
- package/errors/triggers/workflow-not-triggering.yml +60 -60
- package/errors/triggers/workflow-run-default-branch-requirement.yml +78 -0
- package/errors/yaml-syntax/anchors-not-supported.yml +95 -0
- package/errors/yaml-syntax/dynamic-matrix-fromjson-failure.yml +99 -0
- package/errors/yaml-syntax/if-always-true.yml +52 -52
- package/errors/yaml-syntax/missing-expression-wrapper.yml +67 -0
- package/errors/yaml-syntax/needs-indirect-outputs.yml +91 -0
- package/errors/yaml-syntax/reusable-workflow-missing-output-declaration.yml +140 -0
- package/errors/yaml-syntax/secrets-in-if.yml +55 -55
- package/errors/yaml-syntax/unexpected-yaml-key.yml +69 -69
- package/errors/yaml-syntax/working-directory-ignored-on-uses.yml +66 -0
- 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"
|