@htekdev/actions-debugger 1.0.79 → 1.0.81
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-exact-key-hit-skips-save-stale.yml +81 -0
- package/errors/caching-artifacts/cache-lookup-only-no-restore-silent.yml +88 -0
- package/errors/caching-artifacts/upload-artifact-rerun-run-id-collision.yml +67 -0
- package/errors/concurrency-timing/workflow-dispatch-push-shared-concurrency-cancel.yml +64 -0
- package/errors/permissions-auth/dependabot-pr-repository-secrets-not-injected.yml +81 -0
- package/errors/silent-failures/if-failure-not-triggered-on-cancellation.yml +96 -0
- package/errors/triggers/workflow-dispatch-choice-input-api-no-validation.yml +98 -0
- package/errors/yaml-syntax/secrets-context-unavailable-in-if-condition.yml +72 -0
- package/package.json +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
id: caching-artifacts-046
|
|
2
|
+
title: "actions/cache exact key hit skips save step — caches go stale when dependency hash doesn't change"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- save-always
|
|
8
|
+
- cache-hit
|
|
9
|
+
- stale-cache
|
|
10
|
+
- cache-invalidation
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Cache hit occurred on the primary key'
|
|
13
|
+
flags: i
|
|
14
|
+
- regex: 'Not saving cache'
|
|
15
|
+
flags: i
|
|
16
|
+
error_messages:
|
|
17
|
+
- "Cache hit occurred on the primary key, not saving cache."
|
|
18
|
+
- "Not saving cache as it is not requested."
|
|
19
|
+
root_cause: |
|
|
20
|
+
When actions/cache finds an exact match for the primary key, it sets
|
|
21
|
+
cache-hit: 'true' and the post-step save is skipped entirely. The rationale is
|
|
22
|
+
that since the key already exists, re-saving an identical entry would be wasteful.
|
|
23
|
+
|
|
24
|
+
However, this behaviour means that cache content changes that do NOT affect the
|
|
25
|
+
hash (e.g., transitive package security patches, tool binary updates, or
|
|
26
|
+
generated files included in the cached path) are never persisted. The cache
|
|
27
|
+
entry stays frozen at the state it was in when the key was first saved.
|
|
28
|
+
|
|
29
|
+
Common scenario: a workflow caches node_modules keyed on package-lock.json hash.
|
|
30
|
+
The lock file goes unchanged for weeks, but npm packages receive silent patch
|
|
31
|
+
updates or a cached binary becomes corrupt. Every run gets a cache hit, skips the
|
|
32
|
+
save, and the stale or corrupt content continues to be served until the lock file
|
|
33
|
+
hash finally changes.
|
|
34
|
+
fix: |
|
|
35
|
+
Set save-always: true on the cache step to force a save even when the exact key
|
|
36
|
+
already exists. For finer control, split the action into separate
|
|
37
|
+
actions/cache/restore and actions/cache/save steps so the save can run
|
|
38
|
+
conditionally. To immediately invalidate a specific cache entry, delete it via
|
|
39
|
+
the GitHub REST API or the Actions UI under Repository > Actions > Caches.
|
|
40
|
+
fix_code:
|
|
41
|
+
- language: yaml
|
|
42
|
+
label: "Force save on every run with save-always"
|
|
43
|
+
code: |
|
|
44
|
+
- name: Cache node_modules
|
|
45
|
+
uses: actions/cache@v4
|
|
46
|
+
with:
|
|
47
|
+
path: node_modules
|
|
48
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
49
|
+
restore-keys: ${{ runner.os }}-node-
|
|
50
|
+
# Always save even if exact key already exists in the cache
|
|
51
|
+
save-always: true
|
|
52
|
+
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: "Split restore and save for conditional save logic"
|
|
55
|
+
code: |
|
|
56
|
+
- name: Restore cache
|
|
57
|
+
id: cache-restore
|
|
58
|
+
uses: actions/cache/restore@v4
|
|
59
|
+
with:
|
|
60
|
+
path: node_modules
|
|
61
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
62
|
+
restore-keys: ${{ runner.os }}-node-
|
|
63
|
+
|
|
64
|
+
- name: Install dependencies
|
|
65
|
+
run: npm ci
|
|
66
|
+
|
|
67
|
+
- name: Save cache
|
|
68
|
+
uses: actions/cache/save@v4
|
|
69
|
+
if: always()
|
|
70
|
+
with:
|
|
71
|
+
path: node_modules
|
|
72
|
+
key: ${{ steps.cache-restore.outputs.cache-primary-key }}
|
|
73
|
+
prevention:
|
|
74
|
+
- "Use save-always: true when cached content may diverge from the hash key (e.g., tool caches, binary downloads)."
|
|
75
|
+
- "Periodically evict stale cache entries via the GitHub REST API DELETE /repos/{owner}/{repo}/actions/caches/{cache_id}."
|
|
76
|
+
- "Include a tool version or date component in the cache key when the content should rotate on a schedule."
|
|
77
|
+
docs:
|
|
78
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows"
|
|
79
|
+
label: "GitHub Actions: Caching dependencies"
|
|
80
|
+
- url: "https://github.com/actions/cache#save-always"
|
|
81
|
+
label: "actions/cache README: save-always option"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
id: caching-artifacts-048
|
|
2
|
+
title: "actions/cache lookup-only: true checks cache existence but does NOT restore files"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- lookup-only
|
|
8
|
+
- cache-hit
|
|
9
|
+
- restore
|
|
10
|
+
- silent-no-op
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'lookup-only.*true'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'Cache found and can be restored from key'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
error_messages:
|
|
17
|
+
- "Cache found and can be restored from key: node-modules-abc123"
|
|
18
|
+
- "lookup-only is set, skipping restore."
|
|
19
|
+
root_cause: |
|
|
20
|
+
The lookup-only: true input on actions/cache@v4 instructs the action to query
|
|
21
|
+
the cache service and report whether an entry exists (via the cache-hit output)
|
|
22
|
+
without transferring any data to the runner. No files are downloaded or written
|
|
23
|
+
to disk.
|
|
24
|
+
|
|
25
|
+
Developers who add lookup-only: true hoping to get a "lightweight restore that
|
|
26
|
+
skips the upload post-step" receive a misleading green step. The cache-hit output
|
|
27
|
+
reports 'true', the step log says "Cache found and can be restored from key",
|
|
28
|
+
and everything looks successful — but the working directory is empty.
|
|
29
|
+
Subsequent build steps that depend on the cached files fail with cryptic errors
|
|
30
|
+
(missing executables, import errors, or blank node_modules) that appear unrelated
|
|
31
|
+
to caching.
|
|
32
|
+
|
|
33
|
+
The intended use case for lookup-only is a two-job pipeline: Job A checks if the
|
|
34
|
+
cache exists (lookup-only: true) and, if not, builds and saves it; Job B restores
|
|
35
|
+
normally. It is NOT a performance shortcut for regular restore-and-save workflows.
|
|
36
|
+
fix: |
|
|
37
|
+
Remove lookup-only: true from any step that should actually restore cached files.
|
|
38
|
+
Use lookup-only: true only in a dedicated pre-check job that gates whether an
|
|
39
|
+
expensive build step needs to run, combined with a normal actions/cache step in
|
|
40
|
+
the subsequent job that performs the real restore.
|
|
41
|
+
fix_code:
|
|
42
|
+
- language: yaml
|
|
43
|
+
label: "Remove lookup-only for normal cache restore (most cases)"
|
|
44
|
+
code: |
|
|
45
|
+
- name: Restore node_modules cache
|
|
46
|
+
id: cache
|
|
47
|
+
uses: actions/cache@v4
|
|
48
|
+
with:
|
|
49
|
+
path: node_modules
|
|
50
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
51
|
+
restore-keys: ${{ runner.os }}-node-
|
|
52
|
+
# Do NOT set lookup-only: true here — files must be restored
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: "Correct pattern: lookup-only in check job, real restore in build job"
|
|
55
|
+
code: |
|
|
56
|
+
jobs:
|
|
57
|
+
check-cache:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
outputs:
|
|
60
|
+
cache-hit: ${{ steps.lookup.outputs.cache-hit }}
|
|
61
|
+
steps:
|
|
62
|
+
- id: lookup
|
|
63
|
+
uses: actions/cache@v4
|
|
64
|
+
with:
|
|
65
|
+
path: node_modules
|
|
66
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
67
|
+
lookup-only: true # only check — no download
|
|
68
|
+
|
|
69
|
+
build:
|
|
70
|
+
needs: check-cache
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
steps:
|
|
73
|
+
- uses: actions/cache@v4
|
|
74
|
+
with:
|
|
75
|
+
path: node_modules
|
|
76
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
77
|
+
# no lookup-only — restores files and saves after build
|
|
78
|
+
- if: needs.check-cache.outputs.cache-hit != 'true'
|
|
79
|
+
run: npm ci
|
|
80
|
+
prevention:
|
|
81
|
+
- "Only use lookup-only: true in a dedicated cache-check job that feeds a conditional build gate."
|
|
82
|
+
- "After using lookup-only, always follow up with a standard actions/cache step (no lookup-only) in the job that needs the files."
|
|
83
|
+
- "If you want to skip the post-step cache save, use actions/cache/restore instead of setting lookup-only: true."
|
|
84
|
+
docs:
|
|
85
|
+
- url: "https://github.com/actions/cache#inputs"
|
|
86
|
+
label: "actions/cache README: inputs including lookup-only"
|
|
87
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows"
|
|
88
|
+
label: "GitHub Actions: Caching dependencies"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
id: caching-artifacts-047
|
|
2
|
+
title: "Re-run job fails with 'artifact already exists' when artifact name includes github.run_id"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- upload-artifact
|
|
7
|
+
- re-run
|
|
8
|
+
- run_id
|
|
9
|
+
- run_attempt
|
|
10
|
+
- artifact-name
|
|
11
|
+
- collision
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'An artifact with this name already exists on the workflow run'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'Unable to upload artifact.*already exists'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "An artifact with this name already exists on the workflow run."
|
|
19
|
+
- "Unable to upload artifact: An artifact with this name already exists on the run."
|
|
20
|
+
root_cause: |
|
|
21
|
+
github.run_id identifies a workflow run and remains the same value across all
|
|
22
|
+
re-run attempts triggered from the GitHub UI or REST API. When an artifact name
|
|
23
|
+
is constructed using github.run_id (e.g., my-build-${{ github.run_id }}), the
|
|
24
|
+
first attempt uploads successfully. If the job fails and is re-run, the re-run
|
|
25
|
+
attempt tries to create an artifact with the identical name on the same run.
|
|
26
|
+
|
|
27
|
+
actions/upload-artifact@v4 enforces unique artifact names per run as a hard
|
|
28
|
+
error (changed from v3 which silently overwrote). The re-run therefore fails
|
|
29
|
+
immediately with "artifact already exists" before any meaningful work is done.
|
|
30
|
+
|
|
31
|
+
Teams reach for run_id because it appears to be a unique-per-execution
|
|
32
|
+
identifier. It is not: it is unique per workflow trigger, stable across all
|
|
33
|
+
re-run attempts within that trigger.
|
|
34
|
+
fix: |
|
|
35
|
+
Append github.run_attempt to the artifact name. run_attempt starts at 1 and
|
|
36
|
+
increments for each re-run of the same run_id, making the combined value unique
|
|
37
|
+
across attempts. Consumers of the artifact (download steps, workflow_run
|
|
38
|
+
triggers) must include run_attempt in their own references to locate the
|
|
39
|
+
correct artifact.
|
|
40
|
+
fix_code:
|
|
41
|
+
- language: yaml
|
|
42
|
+
label: "Include run_attempt to make artifact name unique across re-runs"
|
|
43
|
+
code: |
|
|
44
|
+
- name: Upload build artifact
|
|
45
|
+
uses: actions/upload-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
# run_attempt increments on each re-run, preventing name collision
|
|
48
|
+
name: my-build-${{ github.run_id }}-${{ github.run_attempt }}
|
|
49
|
+
path: dist/
|
|
50
|
+
- language: yaml
|
|
51
|
+
label: "Download the matching artifact in a downstream job"
|
|
52
|
+
code: |
|
|
53
|
+
- name: Download build artifact
|
|
54
|
+
uses: actions/download-artifact@v4
|
|
55
|
+
with:
|
|
56
|
+
name: my-build-${{ github.run_id }}-${{ github.run_attempt }}
|
|
57
|
+
path: dist/
|
|
58
|
+
prevention:
|
|
59
|
+
- "Never use github.run_id alone as an artifact name uniqueness guarantee — it is stable across all re-runs of the same run."
|
|
60
|
+
- "Combine github.run_id with github.run_attempt to produce a name that is unique within a run AND across re-run attempts."
|
|
61
|
+
- "Use a matrix value or job name as the differentiator instead of run_id when the artifact is consumed by a job in the same workflow."
|
|
62
|
+
- "Check the GitHub UI Actions > Artifacts list to inspect existing artifact names before debugging re-run failures."
|
|
63
|
+
docs:
|
|
64
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/storing-workflow-data-as-artifacts"
|
|
65
|
+
label: "GitHub Actions: Storing workflow data as artifacts"
|
|
66
|
+
- url: "https://github.com/actions/upload-artifact#not-uploading-to-the-same-artifact"
|
|
67
|
+
label: "actions/upload-artifact: unique artifact names per run"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
id: concurrency-timing-040
|
|
2
|
+
title: "workflow_dispatch and push share concurrency group, dispatch cancels in-progress CI"
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- workflow-dispatch
|
|
8
|
+
- push
|
|
9
|
+
- cancel-in-progress
|
|
10
|
+
- event-name
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'This run was cancelled'
|
|
13
|
+
flags: i
|
|
14
|
+
- regex: 'concurrency.*workflow_dispatch|workflow_dispatch.*concurrency'
|
|
15
|
+
flags: ims
|
|
16
|
+
error_messages:
|
|
17
|
+
- "This run was cancelled."
|
|
18
|
+
- "A run for this workflow is already in progress."
|
|
19
|
+
root_cause: |
|
|
20
|
+
When a concurrency group key is built from github.workflow and github.ref alone,
|
|
21
|
+
both workflow_dispatch and push events on the same branch produce an identical key.
|
|
22
|
+
GitHub treats them as competing occupants of the same concurrency slot.
|
|
23
|
+
|
|
24
|
+
With cancel-in-progress: true, a manually dispatched run immediately cancels any
|
|
25
|
+
push-triggered CI run already in progress (or vice versa). Without cancel-in-progress,
|
|
26
|
+
the dispatch run queues behind the push run but still competes for the same slot.
|
|
27
|
+
|
|
28
|
+
This catches developers off guard when they manually trigger a workflow for
|
|
29
|
+
debugging while CI is running — the in-progress CI run is silently cancelled. Or a
|
|
30
|
+
push to the same branch during a long dispatch run causes the dispatch to be evicted.
|
|
31
|
+
fix: |
|
|
32
|
+
Include github.event_name in the concurrency group key to give workflow_dispatch
|
|
33
|
+
and push events separate concurrency slots. Alternatively, use a conditional
|
|
34
|
+
cancel-in-progress expression so manual dispatches are never cancelled.
|
|
35
|
+
fix_code:
|
|
36
|
+
- language: yaml
|
|
37
|
+
label: "Include event_name to separate dispatch and push slots"
|
|
38
|
+
code: |
|
|
39
|
+
# WRONG: dispatch and push on same branch share a slot and cancel each other
|
|
40
|
+
# concurrency:
|
|
41
|
+
# group: ${{ github.workflow }}-${{ github.ref }}
|
|
42
|
+
# cancel-in-progress: true
|
|
43
|
+
|
|
44
|
+
# CORRECT: each event type gets its own concurrency slot
|
|
45
|
+
concurrency:
|
|
46
|
+
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
|
47
|
+
cancel-in-progress: true
|
|
48
|
+
|
|
49
|
+
- language: yaml
|
|
50
|
+
label: "Alternative: only cancel-in-progress for push and pull_request, not dispatch"
|
|
51
|
+
code: |
|
|
52
|
+
concurrency:
|
|
53
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
54
|
+
# Manual dispatches are never cancelled; push/PR runs are stacked-and-cancelled
|
|
55
|
+
cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }}
|
|
56
|
+
prevention:
|
|
57
|
+
- "Always include github.event_name in concurrency group keys when a workflow has multiple trigger events."
|
|
58
|
+
- "Test concurrency behavior by triggering both events in quick succession; bugs only surface under race conditions."
|
|
59
|
+
- "For release or deploy workflows, consider separate workflows for dispatch vs automated CI rather than sharing concurrency."
|
|
60
|
+
docs:
|
|
61
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#concurrency"
|
|
62
|
+
label: "GitHub Actions: Concurrency syntax"
|
|
63
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_dispatch"
|
|
64
|
+
label: "GitHub Actions: workflow_dispatch event"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
id: permissions-auth-048
|
|
2
|
+
title: "Dependabot PR workflows cannot access repository secrets — only Dependabot secrets are injected"
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- dependabot
|
|
7
|
+
- secrets
|
|
8
|
+
- pull-request
|
|
9
|
+
- registry-auth
|
|
10
|
+
- secret-isolation
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Input required and not supplied'
|
|
13
|
+
flags: i
|
|
14
|
+
- regex: '401 Unauthorized'
|
|
15
|
+
flags: i
|
|
16
|
+
error_messages:
|
|
17
|
+
- "Error: Input required and not supplied: token"
|
|
18
|
+
- "npm ERR! code E401"
|
|
19
|
+
- "Error: Unauthorized"
|
|
20
|
+
root_cause: |
|
|
21
|
+
When Dependabot opens or updates a pull request, GitHub Actions workflows triggered
|
|
22
|
+
by that PR run in an isolated secret context. Repository secrets (defined under
|
|
23
|
+
Settings > Secrets and variables > Actions) are NOT injected into Dependabot-triggered
|
|
24
|
+
workflow runs. Only secrets defined under Settings > Secrets and variables > Dependabot
|
|
25
|
+
are available.
|
|
26
|
+
|
|
27
|
+
This means steps that reference secrets.NPM_TOKEN, secrets.DOCKER_PASSWORD,
|
|
28
|
+
secrets.SONAR_TOKEN, or any custom repository secret will receive an empty string.
|
|
29
|
+
Actions that validate their inputs (e.g., a custom action with required: true on a
|
|
30
|
+
token input) will fail with "Input required and not supplied". Registry steps will
|
|
31
|
+
fail with 401 Unauthorized.
|
|
32
|
+
|
|
33
|
+
GITHUB_TOKEN is exempt and is still injected normally for Dependabot runs.
|
|
34
|
+
fix: |
|
|
35
|
+
Re-create the required secrets under Settings > Secrets and variables > Dependabot
|
|
36
|
+
(a separate namespace from Actions secrets). Dependabot reads from its own secret
|
|
37
|
+
store, not the repository Actions secret store. Alternatively, restructure the
|
|
38
|
+
workflow so secret-dependent steps only run on non-Dependabot actors using an
|
|
39
|
+
if: condition.
|
|
40
|
+
fix_code:
|
|
41
|
+
- language: yaml
|
|
42
|
+
label: "Add secrets to Dependabot secret store in GitHub UI"
|
|
43
|
+
code: |
|
|
44
|
+
# In GitHub: Settings > Secrets and variables > Dependabot
|
|
45
|
+
# Add the same secret names you use in your Actions secrets.
|
|
46
|
+
# They are stored separately and only injected for Dependabot-triggered runs.
|
|
47
|
+
|
|
48
|
+
# No workflow YAML change required — just add the secret in the UI.
|
|
49
|
+
# Example workflow remains unchanged:
|
|
50
|
+
jobs:
|
|
51
|
+
build:
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
- run: npm publish
|
|
56
|
+
env:
|
|
57
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # Works once added to Dependabot secrets
|
|
58
|
+
|
|
59
|
+
- language: yaml
|
|
60
|
+
label: "Skip secret-dependent steps when actor is dependabot"
|
|
61
|
+
code: |
|
|
62
|
+
jobs:
|
|
63
|
+
build:
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v4
|
|
67
|
+
- name: Publish to registry
|
|
68
|
+
# Skip publish step for Dependabot PRs that lack the token
|
|
69
|
+
if: github.actor != 'dependabot[bot]'
|
|
70
|
+
run: npm publish
|
|
71
|
+
env:
|
|
72
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
73
|
+
prevention:
|
|
74
|
+
- "Maintain parallel secrets under both Actions and Dependabot secret stores for tokens Dependabot PRs need."
|
|
75
|
+
- "Audit workflows triggered by pull_request to identify steps using custom secrets — they silently fail for Dependabot."
|
|
76
|
+
- "Use github.actor != 'dependabot[bot]' guards on publish/deploy steps that Dependabot PRs should skip."
|
|
77
|
+
docs:
|
|
78
|
+
- url: "https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions#accessing-secrets"
|
|
79
|
+
label: "GitHub Docs: Accessing secrets in Dependabot workflows"
|
|
80
|
+
- url: "https://docs.github.com/en/code-security/dependabot/working-with-dependabot/configuring-access-to-private-registries-for-dependabot"
|
|
81
|
+
label: "GitHub Docs: Dependabot and private registries"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
id: silent-failures-075
|
|
2
|
+
title: "if: failure() notification job is skipped when workflow is manually cancelled"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- if-condition
|
|
7
|
+
- failure
|
|
8
|
+
- cancelled
|
|
9
|
+
- always
|
|
10
|
+
- notification
|
|
11
|
+
- cleanup
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'if:\s*failure\(\)'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'Skipping.*cancelled'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Skipping: The job was skipped because one or more of its needs jobs was cancelled."
|
|
19
|
+
root_cause: |
|
|
20
|
+
When a workflow is manually cancelled (via the GitHub UI "Cancel run" button or
|
|
21
|
+
the REST API), in-progress jobs receive a cancellation signal. Jobs that have not
|
|
22
|
+
yet started are given the conclusion "cancelled". Jobs with if: failure() are
|
|
23
|
+
only evaluated against the "failure" conclusion — "cancelled" is a distinct
|
|
24
|
+
conclusion value. Because cancelled != failure, the condition evaluates to false
|
|
25
|
+
and the notification or cleanup job is silently skipped.
|
|
26
|
+
|
|
27
|
+
This catches teams by surprise when they add an alerting job intended to fire
|
|
28
|
+
whenever CI does not succeed:
|
|
29
|
+
|
|
30
|
+
notify-on-failure:
|
|
31
|
+
needs: build
|
|
32
|
+
if: failure()
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- name: Send Slack alert
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
When an operator cancels a run mid-flight, the build job is marked cancelled,
|
|
39
|
+
not failed. The notify job evaluates if: failure() → false and is skipped with
|
|
40
|
+
no Slack message. The on-call team receives no signal that a run was interrupted.
|
|
41
|
+
|
|
42
|
+
The failure() function returns true only when at least one needed job has a
|
|
43
|
+
"failure" conclusion. It does NOT return true for "cancelled" conclusions.
|
|
44
|
+
fix: |
|
|
45
|
+
Use if: failure() || cancelled() to catch both failure and cancellation, or use
|
|
46
|
+
if: always() combined with a conditional check on the job's outcome inside the
|
|
47
|
+
step if you only want to act on non-success outcomes.
|
|
48
|
+
fix_code:
|
|
49
|
+
- language: yaml
|
|
50
|
+
label: "Catch both failure and cancellation in a notification job"
|
|
51
|
+
code: |
|
|
52
|
+
jobs:
|
|
53
|
+
build:
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
steps:
|
|
56
|
+
- run: make build
|
|
57
|
+
|
|
58
|
+
notify-on-failure:
|
|
59
|
+
needs: build
|
|
60
|
+
# failure() catches job failures; cancelled() catches manual cancellation
|
|
61
|
+
if: failure() || cancelled()
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
steps:
|
|
64
|
+
- name: Send failure or cancellation alert
|
|
65
|
+
run: |
|
|
66
|
+
echo "Build concluded: ${{ needs.build.result }}"
|
|
67
|
+
# Send Slack/PagerDuty notification here
|
|
68
|
+
- language: yaml
|
|
69
|
+
label: "Use always() and gate on outcome inside the step for fine-grained control"
|
|
70
|
+
code: |
|
|
71
|
+
jobs:
|
|
72
|
+
build:
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- run: make build
|
|
76
|
+
|
|
77
|
+
notify:
|
|
78
|
+
needs: build
|
|
79
|
+
if: always()
|
|
80
|
+
runs-on: ubuntu-latest
|
|
81
|
+
steps:
|
|
82
|
+
- name: Notify only on non-success outcomes
|
|
83
|
+
if: needs.build.result != 'success'
|
|
84
|
+
run: |
|
|
85
|
+
echo "Build result: ${{ needs.build.result }}"
|
|
86
|
+
# Handles: failure, cancelled, skipped
|
|
87
|
+
prevention:
|
|
88
|
+
- "Use if: failure() || cancelled() instead of if: failure() for any alert, notification, or cleanup job that must run whenever CI does not succeed."
|
|
89
|
+
- "Prefer if: always() when the downstream job should run regardless of upstream outcome (e.g., always upload test reports)."
|
|
90
|
+
- "Audit all notification and cleanup jobs after cancellation to confirm they fired as expected — cancelled runs don't generate failure alerts by default."
|
|
91
|
+
- "The cancelled() function returns true when the workflow or job has been cancelled; combine with failure() using || for comprehensive coverage."
|
|
92
|
+
docs:
|
|
93
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-conditions-to-control-job-execution"
|
|
94
|
+
label: "GitHub Actions: Using conditions to control job execution"
|
|
95
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-conditions-to-control-job-execution#available-status-check-functions"
|
|
96
|
+
label: "GitHub Actions: Available status check functions — failure(), cancelled(), always()"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
id: triggers-054
|
|
2
|
+
title: "workflow_dispatch choice input not validated when triggered via REST API — invalid values silently accepted"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- workflow-dispatch
|
|
7
|
+
- choice-input
|
|
8
|
+
- rest-api
|
|
9
|
+
- inputs
|
|
10
|
+
- validation
|
|
11
|
+
- gh-cli
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'type:\s*choice'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'inputs\.\w+.*choice'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Workflow ran with input value not in declared choices list"
|
|
19
|
+
root_cause: |
|
|
20
|
+
The type: choice input in on.workflow_dispatch.inputs renders a dropdown
|
|
21
|
+
selector in the GitHub Actions UI and prevents users from submitting values
|
|
22
|
+
outside the declared options list. This validation is enforced entirely
|
|
23
|
+
client-side in the browser.
|
|
24
|
+
|
|
25
|
+
When a workflow is triggered via the GitHub REST API
|
|
26
|
+
(POST /repos/{owner}/{repo}/actions/workflows/{id}/dispatches) or the GitHub
|
|
27
|
+
CLI (gh workflow run --field key=value), GitHub does NOT validate that the
|
|
28
|
+
supplied value matches any of the declared choices. Any arbitrary string is
|
|
29
|
+
accepted and forwarded as-is to the workflow.
|
|
30
|
+
|
|
31
|
+
A workflow dispatched with inputs: { environment: "PROD" } when the declared
|
|
32
|
+
choices are ["prod", "staging", "dev"] will run with inputs.environment == "PROD".
|
|
33
|
+
Every if: condition that compares against the lower-case variants evaluates to
|
|
34
|
+
false, all deployment branches are skipped, and the workflow exits successfully
|
|
35
|
+
with no indication that the value was invalid. The mistyped run is invisible
|
|
36
|
+
unless logs are inspected manually.
|
|
37
|
+
fix: |
|
|
38
|
+
Add an explicit allowlist validation step at the top of the workflow that checks
|
|
39
|
+
the input value and exits with a non-zero status and a descriptive error message
|
|
40
|
+
if the value is not in the expected set. This makes API-triggered runs fail fast
|
|
41
|
+
with a clear error rather than silently no-oping.
|
|
42
|
+
fix_code:
|
|
43
|
+
- language: yaml
|
|
44
|
+
label: "Validate choice input with an allowlist check"
|
|
45
|
+
code: |
|
|
46
|
+
on:
|
|
47
|
+
workflow_dispatch:
|
|
48
|
+
inputs:
|
|
49
|
+
environment:
|
|
50
|
+
type: choice
|
|
51
|
+
description: Target deployment environment
|
|
52
|
+
options:
|
|
53
|
+
- prod
|
|
54
|
+
- staging
|
|
55
|
+
- dev
|
|
56
|
+
|
|
57
|
+
jobs:
|
|
58
|
+
deploy:
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
steps:
|
|
61
|
+
- name: Validate environment input
|
|
62
|
+
run: |
|
|
63
|
+
allowed="prod staging dev"
|
|
64
|
+
input="${{ inputs.environment }}"
|
|
65
|
+
if [[ ! " $allowed " =~ " ${input} " ]]; then
|
|
66
|
+
echo "::error::Invalid environment '${input}'. Allowed values: $allowed"
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
- name: Deploy to ${{ inputs.environment }}
|
|
71
|
+
run: echo "Deploying to ${{ inputs.environment }}"
|
|
72
|
+
- language: yaml
|
|
73
|
+
label: "Case-insensitive validation with normalisation"
|
|
74
|
+
code: |
|
|
75
|
+
- name: Validate and normalise environment input
|
|
76
|
+
id: validate
|
|
77
|
+
run: |
|
|
78
|
+
input=$(echo "${{ inputs.environment }}" | tr '[:upper:]' '[:lower:]')
|
|
79
|
+
allowed=("prod" "staging" "dev")
|
|
80
|
+
valid=false
|
|
81
|
+
for v in "${allowed[@]}"; do
|
|
82
|
+
[[ "$input" == "$v" ]] && valid=true && break
|
|
83
|
+
done
|
|
84
|
+
if [[ "$valid" != "true" ]]; then
|
|
85
|
+
echo "::error::Unrecognised environment '${{ inputs.environment }}' (normalised: '$input'). Allowed: ${allowed[*]}"
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
echo "environment=$input" >> "$GITHUB_OUTPUT"
|
|
89
|
+
prevention:
|
|
90
|
+
- "Treat type: choice as a UI hint only — always validate inputs programmatically inside the workflow."
|
|
91
|
+
- "Add an allowlist guard as the first step of any job that branches on a choice input."
|
|
92
|
+
- "Document valid values and casing rules in the input description field so API callers know the expected format."
|
|
93
|
+
- "Use actionlint to detect misspelled choices at authoring time before they reach production."
|
|
94
|
+
docs:
|
|
95
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_dispatch"
|
|
96
|
+
label: "GitHub Actions: workflow_dispatch event — inputs"
|
|
97
|
+
- url: "https://docs.github.com/en/rest/actions/workflows#create-a-workflow-dispatch-event"
|
|
98
|
+
label: "GitHub REST API: Create a workflow dispatch event"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
id: yaml-syntax-050
|
|
2
|
+
title: "secrets context used in job/step if: condition rejected — only valid in env: and with: blocks"
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- secrets
|
|
7
|
+
- if-condition
|
|
8
|
+
- expression-context
|
|
9
|
+
- actionlint
|
|
10
|
+
- context-availability
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Unrecognized named-value: ''secrets'''
|
|
13
|
+
flags: i
|
|
14
|
+
- regex: 'Context access might be invalid: secrets'
|
|
15
|
+
flags: i
|
|
16
|
+
error_messages:
|
|
17
|
+
- "Unrecognized named-value: 'secrets'. Located at position 1 within expression"
|
|
18
|
+
- "Context access might be invalid: secrets"
|
|
19
|
+
root_cause: |
|
|
20
|
+
The secrets context is only available inside env: blocks and action input with: blocks.
|
|
21
|
+
It cannot be used in if: conditions at the job or step level.
|
|
22
|
+
|
|
23
|
+
A common pattern is to conditionally run a deployment step only when a secret is
|
|
24
|
+
configured: if: secrets.DEPLOY_TOKEN != ''. GitHub's expression evaluator rejects
|
|
25
|
+
this at runtime with "Unrecognized named-value: 'secrets'", failing the step or job
|
|
26
|
+
before it even starts. actionlint reports this as a static analysis error under
|
|
27
|
+
"Context access might be invalid: secrets".
|
|
28
|
+
|
|
29
|
+
The restriction is documented in GitHub's context availability table: the secrets
|
|
30
|
+
context is listed as unavailable for job.<job_id>.if and steps.<step_id>.if.
|
|
31
|
+
fix: |
|
|
32
|
+
Map the secret to an environment variable in an env: block, then check the
|
|
33
|
+
environment variable in the if: condition using the env context. For a job-level
|
|
34
|
+
gate, set the env var at the job level and reference it in a step if: condition.
|
|
35
|
+
fix_code:
|
|
36
|
+
- language: yaml
|
|
37
|
+
label: "Bridge secret through env var for step-level if:"
|
|
38
|
+
code: |
|
|
39
|
+
# WRONG: secrets context not available in if: conditions
|
|
40
|
+
# - name: Deploy
|
|
41
|
+
# if: secrets.DEPLOY_TOKEN != ''
|
|
42
|
+
# run: ./deploy.sh
|
|
43
|
+
|
|
44
|
+
# CORRECT: map secret to env var, check env var in if:
|
|
45
|
+
- name: Deploy
|
|
46
|
+
if: env.DEPLOY_TOKEN != ''
|
|
47
|
+
env:
|
|
48
|
+
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
|
49
|
+
run: ./deploy.sh
|
|
50
|
+
|
|
51
|
+
- language: yaml
|
|
52
|
+
label: "Job-level env gate evaluated in step if:"
|
|
53
|
+
code: |
|
|
54
|
+
jobs:
|
|
55
|
+
deploy:
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
env:
|
|
58
|
+
# Set at job level so all steps can reference it
|
|
59
|
+
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
|
60
|
+
steps:
|
|
61
|
+
- name: Deploy
|
|
62
|
+
if: env.DEPLOY_TOKEN != ''
|
|
63
|
+
run: ./deploy.sh
|
|
64
|
+
prevention:
|
|
65
|
+
- "Never reference secrets.* in if: conditions — always bridge through an env: variable first."
|
|
66
|
+
- "Run actionlint in CI to catch 'Context access might be invalid: secrets' errors before they reach production."
|
|
67
|
+
- "Consider using a boolean repository variable (vars.DEPLOY_ENABLED) as a conditional gate rather than checking secret presence."
|
|
68
|
+
docs:
|
|
69
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#context-availability"
|
|
70
|
+
label: "GitHub Actions: Context availability"
|
|
71
|
+
- url: "https://rhysd.github.io/actionlint/checks.html#check-contexts-and-special-functions"
|
|
72
|
+
label: "actionlint: Context availability checks"
|
package/package.json
CHANGED