@htekdev/actions-debugger 1.0.94 → 1.0.96
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/caching-artifacts-052.yml +107 -0
- package/errors/caching-artifacts/caching-artifacts-053.yml +90 -0
- package/errors/concurrency-timing/concurrency-timing-045.yml +70 -0
- package/errors/known-unsolved/known-unsolved-053.yml +114 -0
- package/errors/permissions-auth/permissions-auth-053.yml +103 -0
- package/errors/permissions-auth/permissions-auth-054.yml +99 -0
- package/errors/silent-failures/silent-failures-087.yml +120 -0
- package/errors/yaml-syntax/yaml-syntax-059.yml +93 -0
- package/package.json +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
id: caching-artifacts-052
|
|
2
|
+
title: '`download-artifact@v4` `name:` Does Not Support Glob Patterns — Use `pattern:` Instead'
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- download-artifact
|
|
7
|
+
- v4-breaking-change
|
|
8
|
+
- glob
|
|
9
|
+
- pattern
|
|
10
|
+
- migration
|
|
11
|
+
- artifact-download
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'download-artifact@v4|download-artifact@v3.*name.*\*'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'name\s*:\s*[''"][^''"]*\*[^''"]*[''"]'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "No artifacts found with the provided name"
|
|
19
|
+
- "Error: Unable to find any artifacts for the associated workflow"
|
|
20
|
+
- "Unable to find artifact 'my-*-artifact'"
|
|
21
|
+
root_cause: |
|
|
22
|
+
In `actions/download-artifact@v3`, the `name:` input accepted glob patterns
|
|
23
|
+
(e.g., `name: 'build-*'`) and would download all matching artifacts.
|
|
24
|
+
In `actions/download-artifact@v4` this behavior changed: `name:` now requires
|
|
25
|
+
an EXACT artifact name match. Glob characters in `name:` are treated as
|
|
26
|
+
literal characters, so `name: 'build-*'` looks for an artifact literally
|
|
27
|
+
named `build-*`, finds none, and either errors or silently downloads nothing
|
|
28
|
+
depending on the `error-no-files-found` setting.
|
|
29
|
+
|
|
30
|
+
The `pattern:` input was introduced in v4 as the replacement for glob-based
|
|
31
|
+
artifact selection. Migrating from v3 to v4 requires moving glob expressions
|
|
32
|
+
from `name:` to `pattern:`.
|
|
33
|
+
|
|
34
|
+
This silently impacts workflows that:
|
|
35
|
+
- Download multiple build artifacts from matrix jobs using a shared prefix
|
|
36
|
+
- Use wildcards to grab all artifacts from a set of parallel jobs
|
|
37
|
+
- Were migrated to v4 without reading the full migration guide
|
|
38
|
+
fix: |
|
|
39
|
+
Replace glob patterns in `name:` with the `pattern:` input in
|
|
40
|
+
`actions/download-artifact@v4`. The `name:` input should only be used
|
|
41
|
+
when downloading a single artifact by its exact name.
|
|
42
|
+
|
|
43
|
+
When using `pattern:`, the action also has a `merge-multiple` option
|
|
44
|
+
that controls whether matched artifacts are merged into one directory
|
|
45
|
+
or placed in separate subdirectories.
|
|
46
|
+
fix_code:
|
|
47
|
+
- language: yaml
|
|
48
|
+
label: "WRONG — glob in name: silently matches nothing in v4"
|
|
49
|
+
code: |
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/download-artifact@v4
|
|
52
|
+
with:
|
|
53
|
+
name: 'build-*' # ❌ globs not supported in name: for v4
|
|
54
|
+
path: ./artifacts
|
|
55
|
+
- language: yaml
|
|
56
|
+
label: "RIGHT — use pattern: for glob matching in v4"
|
|
57
|
+
code: |
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/download-artifact@v4
|
|
60
|
+
with:
|
|
61
|
+
pattern: 'build-*' # ✅ use pattern: for glob matching
|
|
62
|
+
path: ./artifacts
|
|
63
|
+
merge-multiple: true # flatten into single directory
|
|
64
|
+
- language: yaml
|
|
65
|
+
label: "RIGHT — download exact artifact by name in v4"
|
|
66
|
+
code: |
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/download-artifact@v4
|
|
69
|
+
with:
|
|
70
|
+
name: build-linux-amd64 # ✅ exact name, no glob needed
|
|
71
|
+
path: ./dist/linux
|
|
72
|
+
- language: yaml
|
|
73
|
+
label: "Matrix upload + glob download pattern"
|
|
74
|
+
code: |
|
|
75
|
+
jobs:
|
|
76
|
+
build:
|
|
77
|
+
strategy:
|
|
78
|
+
matrix:
|
|
79
|
+
os: [linux, windows, macos]
|
|
80
|
+
runs-on: ubuntu-latest
|
|
81
|
+
steps:
|
|
82
|
+
- uses: actions/upload-artifact@v4
|
|
83
|
+
with:
|
|
84
|
+
name: build-${{ matrix.os }} # distinct name per job
|
|
85
|
+
path: ./dist/
|
|
86
|
+
|
|
87
|
+
package:
|
|
88
|
+
needs: build
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
steps:
|
|
91
|
+
- uses: actions/download-artifact@v4
|
|
92
|
+
with:
|
|
93
|
+
pattern: 'build-*' # ✅ downloads all build-* artifacts
|
|
94
|
+
path: ./all-builds
|
|
95
|
+
merge-multiple: false # keep per-artifact subdirectories
|
|
96
|
+
prevention:
|
|
97
|
+
- "When migrating from download-artifact@v3 to @v4, audit all `name:` inputs for glob characters and move them to `pattern:`."
|
|
98
|
+
- "Use `name:` only for exact single-artifact downloads; use `pattern:` for any glob or multi-artifact download."
|
|
99
|
+
- "Set `error-no-files-found: error` to fail fast when no artifacts match, exposing glob issues early."
|
|
100
|
+
- "Review the download-artifact v4 migration guide before updating the action version."
|
|
101
|
+
docs:
|
|
102
|
+
- url: "https://github.com/actions/download-artifact/blob/main/docs/MIGRATION.md"
|
|
103
|
+
label: "download-artifact v4 migration guide"
|
|
104
|
+
- url: "https://github.com/actions/download-artifact#inputs"
|
|
105
|
+
label: "download-artifact — inputs reference (pattern vs name)"
|
|
106
|
+
- url: "https://github.com/actions/toolkit/releases/tag/%40actions%2Fartifact%402.0.0"
|
|
107
|
+
label: "actions/toolkit v2 — artifact v2 API underlying v4 action"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
id: caching-artifacts-053
|
|
2
|
+
title: "download-artifact@v4 finds no artifacts in workflow_run context without run-id"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- download-artifact
|
|
7
|
+
- workflow_run
|
|
8
|
+
- run-id
|
|
9
|
+
- cross-workflow
|
|
10
|
+
- artifact
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'No artifacts found'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'Unable to find any artifacts for the associated workflow'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
error_messages:
|
|
17
|
+
- "No artifacts found for the associated workflow run"
|
|
18
|
+
- "Unable to find any artifacts for the associated workflow"
|
|
19
|
+
- "Error: Unable to find any artifacts for the associated workflow run"
|
|
20
|
+
root_cause: |
|
|
21
|
+
`actions/download-artifact@v4` defaults to downloading artifacts from the
|
|
22
|
+
CURRENT workflow run (`github.run_id`). In a `workflow_run`-triggered
|
|
23
|
+
workflow, the current run is the downstream (deploy) workflow — which has
|
|
24
|
+
produced no artifacts. The upstream (build) workflow's artifacts belong to
|
|
25
|
+
the triggering run, identified by `github.event.workflow_run.id`.
|
|
26
|
+
|
|
27
|
+
Without setting `run-id: ${{ github.event.workflow_run.id }}`, the download
|
|
28
|
+
step searches the current run's artifacts, finds nothing, and either fails
|
|
29
|
+
with "No artifacts found" or silently exits (depending on `if-no-files-found`
|
|
30
|
+
setting). No artifact was ever associated with the downstream run, so the
|
|
31
|
+
error can be confusing — the artifact clearly exists in the Actions UI under
|
|
32
|
+
the upstream run.
|
|
33
|
+
|
|
34
|
+
This is a distinct issue from the cross-run permissions error (ca-040):
|
|
35
|
+
that occurs when `run-id` IS set but `actions: read` permission is missing.
|
|
36
|
+
This issue occurs when `run-id` is simply not set at all.
|
|
37
|
+
fix: |
|
|
38
|
+
Set `run-id: ${{ github.event.workflow_run.id }}` on the download step to
|
|
39
|
+
target the triggering workflow's run. Also provide `github-token` (required
|
|
40
|
+
for cross-run downloads) and ensure `actions: read` permission is set.
|
|
41
|
+
fix_code:
|
|
42
|
+
- language: yaml
|
|
43
|
+
label: "Correct: set run-id from triggering workflow in workflow_run context"
|
|
44
|
+
code: |
|
|
45
|
+
on:
|
|
46
|
+
workflow_run:
|
|
47
|
+
workflows: ["CI"]
|
|
48
|
+
types: [completed]
|
|
49
|
+
|
|
50
|
+
permissions:
|
|
51
|
+
actions: read
|
|
52
|
+
contents: read
|
|
53
|
+
|
|
54
|
+
jobs:
|
|
55
|
+
deploy:
|
|
56
|
+
if: github.event.workflow_run.conclusion == 'success'
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
steps:
|
|
59
|
+
- name: Download build artifacts
|
|
60
|
+
uses: actions/download-artifact@v4
|
|
61
|
+
with:
|
|
62
|
+
name: build-output
|
|
63
|
+
# Required: point to the upstream run, not the current run
|
|
64
|
+
run-id: ${{ github.event.workflow_run.id }}
|
|
65
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
66
|
+
|
|
67
|
+
- name: Deploy
|
|
68
|
+
run: echo "Deploying from artifact"
|
|
69
|
+
- language: yaml
|
|
70
|
+
label: "Incorrect: missing run-id causes 'No artifacts found'"
|
|
71
|
+
code: |
|
|
72
|
+
jobs:
|
|
73
|
+
deploy:
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
steps:
|
|
76
|
+
- name: Download build artifacts
|
|
77
|
+
uses: actions/download-artifact@v4
|
|
78
|
+
with:
|
|
79
|
+
name: build-output
|
|
80
|
+
# Missing run-id — searches the CURRENT run, which has no artifacts
|
|
81
|
+
prevention:
|
|
82
|
+
- "In any `workflow_run`-triggered workflow, always set `run-id: ${{ github.event.workflow_run.id }}`"
|
|
83
|
+
- "Pair with `github-token: ${{ secrets.GITHUB_TOKEN }}` and `permissions: actions: read`"
|
|
84
|
+
- "Verify artifacts exist under the triggering run via the Actions UI before debugging download steps"
|
|
85
|
+
- "Name artifacts consistently between upload (in CI) and download (in deploy) workflows"
|
|
86
|
+
docs:
|
|
87
|
+
- url: "https://github.com/actions/download-artifact?tab=readme-ov-file#download-artifacts-from-other-workflow-runs-or-repositories"
|
|
88
|
+
label: "actions/download-artifact: Downloading from other workflow runs"
|
|
89
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_run"
|
|
90
|
+
label: "GitHub Docs: workflow_run event"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
id: concurrency-timing-045
|
|
2
|
+
title: "workflow_run-triggered workflows run concurrently — no upstream concurrency linkage"
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- workflow_run
|
|
7
|
+
- concurrency
|
|
8
|
+
- deployment
|
|
9
|
+
- build-deploy-pipeline
|
|
10
|
+
- downstream-workflow
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'on:\s+workflow_run:'
|
|
13
|
+
flags: 'si'
|
|
14
|
+
error_messages:
|
|
15
|
+
- "Multiple deployments triggered simultaneously for the same branch"
|
|
16
|
+
- "Deploy workflow triggered concurrently"
|
|
17
|
+
root_cause: |
|
|
18
|
+
Workflows triggered via `on: workflow_run:` do not inherit any concurrency
|
|
19
|
+
group from the triggering (upstream) workflow. If multiple upstream runs
|
|
20
|
+
complete in quick succession — for example, two commits pushed rapidly — each
|
|
21
|
+
`completed` event spawns an independent downstream run. All downstream runs
|
|
22
|
+
execute concurrently with no serialization or cancellation, even when the
|
|
23
|
+
upstream workflow had a concurrency group that serialized upstream runs.
|
|
24
|
+
|
|
25
|
+
The downstream workflow runs in the context of the default branch and receives
|
|
26
|
+
the triggering run's metadata via `github.event.workflow_run.*`, but GitHub
|
|
27
|
+
does not propagate any concurrency scope from upstream to downstream.
|
|
28
|
+
|
|
29
|
+
For build-then-deploy pipelines this creates a race condition: two deploy runs
|
|
30
|
+
may begin simultaneously, with whichever finishes last determining the final
|
|
31
|
+
deployed state regardless of commit order.
|
|
32
|
+
fix: |
|
|
33
|
+
Add an explicit `concurrency:` block to the `workflow_run`-triggered workflow,
|
|
34
|
+
keyed on `github.event.workflow_run.head_branch` to scope per branch.
|
|
35
|
+
|
|
36
|
+
Use `cancel-in-progress: true` for idempotent deploys (only the latest commit
|
|
37
|
+
matters) or `cancel-in-progress: false` for non-idempotent operations that
|
|
38
|
+
must complete once queued.
|
|
39
|
+
fix_code:
|
|
40
|
+
- language: yaml
|
|
41
|
+
label: "Add explicit concurrency group to workflow_run-triggered deploy workflow"
|
|
42
|
+
code: |
|
|
43
|
+
on:
|
|
44
|
+
workflow_run:
|
|
45
|
+
workflows: ["CI"]
|
|
46
|
+
types: [completed]
|
|
47
|
+
branches: [main]
|
|
48
|
+
|
|
49
|
+
# Without this block, concurrent CI completions spawn concurrent deploys
|
|
50
|
+
concurrency:
|
|
51
|
+
group: deploy-${{ github.event.workflow_run.head_branch }}
|
|
52
|
+
cancel-in-progress: true
|
|
53
|
+
|
|
54
|
+
jobs:
|
|
55
|
+
deploy:
|
|
56
|
+
if: github.event.workflow_run.conclusion == 'success'
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
steps:
|
|
59
|
+
- name: Deploy
|
|
60
|
+
run: echo "Deploying ${{ github.event.workflow_run.head_sha }}"
|
|
61
|
+
prevention:
|
|
62
|
+
- "Always define an explicit `concurrency:` block in `workflow_run`-triggered workflows"
|
|
63
|
+
- "Key the concurrency group on `github.event.workflow_run.head_branch` to scope by branch"
|
|
64
|
+
- "For CI-to-deploy pipelines, use `cancel-in-progress: true` so only the latest commit deploys"
|
|
65
|
+
- "Consider consolidating CI and deploy into a single workflow using `needs:` if elevated permissions are not required"
|
|
66
|
+
docs:
|
|
67
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_run"
|
|
68
|
+
label: "GitHub Docs: workflow_run event"
|
|
69
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/controlling-workflow-and-job-execution"
|
|
70
|
+
label: "GitHub Docs: Controlling workflow and job execution (concurrency)"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
id: known-unsolved-053
|
|
2
|
+
title: '`workflow_dispatch` Has No Native Array or List Input Type — Must Serialize as JSON String'
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: limitation
|
|
5
|
+
tags:
|
|
6
|
+
- workflow_dispatch
|
|
7
|
+
- inputs
|
|
8
|
+
- array
|
|
9
|
+
- list
|
|
10
|
+
- json
|
|
11
|
+
- limitation
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'workflow_dispatch.*inputs|inputs.*type\s*:\s*string.*array'
|
|
14
|
+
flags: 'im'
|
|
15
|
+
- regex: "fromJSON\\(.*inputs\\."
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Input type 'array' is not supported for workflow_dispatch"
|
|
19
|
+
- "Unexpected value 'array'"
|
|
20
|
+
root_cause: |
|
|
21
|
+
GitHub Actions `workflow_dispatch` supports only five input types:
|
|
22
|
+
`string`, `boolean`, `choice`, `number`, and `environment`. There is no
|
|
23
|
+
native `array` or `list` type. Users who need to pass multiple values
|
|
24
|
+
(e.g., a list of services to deploy, a set of environments to target)
|
|
25
|
+
must serialize the array as a JSON string and parse it in the workflow.
|
|
26
|
+
|
|
27
|
+
This limitation affects:
|
|
28
|
+
- Manual dispatch with dynamic target lists
|
|
29
|
+
- CI/CD automation via the REST API (`POST /repos/.../actions/workflows/.../dispatches`)
|
|
30
|
+
- Reuse of workflow_dispatch workflows that naturally accept variable-length input
|
|
31
|
+
- Validation — no schema can prevent malformed JSON from being passed
|
|
32
|
+
|
|
33
|
+
A `type: array` schema was proposed in actions/runner#1844 (800+ reactions)
|
|
34
|
+
and remains one of the highest-voted open feature requests for GitHub Actions.
|
|
35
|
+
fix: |
|
|
36
|
+
Serialize the array as a JSON string in the dispatch call and use
|
|
37
|
+
`fromJSON()` in the workflow to parse it into a matrix or loop variable.
|
|
38
|
+
|
|
39
|
+
Workarounds by use case:
|
|
40
|
+
1. **API dispatch**: Serialize to `'["a","b","c"]'` and use `fromJSON(inputs.targets)`
|
|
41
|
+
2. **UI dispatch**: Document the expected JSON format in the input description
|
|
42
|
+
3. **Choice type**: If the list is finite and known ahead of time, use
|
|
43
|
+
`type: choice` with predefined options instead
|
|
44
|
+
4. **Multiple boolean inputs**: For small fixed sets, use separate boolean
|
|
45
|
+
inputs per option (verbose but typed)
|
|
46
|
+
fix_code:
|
|
47
|
+
- language: yaml
|
|
48
|
+
label: "Workaround — serialize array as JSON string, parse with fromJSON"
|
|
49
|
+
code: |
|
|
50
|
+
on:
|
|
51
|
+
workflow_dispatch:
|
|
52
|
+
inputs:
|
|
53
|
+
services:
|
|
54
|
+
description: 'JSON array of services to deploy e.g. ["api","worker","ui"]'
|
|
55
|
+
required: true
|
|
56
|
+
type: string
|
|
57
|
+
default: '["api","worker"]'
|
|
58
|
+
|
|
59
|
+
jobs:
|
|
60
|
+
deploy:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
strategy:
|
|
63
|
+
matrix:
|
|
64
|
+
service: ${{ fromJSON(inputs.services) }}
|
|
65
|
+
steps:
|
|
66
|
+
- run: echo "Deploying ${{ matrix.service }}"
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: "Workaround — comma-separated string split in shell"
|
|
69
|
+
code: |
|
|
70
|
+
on:
|
|
71
|
+
workflow_dispatch:
|
|
72
|
+
inputs:
|
|
73
|
+
environments:
|
|
74
|
+
description: 'Comma-separated environments e.g. staging,production'
|
|
75
|
+
required: true
|
|
76
|
+
type: string
|
|
77
|
+
default: 'staging'
|
|
78
|
+
|
|
79
|
+
jobs:
|
|
80
|
+
deploy:
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
steps:
|
|
83
|
+
- name: Deploy to each environment
|
|
84
|
+
run: |
|
|
85
|
+
IFS=',' read -ra ENVS <<< "${{ inputs.environments }}"
|
|
86
|
+
for env in "${ENVS[@]}"; do
|
|
87
|
+
echo "Deploying to: $env"
|
|
88
|
+
done
|
|
89
|
+
- language: yaml
|
|
90
|
+
label: "Alternative — use type: choice for known finite sets"
|
|
91
|
+
code: |
|
|
92
|
+
on:
|
|
93
|
+
workflow_dispatch:
|
|
94
|
+
inputs:
|
|
95
|
+
target_env:
|
|
96
|
+
description: 'Target environment'
|
|
97
|
+
required: true
|
|
98
|
+
type: choice
|
|
99
|
+
options:
|
|
100
|
+
- staging
|
|
101
|
+
- production
|
|
102
|
+
- both
|
|
103
|
+
prevention:
|
|
104
|
+
- "Document the expected JSON format in the input `description` field so UI dispatchers know the required syntax."
|
|
105
|
+
- "Add a validation step that runs `echo '${{ inputs.targets }}' | jq .` to fail fast on malformed JSON before processing."
|
|
106
|
+
- "Consider using `type: choice` with predefined options if the list is small and known at workflow-definition time."
|
|
107
|
+
- "For API-driven workflows, generate the JSON array programmatically and pass it as a string in the dispatch payload."
|
|
108
|
+
docs:
|
|
109
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onworkflow_dispatchinputsinput_idtype"
|
|
110
|
+
label: "workflow_dispatch input types — supported values"
|
|
111
|
+
- url: "https://github.com/actions/runner/issues/1844"
|
|
112
|
+
label: "actions/runner #1844 — Support array inputs for workflow_dispatch (800+ reactions)"
|
|
113
|
+
- url: "https://docs.github.com/en/rest/actions/workflows#create-a-workflow-dispatch-event"
|
|
114
|
+
label: "REST API — Create a workflow dispatch event"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
id: permissions-auth-053
|
|
2
|
+
title: 'GITHUB_TOKEN `packages: write` Cannot Delete Package Versions — Requires PAT with `delete:packages`'
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- packages
|
|
7
|
+
- github-token
|
|
8
|
+
- delete-packages
|
|
9
|
+
- container-registry
|
|
10
|
+
- package-cleanup
|
|
11
|
+
- permissions
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'Resource not accessible by integration'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'DELETE.*packages.*versions|packages.*DELETE.*versions'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: '403.*package|package.*403'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- "Resource not accessible by integration"
|
|
21
|
+
- "403 Forbidden — DELETE https://api.github.com/user/packages/{type}/{name}/versions/{id}"
|
|
22
|
+
- "Error: Resource not accessible by integration (HTTP 403)"
|
|
23
|
+
root_cause: |
|
|
24
|
+
The `packages: write` permission in `GITHUB_TOKEN` grants the ability to
|
|
25
|
+
publish and overwrite package versions but does NOT include the ability to
|
|
26
|
+
delete them. Package version deletion requires account-level permission
|
|
27
|
+
(`delete:packages` OAuth scope) that cannot be granted to an installation
|
|
28
|
+
token. The GitHub Packages REST API delete endpoint
|
|
29
|
+
(`DELETE /user/packages/{type}/{name}/versions/{id}` or the org equivalent)
|
|
30
|
+
validates the caller's account-level scope, which GITHUB_TOKEN—being a
|
|
31
|
+
repository-scoped installation token—can never satisfy regardless of the
|
|
32
|
+
`permissions: packages: write` declaration in the workflow.
|
|
33
|
+
|
|
34
|
+
This commonly surfaces in:
|
|
35
|
+
- Container image cleanup jobs that try to prune old tags/digests
|
|
36
|
+
- npm, Maven, or RubyGems package version retention workflows
|
|
37
|
+
- Automated housekeeping that uses `ghcr.io` image management actions
|
|
38
|
+
fix: |
|
|
39
|
+
Store a fine-grained or classic PAT with `delete:packages` scope as a
|
|
40
|
+
repository secret (e.g., `PACKAGES_DELETE_TOKEN`) and use that token when
|
|
41
|
+
calling the Packages delete API or actions that delete package versions.
|
|
42
|
+
|
|
43
|
+
For GitHub Container Registry (ghcr.io) image pruning, use a PAT or a
|
|
44
|
+
GitHub App installation token with Packages write + admin scope. Pass it
|
|
45
|
+
as the `token:` input to the relevant cleanup action.
|
|
46
|
+
fix_code:
|
|
47
|
+
- language: yaml
|
|
48
|
+
label: "WRONG — GITHUB_TOKEN cannot delete package versions (403)"
|
|
49
|
+
code: |
|
|
50
|
+
jobs:
|
|
51
|
+
cleanup:
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
permissions:
|
|
54
|
+
packages: write # ❌ write permission does not grant delete
|
|
55
|
+
steps:
|
|
56
|
+
- name: Delete old package version
|
|
57
|
+
run: |
|
|
58
|
+
# This call will return 403 "Resource not accessible by integration"
|
|
59
|
+
VERSION_ID=12345
|
|
60
|
+
curl -X DELETE \
|
|
61
|
+
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
|
62
|
+
https://api.github.com/user/packages/container/my-image/versions/$VERSION_ID
|
|
63
|
+
- language: yaml
|
|
64
|
+
label: "RIGHT — use a PAT with delete:packages scope"
|
|
65
|
+
code: |
|
|
66
|
+
jobs:
|
|
67
|
+
cleanup:
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
steps:
|
|
70
|
+
- name: Delete old package version
|
|
71
|
+
env:
|
|
72
|
+
GH_TOKEN: ${{ secrets.PACKAGES_DELETE_TOKEN }} # PAT with delete:packages
|
|
73
|
+
run: |
|
|
74
|
+
VERSION_ID=12345
|
|
75
|
+
curl -X DELETE \
|
|
76
|
+
-H "Authorization: Bearer $GH_TOKEN" \
|
|
77
|
+
https://api.github.com/user/packages/container/my-image/versions/$VERSION_ID
|
|
78
|
+
- language: yaml
|
|
79
|
+
label: "RIGHT — ghcr.io cleanup action with PAT"
|
|
80
|
+
code: |
|
|
81
|
+
jobs:
|
|
82
|
+
cleanup-images:
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
steps:
|
|
85
|
+
- uses: snok/container-retention-policy@v3
|
|
86
|
+
with:
|
|
87
|
+
account: user
|
|
88
|
+
token: ${{ secrets.PACKAGES_DELETE_TOKEN }} # PAT with delete:packages
|
|
89
|
+
image-names: my-image
|
|
90
|
+
cut-off: 7 days ago UTC
|
|
91
|
+
keep-n-most-recent: 5
|
|
92
|
+
prevention:
|
|
93
|
+
- "Never assume `packages: write` is sufficient for full package lifecycle management — deletion is always PAT-only."
|
|
94
|
+
- "Create a dedicated PAT or GitHub App with `delete:packages` scope and store it as a secret for cleanup workflows."
|
|
95
|
+
- "Scope cleanup jobs separately from build jobs so the PAT is only used where deletion is needed."
|
|
96
|
+
- "Document in your workflow that PACKAGES_DELETE_TOKEN requires `delete:packages` scope so it is renewed with the right scopes."
|
|
97
|
+
docs:
|
|
98
|
+
- url: "https://docs.github.com/en/rest/packages/packages#delete-a-package-version-for-a-user"
|
|
99
|
+
label: "REST API — Delete a package version for a user"
|
|
100
|
+
- url: "https://docs.github.com/en/packages/learn-github-packages/about-permissions-for-github-packages#about-scopes-and-permissions-for-package-registries"
|
|
101
|
+
label: "About permissions for GitHub Packages"
|
|
102
|
+
- url: "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token"
|
|
103
|
+
label: "Creating a fine-grained PAT with delete:packages"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
id: permissions-auth-054
|
|
2
|
+
title: "OIDC sub claim format changes when environment: block added — IdP trust policy breaks"
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- oidc
|
|
7
|
+
- environment
|
|
8
|
+
- aws
|
|
9
|
+
- gcp
|
|
10
|
+
- azure
|
|
11
|
+
- sub-claim
|
|
12
|
+
- trust-policy
|
|
13
|
+
- deployment-protection
|
|
14
|
+
patterns:
|
|
15
|
+
- regex: 'Not authorized to perform sts:AssumeRoleWithWebIdentity'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'Error: Credentials could not be loaded'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
- regex: 'Permission.*denied.*generateAccessToken|WorkloadIdentityPool.*rejected'
|
|
20
|
+
flags: 'i'
|
|
21
|
+
error_messages:
|
|
22
|
+
- "Not authorized to perform sts:AssumeRoleWithWebIdentity"
|
|
23
|
+
- "Error: Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers"
|
|
24
|
+
- "Permission 'iam.serviceAccounts.getOpenIdToken' denied on resource"
|
|
25
|
+
- "The provided token could not be validated"
|
|
26
|
+
root_cause: |
|
|
27
|
+
GitHub Actions OIDC token `sub` (subject) claim format depends on whether the
|
|
28
|
+
job has an `environment:` key. Without an environment, the subject is:
|
|
29
|
+
`repo:OWNER/REPO:ref:refs/heads/BRANCH`
|
|
30
|
+
|
|
31
|
+
When `environment: production` is present on the job, the format changes to:
|
|
32
|
+
`repo:OWNER/REPO:environment:production`
|
|
33
|
+
|
|
34
|
+
The branch/ref component is replaced entirely by the environment name. AWS IAM
|
|
35
|
+
role trust policies, GCP Workload Identity Federation conditions, and Azure
|
|
36
|
+
federated credential filters that matched the branch-ref format now receive a
|
|
37
|
+
token with a different sub claim and reject the OIDC exchange with a 403 or
|
|
38
|
+
permission-denied error.
|
|
39
|
+
|
|
40
|
+
This commonly occurs when a team adds environment protection rules (required
|
|
41
|
+
reviewers, wait timers) to an existing workflow that already had OIDC
|
|
42
|
+
credentials working. CI passes before adding `environment:` but fails after.
|
|
43
|
+
fix: |
|
|
44
|
+
Update the IdP trust policy to match the new subject format containing the
|
|
45
|
+
environment name. Options:
|
|
46
|
+
1. Narrow to environment: change the condition to match
|
|
47
|
+
`repo:OWNER/REPO:environment:production`.
|
|
48
|
+
2. Use a wildcard: match `repo:OWNER/REPO:*` to accept both formats (less secure).
|
|
49
|
+
3. GitHub subject claim customization (Enterprise): define a consistent sub
|
|
50
|
+
claim format that does not change based on environment presence.
|
|
51
|
+
fix_code:
|
|
52
|
+
- language: yaml
|
|
53
|
+
label: "Workflow: annotate environment and required OIDC permissions"
|
|
54
|
+
code: |
|
|
55
|
+
permissions:
|
|
56
|
+
id-token: write
|
|
57
|
+
contents: read
|
|
58
|
+
|
|
59
|
+
jobs:
|
|
60
|
+
deploy:
|
|
61
|
+
# Adding this key changes the OIDC sub claim format — update IdP trust policy
|
|
62
|
+
environment: production
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
steps:
|
|
65
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
66
|
+
with:
|
|
67
|
+
role-to-assume: arn:aws:iam::123456789012:role/DeployRole
|
|
68
|
+
aws-region: us-east-1
|
|
69
|
+
# AWS trust policy must now use:
|
|
70
|
+
# "token.actions.githubusercontent.com:sub":
|
|
71
|
+
# "StringEquals": "repo:org/repo:environment:production"
|
|
72
|
+
# (not "repo:org/repo:ref:refs/heads/main")
|
|
73
|
+
- language: yaml
|
|
74
|
+
label: "AWS IAM trust policy: match environment sub claim"
|
|
75
|
+
code: |
|
|
76
|
+
# AWS IAM Role Trust Policy — update Condition after adding environment:
|
|
77
|
+
# Before (no environment):
|
|
78
|
+
# "token.actions.githubusercontent.com:sub": "repo:org/repo:ref:refs/heads/main"
|
|
79
|
+
#
|
|
80
|
+
# After (with environment: production):
|
|
81
|
+
# "token.actions.githubusercontent.com:sub": "repo:org/repo:environment:production"
|
|
82
|
+
#
|
|
83
|
+
# Wildcard to accept both (less restrictive):
|
|
84
|
+
# "token.actions.githubusercontent.com:sub":
|
|
85
|
+
# StringLike: "repo:org/repo:*"
|
|
86
|
+
#
|
|
87
|
+
# GCP: update attribute.repository_environment or use attribute mapping
|
|
88
|
+
prevention:
|
|
89
|
+
- "Before adding `environment:` to a job using OIDC, audit and update all IdP trust policies"
|
|
90
|
+
- "Document the expected sub claim format in trust policy comments to avoid future confusion"
|
|
91
|
+
- "Use GitHub's OIDC token debugger step to inspect the actual sub claim at runtime"
|
|
92
|
+
- "For consistent sub format across environment and non-environment jobs, consider subject claim customization"
|
|
93
|
+
docs:
|
|
94
|
+
- url: "https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect#filtering-for-a-specific-environment"
|
|
95
|
+
label: "GitHub Docs: OIDC filtering for a specific environment"
|
|
96
|
+
- url: "https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-subject-claims-for-an-organization-or-repository"
|
|
97
|
+
label: "GitHub Docs: Customizing subject claims"
|
|
98
|
+
- url: "https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services"
|
|
99
|
+
label: "GitHub Docs: Configuring OIDC in AWS"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
id: silent-failures-087
|
|
2
|
+
title: '`fail-fast: false` Does Not Prevent Downstream `needs:` Jobs From Being Skipped When Matrix Jobs Fail'
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- matrix
|
|
7
|
+
- fail-fast
|
|
8
|
+
- needs
|
|
9
|
+
- downstream-job
|
|
10
|
+
- skipped
|
|
11
|
+
- strategy
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'fail-fast\s*:\s*false'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'needs\s*:.*matrix|matrix.*needs\s*:'
|
|
16
|
+
flags: 'im'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "This job was skipped"
|
|
19
|
+
- "Job 'X' is skipped because all dependencies failed or were skipped"
|
|
20
|
+
root_cause: |
|
|
21
|
+
`strategy.fail-fast: false` only prevents other MATRIX jobs from being
|
|
22
|
+
cancelled when one matrix job fails. It does NOT affect how downstream
|
|
23
|
+
jobs that `needs:` the matrix job handle the overall result.
|
|
24
|
+
|
|
25
|
+
When at least one matrix job fails (even with `fail-fast: false` allowing
|
|
26
|
+
all others to complete), the `needs.<matrix-job>.result` for the downstream
|
|
27
|
+
job evaluates to `'failure'`. A downstream job with a standard `needs:`
|
|
28
|
+
dependency is then SKIPPED — not because of `fail-fast`, but because its
|
|
29
|
+
dependency is in a failed state.
|
|
30
|
+
|
|
31
|
+
This surprises developers who add `fail-fast: false` expecting the entire
|
|
32
|
+
pipeline to continue running end-to-end. The final summary/report job
|
|
33
|
+
that needs the matrix job is silently skipped, producing no output.
|
|
34
|
+
fix: |
|
|
35
|
+
Add `if: always()` to downstream jobs that should run regardless of matrix
|
|
36
|
+
outcome. Then explicitly check `needs.<job>.result` values to determine
|
|
37
|
+
pass/fail:
|
|
38
|
+
|
|
39
|
+
- `if: always()` ensures the job is never skipped due to upstream failure
|
|
40
|
+
- Check `contains(needs.*.result, 'failure')` to detect any matrix failure
|
|
41
|
+
- Use `needs.<job>.result == 'success'` for strict pass gates
|
|
42
|
+
fix_code:
|
|
43
|
+
- language: yaml
|
|
44
|
+
label: "WRONG — summary job silently skipped when matrix job fails"
|
|
45
|
+
code: |
|
|
46
|
+
jobs:
|
|
47
|
+
test:
|
|
48
|
+
strategy:
|
|
49
|
+
fail-fast: false # ❌ allows all matrix jobs to run, but...
|
|
50
|
+
matrix:
|
|
51
|
+
node: [18, 20, 22]
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
steps:
|
|
54
|
+
- run: npm test
|
|
55
|
+
|
|
56
|
+
summary:
|
|
57
|
+
needs: test # ❌ still skipped if any matrix job failed
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- run: echo "All tests done"
|
|
61
|
+
- language: yaml
|
|
62
|
+
label: "RIGHT — use if: always() and check result explicitly"
|
|
63
|
+
code: |
|
|
64
|
+
jobs:
|
|
65
|
+
test:
|
|
66
|
+
strategy:
|
|
67
|
+
fail-fast: false
|
|
68
|
+
matrix:
|
|
69
|
+
node: [18, 20, 22]
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
steps:
|
|
72
|
+
- run: npm test
|
|
73
|
+
|
|
74
|
+
summary:
|
|
75
|
+
needs: test
|
|
76
|
+
if: always() # ✅ runs even when test jobs failed
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
steps:
|
|
79
|
+
- name: Check matrix results
|
|
80
|
+
if: contains(needs.test.result, 'failure')
|
|
81
|
+
run: |
|
|
82
|
+
echo "One or more matrix jobs failed"
|
|
83
|
+
exit 1
|
|
84
|
+
- name: Success
|
|
85
|
+
run: echo "All matrix jobs passed"
|
|
86
|
+
- language: yaml
|
|
87
|
+
label: "RIGHT — gate final deploy only when all matrix jobs pass"
|
|
88
|
+
code: |
|
|
89
|
+
jobs:
|
|
90
|
+
test:
|
|
91
|
+
strategy:
|
|
92
|
+
fail-fast: false
|
|
93
|
+
matrix:
|
|
94
|
+
node: [18, 20, 22]
|
|
95
|
+
runs-on: ubuntu-latest
|
|
96
|
+
outputs:
|
|
97
|
+
result: ${{ job.status }}
|
|
98
|
+
steps:
|
|
99
|
+
- run: npm test
|
|
100
|
+
|
|
101
|
+
deploy:
|
|
102
|
+
needs: test
|
|
103
|
+
if: always() && needs.test.result == 'success' # ✅ explicit success check
|
|
104
|
+
runs-on: ubuntu-latest
|
|
105
|
+
steps:
|
|
106
|
+
- run: echo "Deploying"
|
|
107
|
+
prevention:
|
|
108
|
+
- "Always add `if: always()` to aggregation/summary/deploy jobs that need to run after a matrix job."
|
|
109
|
+
- "Explicitly check `needs.<job>.result` or `contains(needs.*.result, 'failure')` rather than relying on implicit pass-through."
|
|
110
|
+
- "Understand that `fail-fast: false` is matrix-scoped — it does not change how the `needs:` graph handles failures."
|
|
111
|
+
- "Use job outputs combined with `if: always()` to build robust reporting jobs that capture all matrix outcomes."
|
|
112
|
+
docs:
|
|
113
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-jobs-in-a-workflow#defining-prerequisite-jobs"
|
|
114
|
+
label: "Defining prerequisite jobs — needs context and result values"
|
|
115
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategyfail-fast"
|
|
116
|
+
label: "workflow syntax — jobs.<job_id>.strategy.fail-fast"
|
|
117
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#status-check-functions"
|
|
118
|
+
label: "Status check functions — always(), success(), failure()"
|
|
119
|
+
- url: "https://github.com/orgs/community/discussions/26822"
|
|
120
|
+
label: "GitHub Community — fail-fast: false but summary job still skipped"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
id: yaml-syntax-059
|
|
2
|
+
title: "needs: key does not accept runtime expressions — must be static job IDs"
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- needs
|
|
7
|
+
- expressions
|
|
8
|
+
- dynamic-jobs
|
|
9
|
+
- job-dependencies
|
|
10
|
+
- parse-time
|
|
11
|
+
- actionlint
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "The pipeline is not valid.*needs|needs.*references.*job.*does not exist"
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'Unrecognized named-value.*steps.*in needs|needs.*invalid.*expression'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "The pipeline is not valid. Job 'deploy' needs 'steps' which does not exist"
|
|
19
|
+
- "Unrecognized named-value: 'steps'"
|
|
20
|
+
- "Job 'X' depends on unknown job '${{ fromJSON(...) }}'"
|
|
21
|
+
- "needs: field value '${{ ... }}' is not a valid job identifier"
|
|
22
|
+
root_cause: |
|
|
23
|
+
The `needs:` key in a GitHub Actions job definition must contain static job ID
|
|
24
|
+
strings. It is evaluated at workflow parse time — before any step or job runs
|
|
25
|
+
— so runtime expressions such as `${{ steps.gen.outputs.jobs }}` or
|
|
26
|
+
`${{ fromJSON(env.DYNAMIC_JOBS) }}` are not evaluated. They are treated as
|
|
27
|
+
literal strings, and since no job with that literal name exists, GitHub reports
|
|
28
|
+
a validation error or the dependency is silently ignored.
|
|
29
|
+
|
|
30
|
+
This surprises developers who successfully use `fromJSON()` in `matrix:` or
|
|
31
|
+
`env:` blocks (which ARE expression-capable) and assume `needs:` works the
|
|
32
|
+
same way. The difference is that the job dependency graph must be fully
|
|
33
|
+
resolved before execution begins; expressions in `needs:` would create a
|
|
34
|
+
circular dependency at parse time.
|
|
35
|
+
|
|
36
|
+
actionlint statically detects this and reports: "needs: field cannot be
|
|
37
|
+
computed by a dynamic value. Use a literal value instead."
|
|
38
|
+
fix: |
|
|
39
|
+
Job dependencies must be statically defined. Use one of these patterns:
|
|
40
|
+
1. List all potential dependent jobs statically; use `if:` conditions on
|
|
41
|
+
each downstream job to skip those not needed.
|
|
42
|
+
2. Use a matrix fan-out + fan-in pattern where the fan-in job has a single
|
|
43
|
+
static `needs:` on the matrix job name (not individual matrix legs).
|
|
44
|
+
3. Restructure so the dependency graph is known at workflow authoring time.
|
|
45
|
+
fix_code:
|
|
46
|
+
- language: yaml
|
|
47
|
+
label: "Correct: static needs: with if: conditions to control execution"
|
|
48
|
+
code: |
|
|
49
|
+
jobs:
|
|
50
|
+
build:
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
outputs:
|
|
53
|
+
should_deploy: ${{ steps.check.outputs.deploy }}
|
|
54
|
+
steps:
|
|
55
|
+
- id: check
|
|
56
|
+
run: echo "deploy=true" >> $GITHUB_OUTPUT
|
|
57
|
+
|
|
58
|
+
deploy:
|
|
59
|
+
# Static needs: always declared; if: controls whether it runs
|
|
60
|
+
needs: [build]
|
|
61
|
+
if: needs.build.outputs.should_deploy == 'true'
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
steps:
|
|
64
|
+
- run: echo "deploying"
|
|
65
|
+
- language: yaml
|
|
66
|
+
label: "Incorrect: expression in needs: causes parse-time validation error"
|
|
67
|
+
code: |
|
|
68
|
+
jobs:
|
|
69
|
+
generate:
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
outputs:
|
|
72
|
+
jobs: ${{ steps.list.outputs.jobs }}
|
|
73
|
+
steps:
|
|
74
|
+
- id: list
|
|
75
|
+
run: echo 'jobs=["build","lint"]' >> $GITHUB_OUTPUT
|
|
76
|
+
|
|
77
|
+
# ERROR: needs: does not evaluate expressions — this is a parse-time failure
|
|
78
|
+
deploy:
|
|
79
|
+
needs: ${{ fromJSON(needs.generate.outputs.jobs) }}
|
|
80
|
+
runs-on: ubuntu-latest
|
|
81
|
+
steps:
|
|
82
|
+
- run: echo "this never runs"
|
|
83
|
+
prevention:
|
|
84
|
+
- "Always use static job ID string literals in `needs:` — no expressions, no fromJSON()"
|
|
85
|
+
- "Use actionlint to pre-validate workflows before pushing: it catches invalid expression contexts"
|
|
86
|
+
- "For dynamic fan-in, depend on the matrix job name itself, not individual matrix leg names"
|
|
87
|
+
- "Use `if: contains(needs.*.result, 'failure')` for conditional logic after static needs"
|
|
88
|
+
docs:
|
|
89
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds"
|
|
90
|
+
label: "GitHub Docs: jobs.<job_id>.needs"
|
|
91
|
+
- url: "https://rhysd.github.io/actionlint/"
|
|
92
|
+
label: "actionlint: Static checker for GitHub Actions workflow files"
|
|
93
|
+
|
package/package.json
CHANGED