@htekdev/actions-debugger 1.0.52 → 1.0.54
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-037.yml +95 -0
- package/errors/concurrency-timing/concurrency-timing-030.yml +102 -0
- package/errors/concurrency-timing/concurrency-timing-031.yml +102 -0
- package/errors/concurrency-timing/concurrency-timing-032.yml +147 -0
- package/errors/permissions-auth/permissions-auth-039.yml +108 -0
- package/errors/runner-environment/runner-environment-111.yml +93 -0
- package/errors/silent-failures/silent-failures-054.yml +100 -0
- package/errors/yaml-syntax/yaml-syntax-037.yml +105 -0
- package/package.json +1 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
id: caching-artifacts-037
|
|
2
|
+
title: "upload-artifact if-no-files-found defaults to 'warn' — empty upload succeeds, download job fails"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- upload-artifact
|
|
7
|
+
- if-no-files-found
|
|
8
|
+
- artifacts
|
|
9
|
+
- silent
|
|
10
|
+
- warn
|
|
11
|
+
- download
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'No files were found with the provided path'
|
|
14
|
+
flags: i
|
|
15
|
+
- regex: 'No artifact uploads were performed'
|
|
16
|
+
flags: i
|
|
17
|
+
- regex: 'was not found for the associated workflow run'
|
|
18
|
+
flags: i
|
|
19
|
+
error_messages:
|
|
20
|
+
- "No files were found with the provided path: ./dist. No artifacts will be uploaded."
|
|
21
|
+
- "Warning: No files were found with the provided path: build/"
|
|
22
|
+
- "No artifact uploads were performed."
|
|
23
|
+
- "Error: An artifact named build-output was not found for the associated workflow run."
|
|
24
|
+
root_cause: |
|
|
25
|
+
The if-no-files-found input of actions/upload-artifact defaults to warn, not error.
|
|
26
|
+
When the upload path: glob matches no files — because the build directory is missing,
|
|
27
|
+
a glob pattern is wrong, the working-directory setting differs from the upload path,
|
|
28
|
+
or a preceding build step failed silently — the upload step logs a warning message and
|
|
29
|
+
exits with code 0 (success).
|
|
30
|
+
|
|
31
|
+
The calling workflow sees a green upload step in the UI. The problem only surfaces in
|
|
32
|
+
the downstream job that calls actions/download-artifact, which fails with an error like
|
|
33
|
+
"An artifact named X was not found for the associated workflow run."
|
|
34
|
+
|
|
35
|
+
Developers spend time debugging the download step or the job that uses the artifact when
|
|
36
|
+
the real problem — the build not producing output — occurred earlier, often in a different
|
|
37
|
+
job. The default warn behavior exists for optional artifacts, but it is a frequent source
|
|
38
|
+
of confusion for required CI artifacts.
|
|
39
|
+
fix: |
|
|
40
|
+
Set if-no-files-found: error on every upload step where the artifact is required.
|
|
41
|
+
The upload step will immediately fail with a descriptive error message that names the
|
|
42
|
+
missing path, pointing directly to the correct job.
|
|
43
|
+
|
|
44
|
+
Reserve warn or ignore only for genuinely optional artifacts — for example, test
|
|
45
|
+
screenshots that only exist when tests fail, or coverage reports that may be skipped
|
|
46
|
+
in some build configurations.
|
|
47
|
+
fix_code:
|
|
48
|
+
- language: yaml
|
|
49
|
+
label: "Correct: fail immediately when required build output is missing"
|
|
50
|
+
code: |
|
|
51
|
+
- name: Upload build artifacts
|
|
52
|
+
uses: actions/upload-artifact@v4
|
|
53
|
+
with:
|
|
54
|
+
name: build-output
|
|
55
|
+
path: ./dist/
|
|
56
|
+
if-no-files-found: error # Fail here — not in the downstream download job
|
|
57
|
+
- language: yaml
|
|
58
|
+
label: "Optional artifact — keep warn or ignore"
|
|
59
|
+
code: |
|
|
60
|
+
- name: Upload test screenshots (optional — only exist on test failure)
|
|
61
|
+
if: failure()
|
|
62
|
+
uses: actions/upload-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: test-screenshots
|
|
65
|
+
path: ./test-results/screenshots/
|
|
66
|
+
if-no-files-found: ignore # OK — screenshots only exist when tests fail
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: "Verify build output before uploading"
|
|
69
|
+
code: |
|
|
70
|
+
- name: Verify dist/ was built
|
|
71
|
+
run: |
|
|
72
|
+
if [ ! -d "./dist" ] || [ -z "$(ls -A ./dist)" ]; then
|
|
73
|
+
echo "ERROR: dist/ directory is empty or missing"
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
- name: Upload build artifacts
|
|
78
|
+
uses: actions/upload-artifact@v4
|
|
79
|
+
with:
|
|
80
|
+
name: build-output
|
|
81
|
+
path: ./dist/
|
|
82
|
+
if-no-files-found: error
|
|
83
|
+
prevention:
|
|
84
|
+
- "Default to if-no-files-found: error in all CI pipeline templates for required artifacts"
|
|
85
|
+
- "Note that upload path: is relative to GITHUB_WORKSPACE, not the step's working-directory setting"
|
|
86
|
+
- "Add an explicit build verification step before upload to fail fast with a clear message"
|
|
87
|
+
- "Audit existing workflows: any upload-artifact step without if-no-files-found: error is a silent failure risk"
|
|
88
|
+
- "When a build step uses continue-on-error: true, verify it did not silently skip output generation before uploading"
|
|
89
|
+
docs:
|
|
90
|
+
- url: "https://github.com/actions/upload-artifact#inputs"
|
|
91
|
+
label: "actions/upload-artifact: Input parameters reference"
|
|
92
|
+
- url: "https://github.com/actions/upload-artifact/blob/main/RELEASES.md"
|
|
93
|
+
label: "actions/upload-artifact: Release notes"
|
|
94
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/storing-and-sharing-data-from-a-workflow"
|
|
95
|
+
label: "GitHub Docs: Storing and sharing data from a workflow"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
id: concurrency-timing-030
|
|
2
|
+
title: 'github.sha concurrency group key makes every run unique — cancel-in-progress never fires'
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- cancel-in-progress
|
|
8
|
+
- github-sha
|
|
9
|
+
- unique-key
|
|
10
|
+
- workflow-cancellation
|
|
11
|
+
- silent-failure
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'group:\s*.*\$\{\{[^}]*github\.sha[^}]*\}\}'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'cancel-in-progress:\s*true'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- 'Old workflow runs are not being cancelled despite cancel-in-progress: true'
|
|
19
|
+
- 'All workflow runs queue independently instead of cancelling previous runs'
|
|
20
|
+
- 'Concurrent workflows are running in parallel when they should be cancelled'
|
|
21
|
+
root_cause: |
|
|
22
|
+
github.sha is unique per commit. When used as (any part of) the concurrency group
|
|
23
|
+
key, every workflow run gets a different group name. Because no two runs ever share
|
|
24
|
+
the same group, the cancel-in-progress mechanism has nothing to act on — all runs
|
|
25
|
+
proceed independently and simultaneously.
|
|
26
|
+
|
|
27
|
+
This is the inverse of the intended behavior. Developers add ${{ github.sha }}
|
|
28
|
+
thinking "this will identify runs for the same commit and cancel old ones," but
|
|
29
|
+
since each commit produces a unique SHA, each run is placed in its own group with
|
|
30
|
+
no overlap. The result is effectively no concurrency control at all.
|
|
31
|
+
|
|
32
|
+
Common mistake patterns:
|
|
33
|
+
group: ${{ github.workflow }}-${{ github.sha }} # unique per commit — never cancels
|
|
34
|
+
group: ${{ github.ref }}-${{ github.sha }} # unique per commit — never cancels
|
|
35
|
+
group: deploy-${{ github.sha }} # unique per commit — never cancels
|
|
36
|
+
|
|
37
|
+
The correct key for "cancel older runs on the same branch" is a value that stays
|
|
38
|
+
constant across pushes to the same branch — github.ref, github.head_ref (for PRs),
|
|
39
|
+
or a composite like github.workflow-github.ref.
|
|
40
|
+
|
|
41
|
+
Source: Frequently reported on Stack Overflow [github-actions] tag and GitHub
|
|
42
|
+
Community discussions. GitHub Docs concurrency examples explicitly use github.ref,
|
|
43
|
+
not github.sha.
|
|
44
|
+
fix: |
|
|
45
|
+
Replace github.sha with github.ref (or github.head_ref for pull request workflows)
|
|
46
|
+
in the concurrency group key. Use a composite that stays constant for all pushes
|
|
47
|
+
to the same branch:
|
|
48
|
+
|
|
49
|
+
- For branch workflows: ${{ github.workflow }}-${{ github.ref }}
|
|
50
|
+
- For PR workflows: ${{ github.workflow }}-${{ github.head_ref }}
|
|
51
|
+
- For global uniqueness: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
|
52
|
+
|
|
53
|
+
Only use github.sha in the group key when you explicitly want every run to be
|
|
54
|
+
isolated (e.g., no cancellation ever — but then cancel-in-progress: true is meaningless).
|
|
55
|
+
fix_code:
|
|
56
|
+
- language: yaml
|
|
57
|
+
label: 'Wrong: github.sha makes every run unique — cancel-in-progress never fires'
|
|
58
|
+
code: |
|
|
59
|
+
# WRONG: every push creates a new unique group — nothing is ever cancelled
|
|
60
|
+
concurrency:
|
|
61
|
+
group: ${{ github.workflow }}-${{ github.sha }}
|
|
62
|
+
cancel-in-progress: true # dead code — no prior run shares this group
|
|
63
|
+
|
|
64
|
+
jobs:
|
|
65
|
+
build:
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
|
|
70
|
+
- language: yaml
|
|
71
|
+
label: 'Correct: github.ref stays constant per branch — prior runs are cancelled'
|
|
72
|
+
code: |
|
|
73
|
+
# CORRECT: all pushes to the same branch share one group
|
|
74
|
+
# cancel-in-progress: true cancels any in-flight run when a new push arrives
|
|
75
|
+
concurrency:
|
|
76
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
77
|
+
cancel-in-progress: true
|
|
78
|
+
|
|
79
|
+
jobs:
|
|
80
|
+
build:
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
steps:
|
|
83
|
+
- uses: actions/checkout@v4
|
|
84
|
+
|
|
85
|
+
- language: yaml
|
|
86
|
+
label: 'PR-specific: use github.head_ref to scope to the PR branch'
|
|
87
|
+
code: |
|
|
88
|
+
# For pull_request workflows, head_ref is the PR branch name (stable per PR)
|
|
89
|
+
concurrency:
|
|
90
|
+
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
|
91
|
+
cancel-in-progress: true
|
|
92
|
+
prevention:
|
|
93
|
+
- 'Never use github.sha in a concurrency group key when the goal is to cancel old runs — it makes every run unique'
|
|
94
|
+
- 'For branch-scoped cancellation use github.ref; for PR-scoped cancellation use github.head_ref'
|
|
95
|
+
- 'If cancel-in-progress: true is set, verify the group key stays constant across multiple pushes to the same branch'
|
|
96
|
+
- 'Use actionlint to catch unreachable concurrency group patterns'
|
|
97
|
+
- 'Read the GitHub Docs concurrency examples — they all use github.ref, not github.sha'
|
|
98
|
+
docs:
|
|
99
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs'
|
|
100
|
+
label: 'GitHub Docs: Controlling concurrency of workflows and jobs'
|
|
101
|
+
- url: 'https://stackoverflow.com/questions/66335225/how-to-cancel-previous-runs-in-the-pr-when-you-push-new-commitschanges'
|
|
102
|
+
label: 'Stack Overflow: How to cancel previous runs when you push new commits'
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
id: concurrency-timing-031
|
|
2
|
+
title: 'cancel-in-progress: false does not create a FIFO queue — only one run can be pending'
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: warning
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- cancel-in-progress
|
|
8
|
+
- queue
|
|
9
|
+
- pending-run
|
|
10
|
+
- fifo
|
|
11
|
+
- workflow-queuing
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'cancel-in-progress:\s*false'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
error_messages:
|
|
16
|
+
- 'Queued workflow run was cancelled unexpectedly even though cancel-in-progress is false'
|
|
17
|
+
- 'Expected runs to queue up but intermediate runs are being dropped'
|
|
18
|
+
- 'Second queued run cancelled when a third run was triggered'
|
|
19
|
+
root_cause: |
|
|
20
|
+
When cancel-in-progress: false is set, many developers expect GitHub Actions to
|
|
21
|
+
maintain a FIFO queue of pending runs: Run 1 active → Run 2 queued → Run 3 waits
|
|
22
|
+
behind Run 2. This is not how it works.
|
|
23
|
+
|
|
24
|
+
GitHub Actions maintains at most ONE pending (queued) run per concurrency group.
|
|
25
|
+
The behavior with cancel-in-progress: false is:
|
|
26
|
+
|
|
27
|
+
1. Run 1: active (running)
|
|
28
|
+
2. Run 2 arrives: Run 2 enters pending state
|
|
29
|
+
3. Run 3 arrives: Run 2 is CANCELLED, Run 3 becomes the new pending run
|
|
30
|
+
4. Run 1 completes: Run 3 (latest) starts — Run 2 was silently dropped
|
|
31
|
+
|
|
32
|
+
This means: with three or more rapid commits, only the first (currently running)
|
|
33
|
+
and the last (most recently pushed) will ever execute. All intermediate runs are
|
|
34
|
+
cancelled and lost — even though cancel-in-progress is explicitly false.
|
|
35
|
+
|
|
36
|
+
This behavior is documented in GitHub Docs ("only the latest queued run will
|
|
37
|
+
start") but is widely misunderstood. The cancel-in-progress: false setting only
|
|
38
|
+
prevents cancelling the ACTIVE run — it does not prevent intermediate pending
|
|
39
|
+
runs from being replaced by newer ones.
|
|
40
|
+
|
|
41
|
+
Source: GitHub Docs concurrency documentation. Discussed in GitHub Community
|
|
42
|
+
discussions and multiple Stack Overflow questions about "runs being unexpectedly
|
|
43
|
+
cancelled with cancel-in-progress: false".
|
|
44
|
+
fix: |
|
|
45
|
+
There is no built-in FIFO queue in GitHub Actions concurrency. To process every
|
|
46
|
+
run without dropping intermediates:
|
|
47
|
+
|
|
48
|
+
1. For pull requests: use GitHub's merge queue — it processes PRs sequentially
|
|
49
|
+
2. For deployments: use a separate queuing action (e.g., softprops/turnstyle) to
|
|
50
|
+
serialize runs without skipping intermediates
|
|
51
|
+
3. For simple "don't cancel active, run latest after": accept that cancel-in-progress:
|
|
52
|
+
false means intermediates are dropped and design accordingly
|
|
53
|
+
4. If every commit must be processed: remove the concurrency block entirely and
|
|
54
|
+
accept parallel runs, or implement idempotent jobs that can run concurrently
|
|
55
|
+
fix_code:
|
|
56
|
+
- language: yaml
|
|
57
|
+
label: 'Misunderstood: cancel-in-progress: false does NOT queue all runs'
|
|
58
|
+
code: |
|
|
59
|
+
# MISUNDERSTOOD: developers expect runs 1, 2, 3 to all execute sequentially
|
|
60
|
+
# ACTUAL behavior: run 2 is cancelled when run 3 arrives
|
|
61
|
+
concurrency:
|
|
62
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
63
|
+
cancel-in-progress: false # protects ACTIVE run only; pending run is replaced
|
|
64
|
+
|
|
65
|
+
jobs:
|
|
66
|
+
deploy:
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v4
|
|
70
|
+
- run: echo "Deploying..."
|
|
71
|
+
|
|
72
|
+
- language: yaml
|
|
73
|
+
label: 'Explicit: accept one-pending semantics and design for idempotent jobs'
|
|
74
|
+
code: |
|
|
75
|
+
# If intermediate commits can be skipped (e.g., deploy only latest):
|
|
76
|
+
# cancel-in-progress: false ensures the active deploy finishes, then
|
|
77
|
+
# only the LATEST pending commit deploys next (intermediates dropped)
|
|
78
|
+
concurrency:
|
|
79
|
+
group: deploy-${{ github.ref }}
|
|
80
|
+
cancel-in-progress: false
|
|
81
|
+
|
|
82
|
+
jobs:
|
|
83
|
+
deploy:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
- name: Deploy latest commit
|
|
88
|
+
run: |
|
|
89
|
+
echo "Deploying ${{ github.sha }}"
|
|
90
|
+
# Idempotent: deploying latest SHA is always safe
|
|
91
|
+
prevention:
|
|
92
|
+
- 'Understand that cancel-in-progress: false only protects the active run — the pending slot still holds at most one run'
|
|
93
|
+
- 'Do not rely on concurrency groups for FIFO queuing — they are a deduplication mechanism, not a queue'
|
|
94
|
+
- 'For sequential processing of every commit, use merge queues or external serialization tooling'
|
|
95
|
+
- 'If every commit must be processed, either remove the concurrency block or use an event-based queue (e.g., repository_dispatch)'
|
|
96
|
+
docs:
|
|
97
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs'
|
|
98
|
+
label: 'GitHub Docs: Controlling concurrency of workflows and jobs'
|
|
99
|
+
- url: 'https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue'
|
|
100
|
+
label: 'GitHub Docs: Managing a merge queue'
|
|
101
|
+
- url: 'https://github.com/softprops/turnstyle'
|
|
102
|
+
label: 'softprops/turnstyle: Wait for in-progress workflows'
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
id: concurrency-timing-032
|
|
2
|
+
title: 'Reusable workflow concurrency: is not inherited from caller — parallel instances can run simultaneously'
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- reusable-workflow
|
|
8
|
+
- workflow-call
|
|
9
|
+
- parallel-runs
|
|
10
|
+
- concurrency-inheritance
|
|
11
|
+
- deployment
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'jobs\.\w+\.uses:\s*'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: '^concurrency:'
|
|
16
|
+
flags: 'im'
|
|
17
|
+
error_messages:
|
|
18
|
+
- 'Multiple simultaneous deployments triggered despite concurrency group on caller workflow'
|
|
19
|
+
- 'Reusable workflow running in parallel when caller has cancel-in-progress: true'
|
|
20
|
+
- 'Concurrency group on calling workflow does not prevent parallel reusable workflow runs'
|
|
21
|
+
root_cause: |
|
|
22
|
+
When a workflow defines concurrency: at the workflow level and calls a reusable
|
|
23
|
+
workflow via jobs.<id>.uses:, the concurrency configuration is NOT propagated
|
|
24
|
+
to the called (callee) workflow. The reusable workflow runs with its own
|
|
25
|
+
independent concurrency context.
|
|
26
|
+
|
|
27
|
+
This means:
|
|
28
|
+
- Workflow A has concurrency: group: deploy-${{ github.ref }}
|
|
29
|
+
- Workflow A calls Reusable-Deploy.yml via jobs.deploy.uses
|
|
30
|
+
- If two branches both trigger Workflow A simultaneously, each instance of Workflow A
|
|
31
|
+
serializes itself via its own concurrency group — but both instances of
|
|
32
|
+
Reusable-Deploy.yml run in parallel with no concurrency restriction
|
|
33
|
+
|
|
34
|
+
The concurrency group defined in the CALLER protects the caller's run from
|
|
35
|
+
duplicate callers on the same ref. It does NOT create any concurrency group for
|
|
36
|
+
the callee. The callee executes as a distinct workflow run with no inherited
|
|
37
|
+
concurrency settings.
|
|
38
|
+
|
|
39
|
+
This is especially problematic for shared deployment reusable workflows:
|
|
40
|
+
- Multiple repos or branches can call the same reusable deployment workflow
|
|
41
|
+
- Each caller serializes itself per its own ref, but the shared callee runs
|
|
42
|
+
multiple parallel instances simultaneously
|
|
43
|
+
- Parallel deploys to the same environment proceed without conflict detection
|
|
44
|
+
|
|
45
|
+
Source: GitHub Docs explicitly states concurrency groups are scoped to the
|
|
46
|
+
specific workflow run. GitHub Community confirmed expected behavior in multiple
|
|
47
|
+
discussions about parallel reusable workflow instances.
|
|
48
|
+
fix: |
|
|
49
|
+
Define concurrency: inside the reusable workflow itself, or accept a
|
|
50
|
+
concurrency-key input and use it within the callee:
|
|
51
|
+
|
|
52
|
+
Option 1 — Static group in reusable workflow:
|
|
53
|
+
Add concurrency: group: deploy-reusable to the callee workflow. This serializes
|
|
54
|
+
ALL calls to that reusable workflow globally.
|
|
55
|
+
|
|
56
|
+
Option 2 — Dynamic group via input:
|
|
57
|
+
Add a concurrency-key input to the reusable workflow. The caller passes
|
|
58
|
+
its own group key. The callee uses it: group: ${{ inputs.concurrency_key }}.
|
|
59
|
+
This gives callers control over which callee instances serialize against each other.
|
|
60
|
+
|
|
61
|
+
Option 3 — Environment-level protection:
|
|
62
|
+
Use GitHub deployment environments with required reviewers. Only one deployment
|
|
63
|
+
to an environment can be in progress at a time (pending review blocks others).
|
|
64
|
+
fix_code:
|
|
65
|
+
- language: yaml
|
|
66
|
+
label: 'Caller: concurrency here only protects the caller — not the callee'
|
|
67
|
+
code: |
|
|
68
|
+
# caller-workflow.yml
|
|
69
|
+
# This concurrency group only prevents duplicate CALLERS for the same ref.
|
|
70
|
+
# It does NOT propagate to the reusable workflow.
|
|
71
|
+
concurrency:
|
|
72
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
73
|
+
cancel-in-progress: true
|
|
74
|
+
|
|
75
|
+
jobs:
|
|
76
|
+
deploy:
|
|
77
|
+
uses: ./.github/workflows/reusable-deploy.yml
|
|
78
|
+
with:
|
|
79
|
+
environment: production
|
|
80
|
+
secrets: inherit
|
|
81
|
+
|
|
82
|
+
- language: yaml
|
|
83
|
+
label: 'Fix option 1: define concurrency inside the reusable workflow'
|
|
84
|
+
code: |
|
|
85
|
+
# reusable-deploy.yml
|
|
86
|
+
on:
|
|
87
|
+
workflow_call:
|
|
88
|
+
inputs:
|
|
89
|
+
environment:
|
|
90
|
+
type: string
|
|
91
|
+
required: true
|
|
92
|
+
|
|
93
|
+
# Add concurrency here to serialize all calls to this reusable workflow
|
|
94
|
+
concurrency:
|
|
95
|
+
group: reusable-deploy-${{ inputs.environment }}
|
|
96
|
+
cancel-in-progress: false # let active deploy finish; queue latest
|
|
97
|
+
|
|
98
|
+
jobs:
|
|
99
|
+
deploy:
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
environment: ${{ inputs.environment }}
|
|
102
|
+
steps:
|
|
103
|
+
- uses: actions/checkout@v4
|
|
104
|
+
- run: echo "Deploying to ${{ inputs.environment }}"
|
|
105
|
+
|
|
106
|
+
- language: yaml
|
|
107
|
+
label: 'Fix option 2: caller passes concurrency key as input'
|
|
108
|
+
code: |
|
|
109
|
+
# reusable-deploy.yml — accepts caller-controlled concurrency key
|
|
110
|
+
on:
|
|
111
|
+
workflow_call:
|
|
112
|
+
inputs:
|
|
113
|
+
concurrency_key:
|
|
114
|
+
type: string
|
|
115
|
+
required: false
|
|
116
|
+
default: 'reusable-deploy'
|
|
117
|
+
|
|
118
|
+
concurrency:
|
|
119
|
+
group: ${{ inputs.concurrency_key }}
|
|
120
|
+
cancel-in-progress: false
|
|
121
|
+
|
|
122
|
+
jobs:
|
|
123
|
+
deploy:
|
|
124
|
+
runs-on: ubuntu-latest
|
|
125
|
+
steps:
|
|
126
|
+
- uses: actions/checkout@v4
|
|
127
|
+
- run: echo "Deploying..."
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
# caller-workflow.yml — passes its own ref as the key
|
|
131
|
+
jobs:
|
|
132
|
+
deploy:
|
|
133
|
+
uses: ./.github/workflows/reusable-deploy.yml
|
|
134
|
+
with:
|
|
135
|
+
concurrency_key: deploy-${{ github.ref }}
|
|
136
|
+
prevention:
|
|
137
|
+
- 'Never assume the caller''s concurrency: group propagates to called reusable workflows — it does not'
|
|
138
|
+
- 'Define concurrency: inside every reusable workflow that manages shared resources (deployments, releases, package publishes)'
|
|
139
|
+
- 'Use deployment environments with required reviewers as an additional serialization layer for production deploys'
|
|
140
|
+
- 'Test reusable workflows by triggering two parallel calls and verifying only one runs at a time'
|
|
141
|
+
docs:
|
|
142
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs'
|
|
143
|
+
label: 'GitHub Docs: Controlling concurrency of workflows and jobs'
|
|
144
|
+
- url: 'https://docs.github.com/en/actions/sharing-automations/reusing-workflows'
|
|
145
|
+
label: 'GitHub Docs: Reusing workflows'
|
|
146
|
+
- url: 'https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment'
|
|
147
|
+
label: 'GitHub Docs: Managing environments for deployment'
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
id: permissions-auth-039
|
|
2
|
+
title: "setup-node registry-url creates .npmrc but NODE_AUTH_TOKEN not set → npm E401"
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- setup-node
|
|
7
|
+
- npm
|
|
8
|
+
- registry
|
|
9
|
+
- authentication
|
|
10
|
+
- publish
|
|
11
|
+
- node_auth_token
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'npm ERR! code E401'
|
|
14
|
+
flags: i
|
|
15
|
+
- regex: 'npm ERR! 401 Unauthorized'
|
|
16
|
+
flags: i
|
|
17
|
+
- regex: 'npm ERR! need auth'
|
|
18
|
+
flags: i
|
|
19
|
+
- regex: 'npm error code EBADAUTH'
|
|
20
|
+
flags: i
|
|
21
|
+
error_messages:
|
|
22
|
+
- "npm ERR! code E401"
|
|
23
|
+
- "npm ERR! 401 Unauthorized - PUT https://registry.npmjs.org/@scope/package-name"
|
|
24
|
+
- "npm ERR! need auth You need to authorize this machine using `npm adduser`"
|
|
25
|
+
- "npm error code EBADAUTH"
|
|
26
|
+
root_cause: |
|
|
27
|
+
When registry-url is set in actions/setup-node, the action generates an .npmrc file
|
|
28
|
+
containing a token placeholder line such as:
|
|
29
|
+
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
|
|
30
|
+
|
|
31
|
+
The variable name NODE_AUTH_TOKEN is hardcoded in this template and must be supplied
|
|
32
|
+
as an environment variable at runtime on every step that communicates with the registry.
|
|
33
|
+
|
|
34
|
+
If the npm publish or npm install step does not declare
|
|
35
|
+
`NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}` in its env: block, the placeholder expands
|
|
36
|
+
to an empty string. npm sends an unauthenticated request and receives HTTP 401
|
|
37
|
+
Unauthorized from the registry.
|
|
38
|
+
|
|
39
|
+
The setup-node step itself succeeds with no warnings — there is no validation that
|
|
40
|
+
NODE_AUTH_TOKEN will be set. The 401 only surfaces during the npm command, misleading
|
|
41
|
+
developers to investigate the token or registry configuration rather than the missing
|
|
42
|
+
env: declaration.
|
|
43
|
+
fix: |
|
|
44
|
+
Add NODE_AUTH_TOKEN to the env: block of every step that runs npm commands against the
|
|
45
|
+
authenticated registry. The variable name must be exactly NODE_AUTH_TOKEN — npm reads
|
|
46
|
+
it directly from the .npmrc template generated by setup-node.
|
|
47
|
+
|
|
48
|
+
For GitHub Packages (npm.pkg.github.com), use secrets.GITHUB_TOKEN.
|
|
49
|
+
For npmjs.com, use a dedicated automation token stored as a repository secret (e.g. NPM_TOKEN).
|
|
50
|
+
|
|
51
|
+
If all registry requests — including npm install of private scoped packages — need
|
|
52
|
+
authentication (not just publish), also set always-auth: true in the setup-node step.
|
|
53
|
+
fix_code:
|
|
54
|
+
- language: yaml
|
|
55
|
+
label: "Correct: NODE_AUTH_TOKEN on the publish step"
|
|
56
|
+
code: |
|
|
57
|
+
- name: Set up Node.js
|
|
58
|
+
uses: actions/setup-node@v4
|
|
59
|
+
with:
|
|
60
|
+
node-version: '20'
|
|
61
|
+
registry-url: 'https://registry.npmjs.org'
|
|
62
|
+
|
|
63
|
+
- name: Publish to npm
|
|
64
|
+
run: npm publish
|
|
65
|
+
env:
|
|
66
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: "GitHub Packages registry variant"
|
|
69
|
+
code: |
|
|
70
|
+
- name: Set up Node.js
|
|
71
|
+
uses: actions/setup-node@v4
|
|
72
|
+
with:
|
|
73
|
+
node-version: '20'
|
|
74
|
+
registry-url: 'https://npm.pkg.github.com'
|
|
75
|
+
scope: '@your-org'
|
|
76
|
+
|
|
77
|
+
- name: Publish to GitHub Packages
|
|
78
|
+
run: npm publish
|
|
79
|
+
env:
|
|
80
|
+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
81
|
+
- language: yaml
|
|
82
|
+
label: "Private package install with always-auth"
|
|
83
|
+
code: |
|
|
84
|
+
- name: Set up Node.js
|
|
85
|
+
uses: actions/setup-node@v4
|
|
86
|
+
with:
|
|
87
|
+
node-version: '20'
|
|
88
|
+
registry-url: 'https://npm.pkg.github.com'
|
|
89
|
+
scope: '@your-org'
|
|
90
|
+
always-auth: true # Send auth on all requests, not just publish
|
|
91
|
+
|
|
92
|
+
- name: Install dependencies (includes private scoped packages)
|
|
93
|
+
run: npm ci
|
|
94
|
+
env:
|
|
95
|
+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
96
|
+
prevention:
|
|
97
|
+
- "Every npm step that interacts with an authenticated registry must declare NODE_AUTH_TOKEN in its env: block"
|
|
98
|
+
- "Setting registry-url in setup-node does NOT automatically forward any secrets to npm — the env: mapping is always required"
|
|
99
|
+
- "Use always-auth: true in setup-node when npm install (not just publish) must authenticate, such as for private scoped packages"
|
|
100
|
+
- "Store your npm automation token as a repository secret: Settings → Secrets and variables → Actions → New repository secret"
|
|
101
|
+
- "For GitHub Packages, NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} is sufficient — no separate token needed"
|
|
102
|
+
docs:
|
|
103
|
+
- url: "https://github.com/actions/setup-node#publishing-to-npmjs-and-github-packages-registries"
|
|
104
|
+
label: "actions/setup-node: Publishing to registries"
|
|
105
|
+
- url: "https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry"
|
|
106
|
+
label: "GitHub Docs: Working with the npm registry"
|
|
107
|
+
- url: "https://docs.npmjs.com/using-private-packages-in-a-ci-cd-workflow"
|
|
108
|
+
label: "npm Docs: Using private packages in CI/CD"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
id: runner-environment-111
|
|
2
|
+
title: "setup-node node-version: 'latest' silently upgrades to new Node.js major, breaking engines field"
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- setup-node
|
|
7
|
+
- node-version
|
|
8
|
+
- latest
|
|
9
|
+
- engines
|
|
10
|
+
- breaking-change
|
|
11
|
+
- major-version
|
|
12
|
+
- semver
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'The engine "node" is incompatible with this module'
|
|
15
|
+
flags: i
|
|
16
|
+
- regex: 'EBADENGINE.*Unsupported engine'
|
|
17
|
+
flags: i
|
|
18
|
+
- regex: 'npm warn EBADENGINE'
|
|
19
|
+
flags: i
|
|
20
|
+
- regex: 'engine.*node.*incompatible.*Expected version'
|
|
21
|
+
flags: i
|
|
22
|
+
error_messages:
|
|
23
|
+
- "error The engine \"node\" is incompatible with this module. Expected version \">=18 <21\". Got \"23.x.x\""
|
|
24
|
+
- "npm warn EBADENGINE Unsupported engine { required: { node: '>=16 <20' }, current: { node: 'v22.0.0' } }"
|
|
25
|
+
- "npm error code EBADENGINE"
|
|
26
|
+
- "error This package requires Node.js >= 18.0.0 and <= 22.x"
|
|
27
|
+
root_cause: |
|
|
28
|
+
When node-version: 'latest' is used in actions/setup-node, the action resolves the
|
|
29
|
+
alias against the official Node.js release schedule manifest on every workflow run.
|
|
30
|
+
When Node.js releases a new major version (e.g. v23.0.0, v24.0.0), the next run silently
|
|
31
|
+
downloads and activates the new major without any warning in the setup-node step output.
|
|
32
|
+
|
|
33
|
+
Packages that declare an engines constraint in their package.json (e.g.
|
|
34
|
+
engines: { node: ">=18 <22" }) then fail during npm install or yarn install with
|
|
35
|
+
EBADENGINE because the newly installed version exceeds the accepted range.
|
|
36
|
+
|
|
37
|
+
The setup-node step itself succeeds and logs the installed version; the failure only
|
|
38
|
+
appears downstream when the package manager processes the engines field. Because the
|
|
39
|
+
workflow ran successfully for months before the Node.js major release, developers
|
|
40
|
+
are not expecting a version bump and may incorrectly blame a dependency change.
|
|
41
|
+
|
|
42
|
+
Native addons and frameworks that have not yet published compatibility updates for the
|
|
43
|
+
new major can also fail during postinstall or build steps.
|
|
44
|
+
fix: |
|
|
45
|
+
Pin to a specific LTS major version string (e.g. '20', '22') rather than 'latest'.
|
|
46
|
+
LTS majors receive security patches but do not automatically jump to a new major.
|
|
47
|
+
|
|
48
|
+
Alternatively, use the node-version-file input to read the version from a .nvmrc or
|
|
49
|
+
.node-version file committed to the repository. This keeps the Node.js version
|
|
50
|
+
consistent between local development and CI and makes version upgrades explicit
|
|
51
|
+
(a PR changes the version file).
|
|
52
|
+
fix_code:
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: "Pin to a specific LTS major (recommended)"
|
|
55
|
+
code: |
|
|
56
|
+
- name: Set up Node.js
|
|
57
|
+
uses: actions/setup-node@v4
|
|
58
|
+
with:
|
|
59
|
+
node-version: '22' # Pinned LTS major — will not jump to Node 24 automatically
|
|
60
|
+
cache: 'npm'
|
|
61
|
+
- language: yaml
|
|
62
|
+
label: "Use .nvmrc for repo-defined version shared with local dev"
|
|
63
|
+
code: |
|
|
64
|
+
# .nvmrc (committed to repository root)
|
|
65
|
+
# 22.x
|
|
66
|
+
|
|
67
|
+
- name: Set up Node.js
|
|
68
|
+
uses: actions/setup-node@v4
|
|
69
|
+
with:
|
|
70
|
+
node-version-file: '.nvmrc' # Same version in local dev and CI
|
|
71
|
+
cache: 'npm'
|
|
72
|
+
- language: yaml
|
|
73
|
+
label: "Pin with explicit patch version for maximum reproducibility"
|
|
74
|
+
code: |
|
|
75
|
+
- name: Set up Node.js
|
|
76
|
+
uses: actions/setup-node@v4
|
|
77
|
+
with:
|
|
78
|
+
node-version: '22.13.1' # Exact patch — fully reproducible but requires manual updates
|
|
79
|
+
cache: 'npm'
|
|
80
|
+
prevention:
|
|
81
|
+
- "Never use node-version: 'latest' in production or long-lived CI workflows — pin to an LTS major"
|
|
82
|
+
- "Prefer '18', '20', or '22' (active LTS) — these receive security patches without surprise major bumps"
|
|
83
|
+
- "Use node-version-file pointing to .nvmrc to keep local development and CI on the same version"
|
|
84
|
+
- "Use Renovate or Dependabot to automate controlled Node.js upgrades with a PR and changelog review"
|
|
85
|
+
- "Test new Node.js majors in a dedicated branch before adopting them in main CI"
|
|
86
|
+
- "Declare an engines field in package.json to make your Node.js version requirement explicit and testable"
|
|
87
|
+
docs:
|
|
88
|
+
- url: "https://github.com/actions/setup-node#supported-version-syntax"
|
|
89
|
+
label: "actions/setup-node: Supported version syntax"
|
|
90
|
+
- url: "https://nodejs.org/en/about/previous-releases"
|
|
91
|
+
label: "Node.js: Release schedule and LTS versions"
|
|
92
|
+
- url: "https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines"
|
|
93
|
+
label: "npm: package.json engines field documentation"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
id: silent-failures-054
|
|
2
|
+
title: "Windows CRLF line endings in committed scripts cause bad interpreter error on Linux runners"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- checkout
|
|
7
|
+
- crlf
|
|
8
|
+
- line-endings
|
|
9
|
+
- windows
|
|
10
|
+
- bash
|
|
11
|
+
- gitattributes
|
|
12
|
+
- bad-interpreter
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'bad interpreter.*No such file or directory'
|
|
15
|
+
flags: i
|
|
16
|
+
- regex: '\^M: command not found'
|
|
17
|
+
flags: ''
|
|
18
|
+
- regex: '/bin/bash\^M'
|
|
19
|
+
flags: ''
|
|
20
|
+
- regex: '\r: command not found'
|
|
21
|
+
flags: ''
|
|
22
|
+
error_messages:
|
|
23
|
+
- "/bin/bash^M: bad interpreter: No such file or directory"
|
|
24
|
+
- "^M: command not found"
|
|
25
|
+
- ": /bin/sh^M: bad interpreter: No such file or directory"
|
|
26
|
+
- "syntax error: unexpected end of file"
|
|
27
|
+
root_cause: |
|
|
28
|
+
Shell scripts, Python files, and other text files committed from Windows workstations
|
|
29
|
+
where core.autocrlf is false (or not configured) retain Windows CRLF (\r\n) line endings.
|
|
30
|
+
actions/checkout preserves committed bytes exactly — it does not normalize line endings.
|
|
31
|
+
|
|
32
|
+
On a Linux runner, the kernel reads the shebang line #!/bin/bash\r as the interpreter
|
|
33
|
+
path /bin/bash^M (with a literal carriage return appended). No file with that name exists,
|
|
34
|
+
so the kernel returns "bad interpreter: No such file or directory." The error message
|
|
35
|
+
looks like a missing binary or path problem, masking the true cause: CRLF line endings.
|
|
36
|
+
|
|
37
|
+
This is a classic silent failure because:
|
|
38
|
+
- The developer's Windows machine runs the script correctly
|
|
39
|
+
- The checkout step succeeds with no warnings about line endings
|
|
40
|
+
- The failure only manifests when the script is executed on a Linux runner
|
|
41
|
+
- Most text editors hide the ^M characters, so the file looks normal in review
|
|
42
|
+
fix: |
|
|
43
|
+
Add a .gitattributes file to the repository root specifying LF normalization for text
|
|
44
|
+
files. This instructs Git to store files with LF endings in the repository regardless
|
|
45
|
+
of the committer's OS or local git configuration.
|
|
46
|
+
|
|
47
|
+
After adding .gitattributes, re-normalize all tracked files: stage all files with the
|
|
48
|
+
renormalize flag (e.g. `add --renormalize .` via the CLI), then commit the result.
|
|
49
|
+
Without this step, already-committed CRLF files remain unchanged.
|
|
50
|
+
|
|
51
|
+
Also add a CI detection step to catch any future regressions before they reach main.
|
|
52
|
+
fix_code:
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: ".gitattributes — enforce LF for scripts and text files"
|
|
55
|
+
code: |
|
|
56
|
+
# .gitattributes (add to repository root)
|
|
57
|
+
|
|
58
|
+
# Normalize all text files to LF in the repository
|
|
59
|
+
* text=auto eol=lf
|
|
60
|
+
|
|
61
|
+
# Explicitly enforce LF for scripts and config files
|
|
62
|
+
*.sh text eol=lf
|
|
63
|
+
*.bash text eol=lf
|
|
64
|
+
*.py text eol=lf
|
|
65
|
+
*.yml text eol=lf
|
|
66
|
+
*.yaml text eol=lf
|
|
67
|
+
*.json text eol=lf
|
|
68
|
+
|
|
69
|
+
# Keep CRLF for Windows-specific files
|
|
70
|
+
*.bat text eol=crlf
|
|
71
|
+
*.cmd text eol=crlf
|
|
72
|
+
- language: yaml
|
|
73
|
+
label: "CI step to detect CRLF in shell and Python scripts"
|
|
74
|
+
code: |
|
|
75
|
+
- name: Check for CRLF line endings in scripts
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
steps:
|
|
78
|
+
- uses: actions/checkout@v4
|
|
79
|
+
- name: Detect CRLF
|
|
80
|
+
run: |
|
|
81
|
+
found=$(find . -name '*.sh' -o -name '*.py' -o -name '*.yml' | \
|
|
82
|
+
xargs file 2>/dev/null | grep CRLF || true)
|
|
83
|
+
if [ -n "$found" ]; then
|
|
84
|
+
echo "ERROR: CRLF line endings detected in the following files:"
|
|
85
|
+
echo "$found"
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
prevention:
|
|
89
|
+
- "Add .gitattributes with `* text=auto eol=lf` to every repository that may be edited on Windows"
|
|
90
|
+
- "Re-normalize existing files after adding .gitattributes: use the CLI `add --renormalize .` flag, then commit"
|
|
91
|
+
- "Configure the autocrlf setting on Windows development machines: set core.autocrlf=input so CRLF is converted to LF on commit"
|
|
92
|
+
- "Configure your editor (VS Code, Notepad++, JetBrains) to use LF line endings for shell and YAML files by default"
|
|
93
|
+
- "Add a CRLF detection step in CI to fail PRs that introduce Windows line endings into script files"
|
|
94
|
+
docs:
|
|
95
|
+
- url: "https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings"
|
|
96
|
+
label: "GitHub Docs: Configuring Git to handle line endings"
|
|
97
|
+
- url: "https://git-scm.com/docs/gitattributes"
|
|
98
|
+
label: "Git: gitattributes documentation"
|
|
99
|
+
- url: "https://github.com/actions/checkout/issues/135"
|
|
100
|
+
label: "actions/checkout Issue #135: Line ending normalization"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
id: yaml-syntax-037
|
|
2
|
+
title: 'Step cannot have both uses: and run: — mutually exclusive keys cause workflow validation failure'
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- yaml-syntax
|
|
7
|
+
- uses
|
|
8
|
+
- run
|
|
9
|
+
- step-definition
|
|
10
|
+
- workflow-validation
|
|
11
|
+
- mutually-exclusive
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'uses:\s*.+\n\s+run:'
|
|
14
|
+
flags: 'im'
|
|
15
|
+
- regex: 'run:\s*.+\n\s+uses:'
|
|
16
|
+
flags: 'im'
|
|
17
|
+
error_messages:
|
|
18
|
+
- "A step must define one of ['run', 'uses']"
|
|
19
|
+
- 'Unexpected value ''run'''
|
|
20
|
+
- 'Workflow file is not valid: .github/workflows/ci.yml (Line: N, Col: M): Unexpected value ''run'''
|
|
21
|
+
- "A step must not define both 'uses' and 'run'"
|
|
22
|
+
root_cause: |
|
|
23
|
+
A GitHub Actions workflow step is either an action reference (uses:) or a
|
|
24
|
+
shell command (run:), but never both simultaneously. These two keys are mutually
|
|
25
|
+
exclusive by design: uses: runs a pre-built action with its own entry point, while
|
|
26
|
+
run: executes arbitrary shell commands in the runner's default shell.
|
|
27
|
+
|
|
28
|
+
Common scenarios that produce this error:
|
|
29
|
+
|
|
30
|
+
1. Adding a quick debug command to an existing action step:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
run: echo "debug" # INVALID — cannot add run: to a uses: step
|
|
33
|
+
|
|
34
|
+
2. Copy-paste error merging two separate steps into one:
|
|
35
|
+
- name: Setup and verify
|
|
36
|
+
uses: actions/setup-node@v4
|
|
37
|
+
run: node --version # INVALID — should be two separate steps
|
|
38
|
+
|
|
39
|
+
3. Attempting to run commands after an action in a single step:
|
|
40
|
+
- uses: docker/build-push-action@v6
|
|
41
|
+
with:
|
|
42
|
+
push: true
|
|
43
|
+
run: docker image ls # INVALID
|
|
44
|
+
|
|
45
|
+
The workflow is rejected at parse time with a validation error before any
|
|
46
|
+
job runs. The exact error message varies by GitHub Actions version but always
|
|
47
|
+
indicates that the step cannot define both keys.
|
|
48
|
+
fix: |
|
|
49
|
+
Split the step into two separate steps: one with uses: for the action, and
|
|
50
|
+
a new step with run: for the shell command. Each step in a job must be
|
|
51
|
+
exclusively one type.
|
|
52
|
+
fix_code:
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: 'Wrong: uses: and run: in the same step — workflow validation fails'
|
|
55
|
+
code: |
|
|
56
|
+
jobs:
|
|
57
|
+
build:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- name: Checkout and show files # INVALID
|
|
61
|
+
uses: actions/checkout@v4
|
|
62
|
+
run: ls -la # INVALID: cannot add run: to uses: step
|
|
63
|
+
|
|
64
|
+
- language: yaml
|
|
65
|
+
label: 'Correct: split into two separate steps'
|
|
66
|
+
code: |
|
|
67
|
+
jobs:
|
|
68
|
+
build:
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
steps:
|
|
71
|
+
- name: Checkout
|
|
72
|
+
uses: actions/checkout@v4 # action step
|
|
73
|
+
|
|
74
|
+
- name: Show files # separate shell step
|
|
75
|
+
run: ls -la
|
|
76
|
+
|
|
77
|
+
- language: yaml
|
|
78
|
+
label: 'Common mistake: adding debug run: to an action step'
|
|
79
|
+
code: |
|
|
80
|
+
jobs:
|
|
81
|
+
build:
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/setup-node@v4
|
|
85
|
+
with:
|
|
86
|
+
node-version: '20'
|
|
87
|
+
# WRONG: adding run: here is invalid
|
|
88
|
+
# run: node --version
|
|
89
|
+
|
|
90
|
+
# CORRECT: add a separate step after the action
|
|
91
|
+
- name: Verify Node version
|
|
92
|
+
run: node --version
|
|
93
|
+
prevention:
|
|
94
|
+
- 'Remember: every step is either uses: (action) OR run: (shell) — never both'
|
|
95
|
+
- 'To run commands after an action, always create a new step with its own name: and run:'
|
|
96
|
+
- 'Use actionlint locally to catch uses:/run: conflicts before pushing'
|
|
97
|
+
- 'Use the GitHub Actions VS Code extension — it highlights mutually exclusive keys in the editor'
|
|
98
|
+
- 'When copying steps from documentation, verify each step has only one of uses: or run:'
|
|
99
|
+
docs:
|
|
100
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsuses'
|
|
101
|
+
label: 'GitHub Docs: jobs.<job_id>.steps[*].uses'
|
|
102
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun'
|
|
103
|
+
label: 'GitHub Docs: jobs.<job_id>.steps[*].run'
|
|
104
|
+
- url: 'https://rhysd.github.io/actionlint/'
|
|
105
|
+
label: 'actionlint: Static checker for GitHub Actions workflow files'
|
package/package.json
CHANGED