@htekdev/actions-debugger 1.0.45 → 1.0.47
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-034.yml +70 -0
- package/errors/concurrency-timing/concurrency-timing-028.yml +97 -0
- package/errors/concurrency-timing/concurrency-timing-029.yml +92 -0
- package/errors/known-unsolved/known-unsolved-036.yml +87 -0
- package/errors/silent-failures/silent-failures-050.yml +93 -0
- package/errors/triggers/triggers-033.yml +100 -0
- package/errors/triggers/triggers-034.yml +87 -0
- package/errors/triggers/triggers-035.yml +114 -0
- package/errors/yaml-syntax/yaml-syntax-035.yml +114 -0
- package/package.json +1 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
id: caching-artifacts-034
|
|
2
|
+
title: "Cache entries created in a PR branch are not accessible from other PR branches or the default branch"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: limitation
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- branch-scope
|
|
8
|
+
- pull-request
|
|
9
|
+
- isolation
|
|
10
|
+
- cache-miss
|
|
11
|
+
- known-limitation
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'cache.*miss.*pull.request|cache.*not.*found.*branch|cache.*not.*accessible.*pr'
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: 'cache.*scope.*branch|branch.specific.*cache.*miss|pr.*branch.*no.*cache'
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Cache not found for input keys"
|
|
19
|
+
root_cause: |
|
|
20
|
+
GitHub Actions cache entries are strictly scoped to the branch that created them. The
|
|
21
|
+
cache lookup order during a restore operation is:
|
|
22
|
+
|
|
23
|
+
1. Exact key match on the CURRENT branch
|
|
24
|
+
2. restore-keys prefix match on the CURRENT branch
|
|
25
|
+
3. restore-keys prefix match on the BASE branch (for PRs) or the DEFAULT branch
|
|
26
|
+
|
|
27
|
+
Consequences of this scoping:
|
|
28
|
+
- A cache saved on PR branch A is NOT readable from PR branch B
|
|
29
|
+
- A cache saved on a PR branch is NOT readable from the default branch's push workflow
|
|
30
|
+
- Each new PR starts with a cold cache until it produces its own branch-scoped entry
|
|
31
|
+
- The base branch cache is only available as a fallback during a PR workflow restore
|
|
32
|
+
|
|
33
|
+
This is an intentional security isolation boundary — one PR cannot read potentially
|
|
34
|
+
sensitive build artifacts cached by another PR. There is no configuration option to
|
|
35
|
+
enable cross-PR cache sharing.
|
|
36
|
+
|
|
37
|
+
Teams migrating from shared CI caches (Jenkins, GitLab with shared runner caches) are
|
|
38
|
+
frequently surprised by persistent cache misses across parallel PRs on active repos.
|
|
39
|
+
fix: |
|
|
40
|
+
Structure cache keys and restore-keys to maximize fallback hits from the default branch.
|
|
41
|
+
The strategy: save a well-keyed cache on the default branch after each merge (readable
|
|
42
|
+
by all PRs as a fallback), and include base-branch and OS-only restore-key prefixes so
|
|
43
|
+
new PR runs warm up quickly via partial matches.
|
|
44
|
+
|
|
45
|
+
For dependency caches, a lockfile-hash-keyed entry on main ensures new PRs get a
|
|
46
|
+
near-hit on first run via restore-keys, rather than a completely cold cache.
|
|
47
|
+
fix_code:
|
|
48
|
+
- language: yaml
|
|
49
|
+
label: "Cache key strategy that maximizes default-branch fallback for all PRs"
|
|
50
|
+
code: |
|
|
51
|
+
- name: Cache dependencies
|
|
52
|
+
uses: actions/cache@v4
|
|
53
|
+
with:
|
|
54
|
+
path: ~/.npm
|
|
55
|
+
# Primary: exact branch + lockfile hash (hit on repeat runs of same PR)
|
|
56
|
+
key: ${{ runner.os }}-npm-${{ github.ref_name }}-${{ hashFiles('**/package-lock.json') }}
|
|
57
|
+
# Fallbacks: base branch (for PRs) → default branch → OS-only
|
|
58
|
+
restore-keys: |
|
|
59
|
+
${{ runner.os }}-npm-${{ github.base_ref }}-
|
|
60
|
+
${{ runner.os }}-npm-
|
|
61
|
+
prevention:
|
|
62
|
+
- "Add restore-keys with base branch and OS-only prefixes so PRs fall back to main's cache"
|
|
63
|
+
- "Ensure the default branch workflow saves a fresh cache after each merge so PRs have a warm fallback"
|
|
64
|
+
- "Be aware that parallel PRs each maintain their own independent branch-scoped cache"
|
|
65
|
+
- "For monorepos, include the workspace or package path in the cache key to prevent cross-PR key collisions"
|
|
66
|
+
docs:
|
|
67
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache"
|
|
68
|
+
label: "GitHub Docs: Cache access restrictions by branch"
|
|
69
|
+
- url: "https://github.com/orgs/community/discussions/10539"
|
|
70
|
+
label: "GitHub Community: Cache access is restricted to the same branch and base branches"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
id: concurrency-timing-028
|
|
2
|
+
title: "Matrix job outputs use last-writer-wins — only the final completing matrix instance's value is visible to downstream jobs"
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- matrix
|
|
7
|
+
- outputs
|
|
8
|
+
- last-writer-wins
|
|
9
|
+
- job-outputs
|
|
10
|
+
- needs
|
|
11
|
+
- aggregation
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'jobs\.\w+\.outputs\.\w+'
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: 'matrix.*output.*empty|matrix.*output.*missing|output.*only.*one.*matrix'
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Job output contains only one matrix item's value despite all matrix jobs completing successfully"
|
|
19
|
+
- "Downstream job receives empty string from needs.<job>.outputs.<name> after matrix build"
|
|
20
|
+
root_cause: |
|
|
21
|
+
When a matrix strategy job writes to GITHUB_OUTPUT, all parallel matrix instances share
|
|
22
|
+
the same output key namespace keyed at the logical job ID level in the workflow DAG.
|
|
23
|
+
Multiple matrix runners mapping to one job ID race to write the same output keys. The
|
|
24
|
+
last matrix runner to flush its output to the Actions service wins — all prior values for
|
|
25
|
+
that key are silently overwritten.
|
|
26
|
+
|
|
27
|
+
GitHub Docs state explicitly: "The value of the output is the last value set by the
|
|
28
|
+
expression that evaluates the output." There is no merge, append, or aggregation of
|
|
29
|
+
outputs across matrix instances.
|
|
30
|
+
|
|
31
|
+
Common manifestations:
|
|
32
|
+
- A release matrix collecting artifact paths — the downstream job sees only one path
|
|
33
|
+
- A test matrix emitting pass/fail status per OS — only the last-completing OS result survives
|
|
34
|
+
- A build matrix producing version or hash strings — final output value is non-deterministic
|
|
35
|
+
and varies by runner scheduling order, making the issue intermittent and hard to reproduce
|
|
36
|
+
|
|
37
|
+
This is an intentional platform constraint. The job outputs map is a flat key-value store
|
|
38
|
+
keyed at the job level, not the matrix-instance level. It cannot store per-instance values.
|
|
39
|
+
fix: |
|
|
40
|
+
Aggregate matrix results via uniquely-named artifacts rather than job outputs.
|
|
41
|
+
|
|
42
|
+
Strategy: each matrix instance uploads its result as a named artifact that includes the
|
|
43
|
+
matrix dimension in the artifact name (required by actions/upload-artifact@v4 anyway, which
|
|
44
|
+
rejects duplicate names). The fan-in dependent job downloads all artifacts and processes
|
|
45
|
+
them together.
|
|
46
|
+
|
|
47
|
+
For simple pass/fail aggregation, rely on the default needs: behavior — a job with
|
|
48
|
+
needs: [build] only starts when ALL matrix instances of build have completed successfully.
|
|
49
|
+
No custom output collection is needed for overall-success gating.
|
|
50
|
+
fix_code:
|
|
51
|
+
- language: yaml
|
|
52
|
+
label: "Aggregate matrix results via uniquely-named artifacts (recommended pattern)"
|
|
53
|
+
code: |
|
|
54
|
+
jobs:
|
|
55
|
+
build:
|
|
56
|
+
strategy:
|
|
57
|
+
matrix:
|
|
58
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
59
|
+
runs-on: ${{ matrix.os }}
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/checkout@v4
|
|
62
|
+
|
|
63
|
+
- name: Build and capture result
|
|
64
|
+
run: echo "artifact-for-${{ matrix.os }}" > result.txt
|
|
65
|
+
|
|
66
|
+
- name: Upload per-matrix artifact
|
|
67
|
+
uses: actions/upload-artifact@v4
|
|
68
|
+
with:
|
|
69
|
+
# Matrix dimension in name prevents v4 duplicate-name error
|
|
70
|
+
name: build-result-${{ matrix.os }}
|
|
71
|
+
path: result.txt
|
|
72
|
+
|
|
73
|
+
release:
|
|
74
|
+
needs: build # Waits for ALL matrix instances to succeed
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
steps:
|
|
77
|
+
- name: Download all matrix results
|
|
78
|
+
uses: actions/download-artifact@v4
|
|
79
|
+
with:
|
|
80
|
+
pattern: build-result-*
|
|
81
|
+
merge-multiple: true
|
|
82
|
+
path: results/
|
|
83
|
+
|
|
84
|
+
- name: Process aggregated results
|
|
85
|
+
run: ls results/ && cat results/result.txt
|
|
86
|
+
prevention:
|
|
87
|
+
- "Never rely on GITHUB_OUTPUT in matrix jobs to pass per-instance values to downstream jobs"
|
|
88
|
+
- "Always include the matrix dimension in artifact names when uploading from matrix jobs"
|
|
89
|
+
- "Use needs: [job] to gate on all matrix instances succeeding — no custom output collection needed for pass/fail"
|
|
90
|
+
- "Add a workflow comment documenting that the fan-in pattern is required for multi-value matrix aggregation"
|
|
91
|
+
docs:
|
|
92
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs"
|
|
93
|
+
label: "GitHub Docs: Passing information between jobs"
|
|
94
|
+
- url: "https://github.com/orgs/community/discussions/26286"
|
|
95
|
+
label: "GitHub Community #26286: Matrix job outputs — last writer wins"
|
|
96
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idoutputs"
|
|
97
|
+
label: "GitHub Docs: jobs.<job_id>.outputs"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
id: concurrency-timing-029
|
|
2
|
+
title: "cancel-in-progress: true does not release a deployment environment lock — subsequent runs wait indefinitely"
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- cancel-in-progress
|
|
8
|
+
- deployment-environment
|
|
9
|
+
- environment-lock
|
|
10
|
+
- queue-stall
|
|
11
|
+
- protection-rules
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'waiting.*deployment.*environment|environment.*lock.*not.*released'
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: 'deployment.*stuck.*waiting|cancel.*in.*progress.*environment.*stall'
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Deployment stuck in 'Waiting for deployment' state after previous run was cancelled"
|
|
19
|
+
- "Job has been waiting for a deployment to complete for over N minutes"
|
|
20
|
+
root_cause: |
|
|
21
|
+
GitHub Actions deployment environments use a distributed lock to ensure only one workflow
|
|
22
|
+
run deploys to an environment at a time. When a job is cancelled while holding or waiting
|
|
23
|
+
for an environment lock, the lock release is not guaranteed to be atomic with the cancellation.
|
|
24
|
+
|
|
25
|
+
The race condition sequence:
|
|
26
|
+
1. Run A acquires the environment lock and begins deploying
|
|
27
|
+
2. Run B starts, enters "Waiting for deployment" state (environment locked by A)
|
|
28
|
+
3. Run C triggers with cancel-in-progress: true — Run B (the waiting run) is cancelled
|
|
29
|
+
4. Run A completes and releases its lock
|
|
30
|
+
5. Run C's deploy job is now queued but sees the environment still showing a pending entry
|
|
31
|
+
from the cancelled Run B, causing it to wait indefinitely
|
|
32
|
+
|
|
33
|
+
A separate scenario: Run A itself is cancelled mid-deploy. The environment can enter
|
|
34
|
+
a "deployment in progress" limbo state where no active run owns the lock but the
|
|
35
|
+
environment page still shows a pending deployment. New runs queue successfully but
|
|
36
|
+
hang waiting for a lock that will never be released automatically.
|
|
37
|
+
|
|
38
|
+
This is exacerbated on active repos where pushes arrive faster than deploy jobs complete,
|
|
39
|
+
causing continuous queue stalls on the environment.
|
|
40
|
+
fix: |
|
|
41
|
+
Separate the concurrency group into two distinct layers: one for build/test jobs
|
|
42
|
+
(safe to cancel freely) and one for deploy jobs (must serialize to completion).
|
|
43
|
+
|
|
44
|
+
Build jobs use cancel-in-progress: true — wasteful test runs are discarded when
|
|
45
|
+
a newer commit arrives. Deploy jobs use cancel-in-progress: false — they run to
|
|
46
|
+
completion serially, preventing mid-deploy cancellations that create lock limbo.
|
|
47
|
+
|
|
48
|
+
Set timeout-minutes on all deploy jobs so stuck environment waits self-resolve with
|
|
49
|
+
a clear failure rather than hanging for the job's default 6-hour timeout.
|
|
50
|
+
|
|
51
|
+
To manually recover a stalled environment: navigate to Settings → Environments →
|
|
52
|
+
select the stalled environment → cancel any pending deployment entries in the UI.
|
|
53
|
+
fix_code:
|
|
54
|
+
- language: yaml
|
|
55
|
+
label: "Separate concurrency groups — cancel builds freely, serialize deploys safely"
|
|
56
|
+
code: |
|
|
57
|
+
jobs:
|
|
58
|
+
build:
|
|
59
|
+
# Cancel in-progress builds freely — no environment lock at stake
|
|
60
|
+
concurrency:
|
|
61
|
+
group: build-${{ github.ref }}
|
|
62
|
+
cancel-in-progress: true
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@v4
|
|
66
|
+
- run: echo "build and test steps"
|
|
67
|
+
|
|
68
|
+
deploy:
|
|
69
|
+
needs: build
|
|
70
|
+
environment: production
|
|
71
|
+
# NEVER cancel in-progress deploy jobs — let them finish to avoid lock limbo
|
|
72
|
+
concurrency:
|
|
73
|
+
group: deploy-production
|
|
74
|
+
cancel-in-progress: false
|
|
75
|
+
timeout-minutes: 30 # Self-heal on stuck environment wait (default is 6h)
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
steps:
|
|
78
|
+
- uses: actions/checkout@v4
|
|
79
|
+
- run: echo "deploy steps here"
|
|
80
|
+
prevention:
|
|
81
|
+
- "Never use cancel-in-progress: true on jobs that target a deployment environment"
|
|
82
|
+
- "Always set timeout-minutes on deploy jobs — the default 6-hour timeout makes stalls very painful"
|
|
83
|
+
- "Use separate concurrency groups for build (cancel-friendly) vs deploy (serialize) stages"
|
|
84
|
+
- "Monitor the Environments page for stale 'Deployment pending' entries after CI incidents"
|
|
85
|
+
- "Consider dedicated environment concurrency groups (e.g., deploy-production vs deploy-staging) to prevent cross-environment interference"
|
|
86
|
+
docs:
|
|
87
|
+
- url: "https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment"
|
|
88
|
+
label: "GitHub Docs: Managing environments for deployment"
|
|
89
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#concurrency"
|
|
90
|
+
label: "GitHub Docs: Workflow concurrency"
|
|
91
|
+
- url: "https://github.com/orgs/community/discussions/14490"
|
|
92
|
+
label: "GitHub Community #14490: cancel-in-progress and deployment environment lock"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
id: known-unsolved-036
|
|
2
|
+
title: "Scheduled workflows auto-disabled after 60 days of repository inactivity"
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: limitation
|
|
5
|
+
tags:
|
|
6
|
+
- schedule
|
|
7
|
+
- cron
|
|
8
|
+
- inactivity
|
|
9
|
+
- disabled
|
|
10
|
+
- known-limitation
|
|
11
|
+
- maintenance
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'This scheduled workflow is disabled|scheduled workflow.*disabled.*no.*activity'
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: 'schedule.*60.day.*inactiv|60.day.*no.*activity.*workflow'
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "This scheduled workflow is disabled because there has been no repository activity in the past 60 days"
|
|
19
|
+
root_cause: |
|
|
20
|
+
GitHub automatically disables on: schedule workflows in repositories that have had no
|
|
21
|
+
activity (pushes, pull requests, merges) for 60 days. This is a platform resource
|
|
22
|
+
management decision to reduce compute on dormant repositories.
|
|
23
|
+
|
|
24
|
+
When triggered, the Actions tab displays a banner:
|
|
25
|
+
"This scheduled workflow is disabled because there has been no repository activity
|
|
26
|
+
in the past 60 days."
|
|
27
|
+
|
|
28
|
+
GitHub sends a notification email before disabling, but many maintainers miss it.
|
|
29
|
+
|
|
30
|
+
Common affected scenarios:
|
|
31
|
+
- Maintenance repos (dependency update jobs, reporting pipelines) with infrequent code changes
|
|
32
|
+
- Scheduled audit or monitoring workflows in stable, rarely-pushed codebases
|
|
33
|
+
- Open source repos that go quiet between releases
|
|
34
|
+
- Returning after extended leave to find weeks or months of scheduled runs silently missed
|
|
35
|
+
|
|
36
|
+
There is no workflow-level setting to prevent automatic disabling. The only remedies
|
|
37
|
+
are to manually re-enable via the GitHub UI or maintain repository activity artificially.
|
|
38
|
+
fix: |
|
|
39
|
+
To re-enable a disabled scheduled workflow:
|
|
40
|
+
1. Navigate to the repository Actions tab
|
|
41
|
+
2. Select the disabled workflow in the left sidebar
|
|
42
|
+
3. Click the "Enable workflow" button shown in the banner
|
|
43
|
+
|
|
44
|
+
To prevent future auto-disabling, add a keep-alive workflow that creates an empty
|
|
45
|
+
commit on a schedule shorter than 60 days. Using stefanzweifel/git-auto-commit-action
|
|
46
|
+
with --allow-empty avoids modifying tracked files while still generating commit activity
|
|
47
|
+
that resets the 60-day counter.
|
|
48
|
+
|
|
49
|
+
There is no REST API endpoint to re-enable disabled scheduled workflows — it must be
|
|
50
|
+
done via the GitHub UI or by pushing a new commit to the repository.
|
|
51
|
+
fix_code:
|
|
52
|
+
- language: yaml
|
|
53
|
+
label: "Keep-alive workflow — runs monthly to prevent schedule auto-disable"
|
|
54
|
+
code: |
|
|
55
|
+
# .github/workflows/keep-alive.yml
|
|
56
|
+
name: Keep Alive
|
|
57
|
+
|
|
58
|
+
on:
|
|
59
|
+
schedule:
|
|
60
|
+
- cron: '0 0 1 * *' # First of each month — well within 60-day window
|
|
61
|
+
workflow_dispatch:
|
|
62
|
+
|
|
63
|
+
jobs:
|
|
64
|
+
keep-alive:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
permissions:
|
|
67
|
+
contents: write
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v4
|
|
70
|
+
|
|
71
|
+
- name: Maintain repository activity
|
|
72
|
+
uses: stefanzweifel/git-auto-commit-action@v5
|
|
73
|
+
with:
|
|
74
|
+
commit_message: 'chore: keep-alive [skip ci]'
|
|
75
|
+
commit_options: '--allow-empty'
|
|
76
|
+
prevention:
|
|
77
|
+
- "Monitor the Actions tab for the 60-day inactivity warning banner"
|
|
78
|
+
- "Add a keep-alive workflow to all repos with important scheduled jobs but infrequent commits"
|
|
79
|
+
- "Subscribe to GitHub notification emails so the pre-disable warning is not missed"
|
|
80
|
+
- "Document this limitation in the repo CONTRIBUTING guide for future maintainers"
|
|
81
|
+
docs:
|
|
82
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule"
|
|
83
|
+
label: "GitHub Docs: schedule trigger"
|
|
84
|
+
- url: "https://github.com/orgs/community/discussions/5614"
|
|
85
|
+
label: "GitHub Community #5614: Scheduled workflow disabled after 60 days of inactivity"
|
|
86
|
+
- url: "https://github.com/stefanzweifel/git-auto-commit-action"
|
|
87
|
+
label: "stefanzweifel/git-auto-commit-action — keep-alive commit pattern"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
id: silent-failures-050
|
|
2
|
+
title: "continue-on-error: true on a failing step silently prevents if: failure() downstream steps from running"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- continue-on-error
|
|
7
|
+
- if-condition
|
|
8
|
+
- failure-check
|
|
9
|
+
- step-outcome
|
|
10
|
+
- silent-skip
|
|
11
|
+
- cleanup
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'continue.on.error.*if.*failure|if.*failure.*not.*running.*continue.on.error'
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: 'cleanup.*step.*skipped.*continue.on.error|failure.*handler.*not.*triggered'
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Downstream step with if: failure() silently skipped when preceding step uses continue-on-error: true"
|
|
19
|
+
root_cause: |
|
|
20
|
+
When a step fails AND has continue-on-error: true, GitHub Actions marks that step's
|
|
21
|
+
conclusion as success (the error was absorbed) while preserving its outcome as failure.
|
|
22
|
+
Because the step conclusion is success, the job's overall result remains success.
|
|
23
|
+
|
|
24
|
+
Any downstream step or job conditioned on if: failure() evaluates the job's overall
|
|
25
|
+
status, not individual step outcomes. Since the job is still succeeding (the failure was
|
|
26
|
+
continued past), if: failure() evaluates to false — and those downstream steps are
|
|
27
|
+
silently skipped with no error or warning.
|
|
28
|
+
|
|
29
|
+
The pattern developers expect to work (but doesn't):
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: npm test
|
|
34
|
+
continue-on-error: true
|
|
35
|
+
- name: Upload failure report # NEVER runs — job outcome is still 'success'
|
|
36
|
+
if: failure()
|
|
37
|
+
|
|
38
|
+
Silently broken patterns:
|
|
39
|
+
- Failure notification steps after continue-on-error test or lint steps
|
|
40
|
+
- Cleanup steps conditioned on job failure after a continued-on-error build
|
|
41
|
+
- Report upload or artifact save steps expected to fire when tests fail
|
|
42
|
+
- Alerting steps (Slack, PagerDuty) conditioned on failure() after a tolerated failure
|
|
43
|
+
fix: |
|
|
44
|
+
Reference the specific failing step's outcome directly using steps.<id>.outcome
|
|
45
|
+
instead of the job-level failure() function. Give the step-with-continue-on-error an
|
|
46
|
+
explicit id: field, then condition the downstream step on
|
|
47
|
+
steps.<step-id>.outcome == 'failure'.
|
|
48
|
+
|
|
49
|
+
This correctly captures the step-level failure even when the job overall is succeeding.
|
|
50
|
+
fix_code:
|
|
51
|
+
- language: yaml
|
|
52
|
+
label: "Wrong: if: failure() silently never fires after a continue-on-error step"
|
|
53
|
+
code: |
|
|
54
|
+
steps:
|
|
55
|
+
- name: Run tests
|
|
56
|
+
run: npm test
|
|
57
|
+
continue-on-error: true
|
|
58
|
+
# Job outcome stays 'success' — if: failure() below never executes
|
|
59
|
+
|
|
60
|
+
- name: Upload test failure report
|
|
61
|
+
if: failure()
|
|
62
|
+
uses: actions/upload-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: test-report
|
|
65
|
+
path: test-results/
|
|
66
|
+
- language: yaml
|
|
67
|
+
label: "Correct: check the specific step outcome to catch the continued failure"
|
|
68
|
+
code: |
|
|
69
|
+
steps:
|
|
70
|
+
- name: Run tests
|
|
71
|
+
id: run-tests # Explicit ID required to reference outcome below
|
|
72
|
+
run: npm test
|
|
73
|
+
continue-on-error: true
|
|
74
|
+
|
|
75
|
+
- name: Upload test failure report
|
|
76
|
+
# Fires when tests failed — works correctly even though the job is still 'success'
|
|
77
|
+
if: steps.run-tests.outcome == 'failure'
|
|
78
|
+
uses: actions/upload-artifact@v4
|
|
79
|
+
with:
|
|
80
|
+
name: test-report
|
|
81
|
+
path: test-results/
|
|
82
|
+
prevention:
|
|
83
|
+
- "Never rely on if: failure() to detect failures from steps that use continue-on-error: true"
|
|
84
|
+
- "Assign explicit id: to any step with continue-on-error that needs downstream conditional checks"
|
|
85
|
+
- "Use steps.<id>.outcome == 'failure' to condition steps on a specific earlier step's result"
|
|
86
|
+
- "Test failure-handler steps explicitly by temporarily forcing the upstream step to fail without continue-on-error"
|
|
87
|
+
docs:
|
|
88
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-conditions-to-control-job-execution"
|
|
89
|
+
label: "GitHub Docs: Using conditions to control job execution"
|
|
90
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context"
|
|
91
|
+
label: "GitHub Docs: steps context — outcome vs conclusion"
|
|
92
|
+
- url: "https://github.com/orgs/community/discussions/25420"
|
|
93
|
+
label: "GitHub Community: continue-on-error and if: failure() interaction explained"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
id: triggers-033
|
|
2
|
+
title: "Commits, PRs, and releases created using GITHUB_TOKEN do not trigger new workflow runs"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: limitation
|
|
5
|
+
tags:
|
|
6
|
+
- GITHUB_TOKEN
|
|
7
|
+
- loopback-prevention
|
|
8
|
+
- push-event
|
|
9
|
+
- pull-request
|
|
10
|
+
- no-trigger
|
|
11
|
+
- known-limitation
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'GITHUB_TOKEN.*push.*not.*trigger|workflow.*not.*triggered.*after.*commit'
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: 'auto.commit.*no.*workflow|push.*token.*not.*firing.*CI'
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Workflow not triggered after automated push using GITHUB_TOKEN"
|
|
19
|
+
root_cause: |
|
|
20
|
+
GitHub Actions intentionally prevents new workflow runs from being triggered by events
|
|
21
|
+
performed with the built-in GITHUB_TOKEN. This loopback prevention exists to stop
|
|
22
|
+
accidental or malicious infinite workflow loops — for example, a push workflow that
|
|
23
|
+
commits back to the repo and re-triggers itself in perpetuity.
|
|
24
|
+
|
|
25
|
+
Affected operations when performed with GITHUB_TOKEN:
|
|
26
|
+
- Pushing a commit to a branch (no push event fired; CI does not run on the new SHA)
|
|
27
|
+
- Creating a pull request via REST API (no pull_request event fired; PR checks do not start)
|
|
28
|
+
- Publishing a release (no release event fired)
|
|
29
|
+
- Merging a PR programmatically (no push or pull_request closed event fired)
|
|
30
|
+
|
|
31
|
+
Common surprise scenarios:
|
|
32
|
+
- Auto-version-bump workflows that commit package.json and expect CI to run on the result
|
|
33
|
+
- Automated changelog commits that should trigger a docs publish pipeline
|
|
34
|
+
- Bots that open PRs using GITHUB_TOKEN expecting branch protection checks to start
|
|
35
|
+
- Tag-from-workflow jobs expecting the resulting tag push to start a release pipeline
|
|
36
|
+
fix: |
|
|
37
|
+
To trigger new workflow runs from operations performed inside a workflow, use a Personal
|
|
38
|
+
Access Token (PAT) or a GitHub App installation token instead of GITHUB_TOKEN. Events
|
|
39
|
+
created with these tokens are not subject to the loopback prevention rule.
|
|
40
|
+
|
|
41
|
+
For organizational workflows, GitHub App tokens are the recommended approach — they
|
|
42
|
+
do not rely on a personal user's credentials and can be scoped to minimum permissions.
|
|
43
|
+
|
|
44
|
+
If triggering new runs is NOT needed, add [skip ci] to the commit message to make the
|
|
45
|
+
intent explicit and prevent accidental CI consumption if a PAT is used later.
|
|
46
|
+
fix_code:
|
|
47
|
+
- language: yaml
|
|
48
|
+
label: "PAT-based checkout so the resulting push triggers downstream CI"
|
|
49
|
+
code: |
|
|
50
|
+
jobs:
|
|
51
|
+
version-bump:
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
steps:
|
|
54
|
+
# Check out with PAT — any subsequent push WILL trigger a new push workflow run
|
|
55
|
+
- uses: actions/checkout@v4
|
|
56
|
+
with:
|
|
57
|
+
token: ${{ secrets.MY_PAT }}
|
|
58
|
+
|
|
59
|
+
- name: Bump version
|
|
60
|
+
run: npm version patch --no-git-tag-version
|
|
61
|
+
|
|
62
|
+
- name: Commit and push version bump
|
|
63
|
+
uses: stefanzweifel/git-auto-commit-action@v5
|
|
64
|
+
with:
|
|
65
|
+
commit_message: 'chore: bump version'
|
|
66
|
+
- language: yaml
|
|
67
|
+
label: "GitHub App token so an auto-created PR triggers branch protection checks"
|
|
68
|
+
code: |
|
|
69
|
+
jobs:
|
|
70
|
+
create-pr:
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
steps:
|
|
73
|
+
- name: Get GitHub App token
|
|
74
|
+
id: app-token
|
|
75
|
+
uses: actions/create-github-app-token@v1
|
|
76
|
+
with:
|
|
77
|
+
app-id: ${{ secrets.APP_ID }}
|
|
78
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
79
|
+
|
|
80
|
+
- uses: actions/checkout@v4
|
|
81
|
+
with:
|
|
82
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
83
|
+
|
|
84
|
+
- name: Create PR — App token causes PR checks to be triggered
|
|
85
|
+
uses: peter-evans/create-pull-request@v6
|
|
86
|
+
with:
|
|
87
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
88
|
+
title: 'chore: automated update'
|
|
89
|
+
prevention:
|
|
90
|
+
- "Use a PAT or GitHub App token when downstream workflow runs must be triggered by a commit or PR"
|
|
91
|
+
- "Add [skip ci] to auto-commit messages when downstream CI triggering is explicitly NOT needed"
|
|
92
|
+
- "Document in workflow comments why PAT or App token is used instead of GITHUB_TOKEN"
|
|
93
|
+
- "When debugging missing CI runs after automated commits, check whether GITHUB_TOKEN loopback prevention applies"
|
|
94
|
+
docs:
|
|
95
|
+
- url: "https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow"
|
|
96
|
+
label: "GitHub Docs: Automatic token authentication — events triggered by GITHUB_TOKEN"
|
|
97
|
+
- url: "https://github.com/orgs/community/discussions/27072"
|
|
98
|
+
label: "GitHub Community: Push with GITHUB_TOKEN does not trigger new workflow runs"
|
|
99
|
+
- url: "https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow"
|
|
100
|
+
label: "GitHub Docs: Using a GitHub App token in Actions workflows"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
id: triggers-034
|
|
2
|
+
title: "New workflow file added in a PR branch does not trigger — workflows only activate after merging to the default branch"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: limitation
|
|
5
|
+
tags:
|
|
6
|
+
- triggers
|
|
7
|
+
- new-workflow
|
|
8
|
+
- pr-branch
|
|
9
|
+
- default-branch
|
|
10
|
+
- known-limitation
|
|
11
|
+
- onboarding
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'workflow.*not.*trigger.*pull.request|new.*workflow.*file.*no.*run'
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: '\.github.workflows.*pr.*not.*running|workflow.*added.*branch.*not.*firing'
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "New workflow file added in PR branch produces no workflow runs — the Actions tab shows nothing for the PR"
|
|
19
|
+
- "Workflow YAML exists on PR branch but no jobs are queued for push or pull_request events"
|
|
20
|
+
root_cause: |
|
|
21
|
+
GitHub Actions only executes workflow files that exist on the repository's default
|
|
22
|
+
branch (usually main or master). When a new workflow YAML file is added inside a
|
|
23
|
+
pull request branch, GitHub does NOT execute that workflow for any event on that
|
|
24
|
+
branch — not for the push that added the file, not for subsequent pushes to the branch,
|
|
25
|
+
and not for the pull_request event generated when the PR was opened.
|
|
26
|
+
|
|
27
|
+
This is an intentional security boundary. Executing arbitrary workflow files from
|
|
28
|
+
unreviewed feature branches would allow any contributor to inject CI code that runs
|
|
29
|
+
with write permissions before a maintainer has reviewed it.
|
|
30
|
+
|
|
31
|
+
GitHub's documentation states: "A workflow is triggered by an event that occurs in the
|
|
32
|
+
repository where the workflow file lives." The key constraint is that "lives" means on
|
|
33
|
+
the default branch — not just anywhere in the repository's file tree.
|
|
34
|
+
|
|
35
|
+
Consequences:
|
|
36
|
+
- A PR that adds the first CI workflow for a repo produces zero CI runs
|
|
37
|
+
- A PR adding a new required status check workflow blocks itself forever (the check
|
|
38
|
+
never posts because the workflow doesn't exist on the default branch)
|
|
39
|
+
- New workflow features cannot be validated via CI during the PR review itself
|
|
40
|
+
fix: |
|
|
41
|
+
To test a new workflow file before merging:
|
|
42
|
+
1. Temporarily push just the workflow file directly to the default branch (main/master)
|
|
43
|
+
2. Manually trigger it via workflow_dispatch from the Actions tab to verify it runs correctly
|
|
44
|
+
3. Then open (or reopen) the feature PR — the workflow now exists on the default branch
|
|
45
|
+
and will run against the PR
|
|
46
|
+
|
|
47
|
+
For local testing before any push: use nektos/act to run the workflow locally against
|
|
48
|
+
your branch checkout. This validates the YAML and execution logic without needing the
|
|
49
|
+
file on the default branch.
|
|
50
|
+
|
|
51
|
+
For required status checks: never add a branch protection required status check for a
|
|
52
|
+
workflow that does not yet exist on the default branch — the check will never post a
|
|
53
|
+
result and the PR will be permanently blocked with no path to merge.
|
|
54
|
+
fix_code:
|
|
55
|
+
- language: yaml
|
|
56
|
+
label: "Always include workflow_dispatch to enable manual testing immediately after the first merge"
|
|
57
|
+
code: |
|
|
58
|
+
# New workflow — add workflow_dispatch so it is testable the moment it lands on main
|
|
59
|
+
name: CI
|
|
60
|
+
|
|
61
|
+
on:
|
|
62
|
+
push:
|
|
63
|
+
branches: [main]
|
|
64
|
+
pull_request:
|
|
65
|
+
branches: [main]
|
|
66
|
+
workflow_dispatch: # Allows manual trigger from the Actions tab once merged
|
|
67
|
+
|
|
68
|
+
jobs:
|
|
69
|
+
test:
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v4
|
|
73
|
+
- name: Run tests
|
|
74
|
+
run: echo "workflow is now active on default branch"
|
|
75
|
+
prevention:
|
|
76
|
+
- "Merge a minimal 'smoke test' version of the workflow to the default branch first to activate it, then expand in the feature PR"
|
|
77
|
+
- "Include workflow_dispatch in all new workflows to enable immediate manual testing after the first merge"
|
|
78
|
+
- "Use nektos/act for local workflow testing before pushing — validates logic without needing the file on main"
|
|
79
|
+
- "Never add a required status check for a workflow that does not yet exist on the default branch"
|
|
80
|
+
- "Document in PR description that new workflows require a prior bootstrap merge to main before CI shows results"
|
|
81
|
+
docs:
|
|
82
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow"
|
|
83
|
+
label: "GitHub Docs: Triggering a workflow"
|
|
84
|
+
- url: "https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections"
|
|
85
|
+
label: "GitHub Docs: Security hardening — script injection risks"
|
|
86
|
+
- url: "https://github.com/nektos/act"
|
|
87
|
+
label: "nektos/act — Run GitHub Actions workflows locally"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
id: triggers-035
|
|
2
|
+
title: "push: paths: filter silently prevents workflow from running on tag pushes — tags don't modify files"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- push
|
|
7
|
+
- paths-filter
|
|
8
|
+
- tags
|
|
9
|
+
- silent-skip
|
|
10
|
+
- tag-push
|
|
11
|
+
- release
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'push.*tags.*paths.*not.*trigger|tag.*push.*paths.*filter.*skip'
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: 'release.*workflow.*not.*run.*tag|on\.push.*paths.*tags.*silent'
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Release workflow does not trigger on tag push when push: paths: filter is present"
|
|
19
|
+
- "Tag v1.0.0 pushed but no workflow run created — paths filter silently blocking trigger"
|
|
20
|
+
root_cause: |
|
|
21
|
+
When a GitHub Actions push trigger includes both tags: and paths: conditions, GitHub
|
|
22
|
+
evaluates BOTH conditions using AND logic — the event must match the tag pattern AND
|
|
23
|
+
at least one file in the push must match the paths pattern. A tag push (e.g., v1.0.0)
|
|
24
|
+
creates a new ref pointing to an existing commit without modifying any file contents.
|
|
25
|
+
Because no files are changed, the paths: filter never matches, and the workflow
|
|
26
|
+
silently does not run.
|
|
27
|
+
|
|
28
|
+
Example trigger block that silently fails on all tag pushes:
|
|
29
|
+
|
|
30
|
+
on:
|
|
31
|
+
push:
|
|
32
|
+
branches: [main]
|
|
33
|
+
tags: ['v*']
|
|
34
|
+
paths: ['src/**', 'package.json'] # blocks ALL tag triggers — tags change no files
|
|
35
|
+
|
|
36
|
+
YAML does not support duplicate map keys, so two separate on: push: blocks are not
|
|
37
|
+
possible in the same workflow file. The paths: filter applies globally to every push
|
|
38
|
+
event evaluated by that trigger block, including tag pushes.
|
|
39
|
+
|
|
40
|
+
This is commonly introduced when developers add a paths: filter to an existing release
|
|
41
|
+
workflow to avoid running expensive builds on documentation commits, inadvertently
|
|
42
|
+
also filtering out all tag-triggered release runs. The workflow appears to work on
|
|
43
|
+
branch pushes (which do change files) while silently never running on tag pushes.
|
|
44
|
+
fix: |
|
|
45
|
+
Option 1 (recommended): Replace push: tags: with the release event. The release event
|
|
46
|
+
fires when a GitHub Release is published from a tag and has no paths filter concept.
|
|
47
|
+
This also provides richer release metadata (release notes, asset URLs) via github.event.
|
|
48
|
+
|
|
49
|
+
Option 2: Remove paths: from the trigger and use an if: condition on jobs to skip
|
|
50
|
+
non-relevant branch pushes. The job runs for all tag pushes unconditionally, and
|
|
51
|
+
runs for branch pushes only when the if: condition evaluates to true based on
|
|
52
|
+
changed file data or event type.
|
|
53
|
+
|
|
54
|
+
Option 3: Use paths-ignore: instead of paths: with an inverted pattern. This approach
|
|
55
|
+
still silently blocks tag pushes (same AND-logic issue), so it should only be combined
|
|
56
|
+
with Option 1 or 2.
|
|
57
|
+
fix_code:
|
|
58
|
+
- language: yaml
|
|
59
|
+
label: "Replace push: tags: with release event — no paths filter interference"
|
|
60
|
+
code: |
|
|
61
|
+
on:
|
|
62
|
+
push:
|
|
63
|
+
branches: [main]
|
|
64
|
+
paths:
|
|
65
|
+
- 'src/**'
|
|
66
|
+
- 'package.json'
|
|
67
|
+
# Use release event for tag-based release workflows
|
|
68
|
+
# Fires on Release published — no paths filter concept applies
|
|
69
|
+
release:
|
|
70
|
+
types: [published]
|
|
71
|
+
|
|
72
|
+
jobs:
|
|
73
|
+
build-and-release:
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v4
|
|
77
|
+
- run: echo "runs on branch push matching paths AND on release publish"
|
|
78
|
+
- language: yaml
|
|
79
|
+
label: "Remove paths: and guard branch pushes with if: condition instead"
|
|
80
|
+
code: |
|
|
81
|
+
on:
|
|
82
|
+
push:
|
|
83
|
+
branches: [main]
|
|
84
|
+
tags: ['v*.*.*']
|
|
85
|
+
# No paths: filter — use if: condition on job to skip irrelevant branch pushes
|
|
86
|
+
|
|
87
|
+
jobs:
|
|
88
|
+
release:
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
# Always run on tag pushes; on branch pushes only if relevant files changed
|
|
91
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
92
|
+
steps:
|
|
93
|
+
- uses: actions/checkout@v4
|
|
94
|
+
- run: echo "only runs on tag pushes"
|
|
95
|
+
|
|
96
|
+
ci:
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
# Separate job for branch-push CI with file-change guard
|
|
99
|
+
if: "!startsWith(github.ref, 'refs/tags/')"
|
|
100
|
+
steps:
|
|
101
|
+
- uses: actions/checkout@v4
|
|
102
|
+
- run: echo "only runs on branch pushes"
|
|
103
|
+
prevention:
|
|
104
|
+
- "Never mix tags: and paths: in the same push trigger block — paths filter applies to tag pushes too"
|
|
105
|
+
- "Use the release event instead of push: tags: for release workflows to avoid paths filter confusion"
|
|
106
|
+
- "After adding a paths: filter to any workflow, test by pushing a tag to verify release triggering still works"
|
|
107
|
+
- "Add a comment near any paths: filter warning that it will also suppress tag-push triggers in the same block"
|
|
108
|
+
docs:
|
|
109
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#push"
|
|
110
|
+
label: "GitHub Docs: push event trigger and filters"
|
|
111
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#release"
|
|
112
|
+
label: "GitHub Docs: release event trigger"
|
|
113
|
+
- url: "https://github.com/orgs/community/discussions/25597"
|
|
114
|
+
label: "GitHub Community #25597: paths filter prevents tag-triggered workflow runs"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
id: yaml-syntax-035
|
|
2
|
+
title: "Unquoted YAML value starting with ${{ expression }} causes parse error — { triggers flow mapping syntax"
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- yaml
|
|
7
|
+
- expressions
|
|
8
|
+
- quoting
|
|
9
|
+
- parse-error
|
|
10
|
+
- flow-mapping
|
|
11
|
+
- workflow-syntax
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'mapping values are not allowed|did not find expected key'
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: 'invalid workflow file.*yaml|yaml.*parse.*error.*expression'
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Invalid workflow file: .github/workflows/workflow.yml#L12: mapping values are not allowed in this context"
|
|
19
|
+
- "You have an error in your yaml syntax on line X — expected a scalar value but found a mapping"
|
|
20
|
+
root_cause: |
|
|
21
|
+
The YAML specification defines { as the start of a flow mapping (an inline key-value
|
|
22
|
+
dictionary). GitHub Actions expressions begin with ${{ — which contains { as the second
|
|
23
|
+
character. When a YAML scalar value starts with ${{ without surrounding quotes, most
|
|
24
|
+
YAML parsers interpret the opening { as the beginning of a flow mapping and produce a
|
|
25
|
+
parse error.
|
|
26
|
+
|
|
27
|
+
The YAML rule: any scalar value that would be ambiguous with YAML structure characters
|
|
28
|
+
(including { [ : > |) must be quoted. An unquoted value starting with ${{ violates
|
|
29
|
+
this rule.
|
|
30
|
+
|
|
31
|
+
Affected contexts:
|
|
32
|
+
- env: block values: MY_VAR: ${{ secrets.TOKEN }} ← parse error
|
|
33
|
+
- with: block inputs: version: ${{ inputs.version }} ← parse error
|
|
34
|
+
- name: step names: name: ${{ matrix.os }} build ← parse error in some parsers
|
|
35
|
+
- run: inline shell is safe — run: is a block scalar, expressions inside are not YAML values
|
|
36
|
+
|
|
37
|
+
The if: condition is a special case. GitHub Actions parses if: fields with implicit
|
|
38
|
+
expression wrapping, so if: github.ref == 'refs/heads/main' works without ${{ }}.
|
|
39
|
+
However, if: ${{ condition }} also works when quoted. Unquoted if: ${{ ... }} may
|
|
40
|
+
work in permissive parsers but is not spec-compliant.
|
|
41
|
+
|
|
42
|
+
actionlint and yamllint both flag unquoted expressions as errors. GitHub's own
|
|
43
|
+
workflow syntax checker may silently accept some cases while rejecting others,
|
|
44
|
+
making the error inconsistent across different fields.
|
|
45
|
+
fix: |
|
|
46
|
+
Quote any YAML value that starts with ${{ using either single or double quotes.
|
|
47
|
+
Double quotes are standard; single quotes work equally well and may be preferred
|
|
48
|
+
when the expression contains double quote characters.
|
|
49
|
+
|
|
50
|
+
For multiline shell commands (run: | blocks), expressions inside the block scalar
|
|
51
|
+
do not need quoting — they are not parsed as YAML values.
|
|
52
|
+
|
|
53
|
+
For if: conditions, omit the ${{ }} wrapper entirely — GitHub Actions implicitly
|
|
54
|
+
wraps if: values in expression evaluation. Use: if: github.ref == 'refs/heads/main'
|
|
55
|
+
not if: ${{ github.ref == 'refs/heads/main' }}.
|
|
56
|
+
fix_code:
|
|
57
|
+
- language: yaml
|
|
58
|
+
label: "Quote YAML values starting with ${{ to prevent YAML parse errors"
|
|
59
|
+
code: |
|
|
60
|
+
# Incorrect — unquoted values starting with ${{ cause YAML parse errors
|
|
61
|
+
jobs:
|
|
62
|
+
bad:
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
env:
|
|
65
|
+
MY_VAR: ${{ secrets.MY_SECRET }} # parse error
|
|
66
|
+
|
|
67
|
+
# Correct — double-quoted (standard)
|
|
68
|
+
jobs:
|
|
69
|
+
good-double:
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
env:
|
|
72
|
+
MY_VAR: "${{ secrets.MY_SECRET }}" # safe
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/setup-node@v4
|
|
75
|
+
with:
|
|
76
|
+
node-version: "${{ inputs.node-version }}"
|
|
77
|
+
|
|
78
|
+
# Also correct — single-quoted
|
|
79
|
+
jobs:
|
|
80
|
+
good-single:
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
env:
|
|
83
|
+
MY_VAR: '${{ secrets.MY_SECRET }}' # safe
|
|
84
|
+
|
|
85
|
+
# run: block scalar — no quoting needed inside |
|
|
86
|
+
jobs:
|
|
87
|
+
run-block:
|
|
88
|
+
runs-on: ubuntu-latest
|
|
89
|
+
steps:
|
|
90
|
+
- name: Show SHA
|
|
91
|
+
run: |
|
|
92
|
+
echo ${{ github.sha }} # safe — inside block scalar
|
|
93
|
+
|
|
94
|
+
# if: — use bare expression without ${{ }} wrapper
|
|
95
|
+
jobs:
|
|
96
|
+
guarded:
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
steps:
|
|
99
|
+
- name: Step only on main
|
|
100
|
+
if: github.ref == 'refs/heads/main' # preferred — no ${{ }} needed
|
|
101
|
+
run: echo "on main"
|
|
102
|
+
prevention:
|
|
103
|
+
- "Quote all YAML values that begin with ${{ — use double or single quotes consistently"
|
|
104
|
+
- "Install actionlint as a pre-commit hook to catch unquoted expression errors before push"
|
|
105
|
+
- "Use block scalar (|) for multi-line run: steps — no expression quoting needed inside block scalars"
|
|
106
|
+
- "For if: conditions, write bare expressions without the ${{ }} wrapper"
|
|
107
|
+
- "Enable GitHub's built-in YAML syntax check — push to a test branch and review the Actions tab immediately"
|
|
108
|
+
docs:
|
|
109
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions"
|
|
110
|
+
label: "GitHub Docs: Workflow syntax for GitHub Actions"
|
|
111
|
+
- url: "https://rhysd.github.io/actionlint/"
|
|
112
|
+
label: "actionlint — static checker for GitHub Actions workflow files"
|
|
113
|
+
- url: "https://yaml.org/spec/1.2.2/#flow-mappings"
|
|
114
|
+
label: "YAML 1.2 spec: Flow mappings"
|
package/package.json
CHANGED