@htekdev/actions-debugger 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +108 -108
- package/errors/_schema.json +89 -89
- package/errors/caching-artifacts/artifact-storage-quota-exceeded.yml +118 -0
- package/errors/caching-artifacts/cache-miss.yml +56 -56
- package/errors/caching-artifacts/cache-save-cancelled-job.yml +82 -0
- package/errors/caching-artifacts/cache-v3-to-v4-breaking-changes.yml +95 -0
- package/errors/caching-artifacts/cross-repo-artifacts-not-supported.yml +102 -0
- package/errors/caching-artifacts/upload-artifact-no-files-found.yml +92 -0
- package/errors/caching-artifacts/upload-artifact-v4-breaking.yml +67 -67
- package/errors/concurrency-timing/cancel-in-progress-deploy-drops.yml +97 -0
- package/errors/concurrency-timing/jobs-cancelled-unexpectedly.yml +60 -60
- package/errors/concurrency-timing/skipped-needs-cascade.yml +103 -0
- package/errors/concurrency-timing/workflow-run-conclusion-unchecked.yml +100 -0
- package/errors/known-unsolved/composite-input-env-vars-missing.yml +91 -0
- package/errors/known-unsolved/composite-nested-outputs-null.yml +101 -0
- package/errors/known-unsolved/no-dynamic-secret-access.yml +111 -0
- package/errors/known-unsolved/no-step-level-rerun.yml +94 -0
- package/errors/known-unsolved/no-step-retry.yml +53 -53
- package/errors/known-unsolved/workflow-rerun-limit.yml +101 -0
- package/errors/permissions-auth/checkout-submodule-private-auth.yml +91 -0
- package/errors/permissions-auth/fork-pr-secrets-unavailable.yml +97 -0
- package/errors/permissions-auth/gcp-oidc-workload-identity-misconfigured.yml +130 -0
- package/errors/permissions-auth/github-token-403.yml +64 -64
- package/errors/permissions-auth/github-token-protected-branch-push.yml +109 -0
- package/errors/permissions-auth/oidc-aws-failure.yml +85 -85
- package/errors/permissions-auth/oidc-azure-subject-mismatch.yml +91 -0
- package/errors/runner-environment/disk-space.yml +57 -57
- package/errors/runner-environment/docker-buildx-not-setup.yml +106 -0
- package/errors/runner-environment/macos-homebrew-path.yml +90 -0
- package/errors/runner-environment/node-runtime-deprecation.yml +56 -56
- package/errors/runner-environment/node20-to-node24-migration.yml +118 -0
- package/errors/runner-environment/npm-ci-lockfile-mismatch.yml +112 -0
- package/errors/runner-environment/self-hosted-stale-toolcache.yml +73 -0
- package/errors/runner-environment/setup-node-version-file-missing.yml +105 -0
- package/errors/runner-environment/windows-execution-policy.yml +83 -0
- package/errors/silent-failures/add-mask-no-retroactive-masking.yml +75 -0
- package/errors/silent-failures/composite-boolean-inputs-as-strings.yml +110 -0
- package/errors/silent-failures/conditional-output-null-downstream.yml +82 -0
- package/errors/silent-failures/continue-on-error-masks-failure.yml +86 -0
- package/errors/silent-failures/github-token-no-trigger.yml +57 -57
- package/errors/silent-failures/reusable-workflow-env-secrets-empty.yml +90 -0
- package/errors/silent-failures/scheduled-workflow-disabled.yml +59 -59
- package/errors/silent-failures/sparse-checkout-sticky-cone-mode.yml +120 -0
- package/errors/triggers/cron-schedule-late.yml +59 -59
- package/errors/triggers/pull-request-target-rce-risk.yml +117 -0
- package/errors/triggers/workflow-not-triggering.yml +60 -60
- package/errors/triggers/workflow-run-default-branch-requirement.yml +78 -0
- package/errors/yaml-syntax/anchors-not-supported.yml +95 -0
- package/errors/yaml-syntax/dynamic-matrix-fromjson-failure.yml +99 -0
- package/errors/yaml-syntax/if-always-true.yml +52 -52
- package/errors/yaml-syntax/missing-expression-wrapper.yml +67 -0
- package/errors/yaml-syntax/needs-indirect-outputs.yml +91 -0
- package/errors/yaml-syntax/reusable-workflow-missing-output-declaration.yml +140 -0
- package/errors/yaml-syntax/secrets-in-if.yml +55 -55
- package/errors/yaml-syntax/unexpected-yaml-key.yml +69 -69
- package/errors/yaml-syntax/working-directory-ignored-on-uses.yml +66 -0
- package/package.json +70 -67
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
id: triggers-005
|
|
2
|
+
title: "pull_request_target Runs Fork Code with Base Branch Secrets — RCE Risk"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- pull_request_target
|
|
7
|
+
- security
|
|
8
|
+
- fork
|
|
9
|
+
- secrets
|
|
10
|
+
- code-injection
|
|
11
|
+
- pwn-request
|
|
12
|
+
- base-branch
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: "pull_request_target"
|
|
15
|
+
flags: "i"
|
|
16
|
+
- regex: "github\\.event\\.pull_request\\.head\\.sha"
|
|
17
|
+
flags: "i"
|
|
18
|
+
error_messages:
|
|
19
|
+
- "Warning: Workflow uses pull_request_target with actions/checkout on PR head — potential code injection vulnerability."
|
|
20
|
+
root_cause: |
|
|
21
|
+
`pull_request_target` was designed to give workflows triggered by fork PRs access to
|
|
22
|
+
**base branch secrets and write permissions**. Unlike `pull_request`, it runs in the
|
|
23
|
+
context of the **base repository**, not the fork — so `secrets.*` is available and
|
|
24
|
+
`GITHUB_TOKEN` has write permissions.
|
|
25
|
+
|
|
26
|
+
The danger: many developers combine `pull_request_target` with
|
|
27
|
+
`actions/checkout@v4 ref: ${{ github.event.pull_request.head.sha }}` to check out the
|
|
28
|
+
**fork's code** for testing. Running untrusted fork code with full secret access is a
|
|
29
|
+
critical Remote Code Execution (RCE) vulnerability — the contributor can exfiltrate
|
|
30
|
+
all repository secrets simply by adding malicious code to their PR.
|
|
31
|
+
|
|
32
|
+
This class of vulnerability is known as "pwn request" and has been exploited against
|
|
33
|
+
major open-source projects.
|
|
34
|
+
fix: |
|
|
35
|
+
When using `pull_request_target`, ALWAYS check out the base branch (not the PR head).
|
|
36
|
+
If you need to run the PR author's code AND have secret access, use a two-workflow pattern:
|
|
37
|
+
an unprivileged `pull_request` workflow builds and uploads artifacts, then a privileged
|
|
38
|
+
`workflow_run` workflow (with no access to fork code) deploys them.
|
|
39
|
+
fix_code:
|
|
40
|
+
- language: yaml
|
|
41
|
+
label: "CRITICAL VULNERABILITY — checkout of fork code with secret access"
|
|
42
|
+
code: |
|
|
43
|
+
on: pull_request_target # has secret access
|
|
44
|
+
|
|
45
|
+
jobs:
|
|
46
|
+
test:
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
# DANGEROUS: checking out untrusted PR code with base repo secrets
|
|
50
|
+
- uses: actions/checkout@v4
|
|
51
|
+
with:
|
|
52
|
+
ref: ${{ github.event.pull_request.head.sha }} # NEVER DO THIS
|
|
53
|
+
- run: npm test # attacker's code runs with your secrets
|
|
54
|
+
- language: yaml
|
|
55
|
+
label: "SAFE — pull_request_target checking out base only (for labels/comments)"
|
|
56
|
+
code: |
|
|
57
|
+
on:
|
|
58
|
+
pull_request_target:
|
|
59
|
+
types: [opened, labeled]
|
|
60
|
+
|
|
61
|
+
jobs:
|
|
62
|
+
label:
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
permissions:
|
|
65
|
+
pull-requests: write
|
|
66
|
+
steps:
|
|
67
|
+
# SAFE: checking out base repo code only (not the PR branch)
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
# No 'ref:' — defaults to base branch
|
|
70
|
+
- name: Add label
|
|
71
|
+
uses: actions-ecosystem/action-add-labels@v1
|
|
72
|
+
with:
|
|
73
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
74
|
+
labels: needs-review
|
|
75
|
+
- language: yaml
|
|
76
|
+
label: "SAFE — two-workflow pattern: build unprivileged, deploy privileged"
|
|
77
|
+
code: |
|
|
78
|
+
# Workflow 1: ci.yml — triggered by pull_request (no secrets, untrusted code OK)
|
|
79
|
+
on: pull_request
|
|
80
|
+
jobs:
|
|
81
|
+
build:
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/checkout@v4 # fork code, but no secrets
|
|
85
|
+
- run: npm ci && npm run build
|
|
86
|
+
- uses: actions/upload-artifact@v4
|
|
87
|
+
with:
|
|
88
|
+
name: build-output
|
|
89
|
+
path: dist/
|
|
90
|
+
|
|
91
|
+
# Workflow 2: deploy-pr-preview.yml — triggered by workflow_run (has secrets, no fork code)
|
|
92
|
+
on:
|
|
93
|
+
workflow_run:
|
|
94
|
+
workflows: [CI]
|
|
95
|
+
types: [completed]
|
|
96
|
+
jobs:
|
|
97
|
+
deploy:
|
|
98
|
+
runs-on: ubuntu-latest
|
|
99
|
+
if: github.event.workflow_run.conclusion == 'success'
|
|
100
|
+
steps:
|
|
101
|
+
- uses: actions/download-artifact@v4 # downloads artifact, not fork code
|
|
102
|
+
with:
|
|
103
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
104
|
+
run-id: ${{ github.event.workflow_run.id }}
|
|
105
|
+
name: build-output
|
|
106
|
+
- run: ./deploy-preview.sh # safe: runs base repo script with downloaded artifact
|
|
107
|
+
prevention:
|
|
108
|
+
- "NEVER use `actions/checkout ref: github.event.pull_request.head.sha` inside a `pull_request_target` workflow."
|
|
109
|
+
- "Use the two-workflow pattern (unprivileged `pull_request` + privileged `workflow_run`) for PR preview deployments."
|
|
110
|
+
- "Restrict `pull_request_target` to safe operations: labeling, commenting, and running base repo scripts only."
|
|
111
|
+
docs:
|
|
112
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request_target"
|
|
113
|
+
label: "pull_request_target event"
|
|
114
|
+
- url: "https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/"
|
|
115
|
+
label: "Preventing pwn requests (GitHub Security Lab)"
|
|
116
|
+
- url: "https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections"
|
|
117
|
+
label: "Security hardening — script injection"
|
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
id: triggers-001
|
|
2
|
-
title: "Workflow Not Triggering At All"
|
|
3
|
-
category: triggers
|
|
4
|
-
severity: error
|
|
5
|
-
tags:
|
|
6
|
-
- triggers
|
|
7
|
-
- workflow-file
|
|
8
|
-
- default-branch
|
|
9
|
-
- on
|
|
10
|
-
- events
|
|
11
|
-
patterns:
|
|
12
|
-
- regex: "This event will only trigger a workflow run if the workflow file exists on the default branch"
|
|
13
|
-
flags: "i"
|
|
14
|
-
- regex: "Workflow file .* not found on the default branch"
|
|
15
|
-
flags: "i"
|
|
16
|
-
- regex: "No runs found for this workflow"
|
|
17
|
-
flags: "i"
|
|
18
|
-
error_messages:
|
|
19
|
-
- "This event will only trigger a workflow run if the workflow file exists on the default branch"
|
|
20
|
-
root_cause: |
|
|
21
|
-
Some events only trigger if the workflow file itself exists on the repository's default
|
|
22
|
-
branch. A workflow can look perfectly valid in a feature branch, but GitHub will ignore it
|
|
23
|
-
for certain event types until the file is present on the default branch with correct `on:`
|
|
24
|
-
syntax and placement under `.github/workflows/`.
|
|
25
|
-
|
|
26
|
-
A wrong indentation level under `on:`, a typo in the event name, or putting the file in the
|
|
27
|
-
wrong directory can create the same symptom: nothing runs.
|
|
28
|
-
fix: |
|
|
29
|
-
Confirm the workflow file lives in `.github/workflows/`, validate the `on:` block, and make
|
|
30
|
-
sure the workflow file already exists on the default branch for the event you expect to fire.
|
|
31
|
-
fix_code:
|
|
32
|
-
- language: yaml
|
|
33
|
-
label: "Use valid workflow location and trigger syntax"
|
|
34
|
-
code: |
|
|
35
|
-
name: CI
|
|
36
|
-
|
|
37
|
-
on:
|
|
38
|
-
push:
|
|
39
|
-
branches:
|
|
40
|
-
- main
|
|
41
|
-
pull_request:
|
|
42
|
-
|
|
43
|
-
jobs:
|
|
44
|
-
test:
|
|
45
|
-
runs-on: ubuntu-latest
|
|
46
|
-
steps:
|
|
47
|
-
- uses: actions/checkout@v4
|
|
48
|
-
- run: npm test
|
|
49
|
-
prevention:
|
|
50
|
-
- "Keep workflow files only under `.github/workflows/`."
|
|
51
|
-
- "Merge new workflow files to the default branch before relying on events like `schedule` or `workflow_dispatch`."
|
|
52
|
-
- "Validate event names and indentation in every `on:` block."
|
|
53
|
-
docs:
|
|
54
|
-
- url: "https://docs.github.com/en/actions/reference/events-that-trigger-workflows"
|
|
55
|
-
label: "Events that trigger workflows"
|
|
56
|
-
- url: "https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions"
|
|
57
|
-
label: "Workflow syntax for GitHub Actions"
|
|
58
|
-
source:
|
|
59
|
-
article: "https://htek.dev/articles/github-actions-debugging-guide"
|
|
60
|
-
section: "Workflow not triggering"
|
|
1
|
+
id: triggers-001
|
|
2
|
+
title: "Workflow Not Triggering At All"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- triggers
|
|
7
|
+
- workflow-file
|
|
8
|
+
- default-branch
|
|
9
|
+
- on
|
|
10
|
+
- events
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: "This event will only trigger a workflow run if the workflow file exists on the default branch"
|
|
13
|
+
flags: "i"
|
|
14
|
+
- regex: "Workflow file .* not found on the default branch"
|
|
15
|
+
flags: "i"
|
|
16
|
+
- regex: "No runs found for this workflow"
|
|
17
|
+
flags: "i"
|
|
18
|
+
error_messages:
|
|
19
|
+
- "This event will only trigger a workflow run if the workflow file exists on the default branch"
|
|
20
|
+
root_cause: |
|
|
21
|
+
Some events only trigger if the workflow file itself exists on the repository's default
|
|
22
|
+
branch. A workflow can look perfectly valid in a feature branch, but GitHub will ignore it
|
|
23
|
+
for certain event types until the file is present on the default branch with correct `on:`
|
|
24
|
+
syntax and placement under `.github/workflows/`.
|
|
25
|
+
|
|
26
|
+
A wrong indentation level under `on:`, a typo in the event name, or putting the file in the
|
|
27
|
+
wrong directory can create the same symptom: nothing runs.
|
|
28
|
+
fix: |
|
|
29
|
+
Confirm the workflow file lives in `.github/workflows/`, validate the `on:` block, and make
|
|
30
|
+
sure the workflow file already exists on the default branch for the event you expect to fire.
|
|
31
|
+
fix_code:
|
|
32
|
+
- language: yaml
|
|
33
|
+
label: "Use valid workflow location and trigger syntax"
|
|
34
|
+
code: |
|
|
35
|
+
name: CI
|
|
36
|
+
|
|
37
|
+
on:
|
|
38
|
+
push:
|
|
39
|
+
branches:
|
|
40
|
+
- main
|
|
41
|
+
pull_request:
|
|
42
|
+
|
|
43
|
+
jobs:
|
|
44
|
+
test:
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v4
|
|
48
|
+
- run: npm test
|
|
49
|
+
prevention:
|
|
50
|
+
- "Keep workflow files only under `.github/workflows/`."
|
|
51
|
+
- "Merge new workflow files to the default branch before relying on events like `schedule` or `workflow_dispatch`."
|
|
52
|
+
- "Validate event names and indentation in every `on:` block."
|
|
53
|
+
docs:
|
|
54
|
+
- url: "https://docs.github.com/en/actions/reference/events-that-trigger-workflows"
|
|
55
|
+
label: "Events that trigger workflows"
|
|
56
|
+
- url: "https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions"
|
|
57
|
+
label: "Workflow syntax for GitHub Actions"
|
|
58
|
+
source:
|
|
59
|
+
article: "https://htek.dev/articles/github-actions-debugging-guide"
|
|
60
|
+
section: "Workflow not triggering"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
id: triggers-004
|
|
2
|
+
title: "workflow_run Trigger Fires Only When Source Workflow Exists on Default Branch"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- workflow_run
|
|
7
|
+
- triggers
|
|
8
|
+
- default-branch
|
|
9
|
+
- main
|
|
10
|
+
- workflow-file
|
|
11
|
+
- not-triggering
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "workflow_run.*not triggering"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "workflow_run.*workflow.*not found"
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "The workflow 'CI' referenced by workflow_run trigger was not found or does not exist on the default branch."
|
|
19
|
+
root_cause: |
|
|
20
|
+
GitHub resolves `workflow_run` triggers by looking up the **source workflow's file name
|
|
21
|
+
on the repository's default branch** at trigger time. This has two important implications:
|
|
22
|
+
|
|
23
|
+
1. **The source workflow file must exist on the default branch** — a `workflow_run` trigger
|
|
24
|
+
in a PR branch will not fire when the referenced workflow runs on that branch, if the
|
|
25
|
+
source workflow file doesn't exist on `main`/`master` yet.
|
|
26
|
+
|
|
27
|
+
2. **The trigger workflow itself must also be on the default branch** — the `workflow_run`
|
|
28
|
+
trigger in `on:` is only read from the default branch, regardless of which branch
|
|
29
|
+
the triggering push happened on.
|
|
30
|
+
|
|
31
|
+
This is a common blocker during initial feature development: a developer adds both the
|
|
32
|
+
source workflow and the `workflow_run` consumer in the same PR branch. Neither fires
|
|
33
|
+
correctly until both files are merged to the default branch.
|
|
34
|
+
fix: |
|
|
35
|
+
Ensure both the source workflow and the `workflow_run` consumer exist on the default
|
|
36
|
+
branch before testing. When adding new CI workflows, merge to main/master first, then
|
|
37
|
+
test from a feature branch.
|
|
38
|
+
fix_code:
|
|
39
|
+
- language: yaml
|
|
40
|
+
label: "Example workflow_run trigger — source must be on default branch"
|
|
41
|
+
code: |
|
|
42
|
+
# .github/workflows/deploy.yml (must be on default branch to work)
|
|
43
|
+
on:
|
|
44
|
+
workflow_run:
|
|
45
|
+
workflows: ["CI"] # "CI" must match the 'name:' field in ci.yml
|
|
46
|
+
types: [completed] # fires when CI workflow completes (any branch)
|
|
47
|
+
branches: [main] # optional: only when CI ran on these branches
|
|
48
|
+
|
|
49
|
+
jobs:
|
|
50
|
+
deploy:
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
53
|
+
steps:
|
|
54
|
+
- run: ./deploy.sh
|
|
55
|
+
- language: yaml
|
|
56
|
+
label: "Check workflow name matches exactly (case-sensitive)"
|
|
57
|
+
code: |
|
|
58
|
+
# .github/workflows/ci.yml
|
|
59
|
+
name: CI # this exact string must match workflow_run trigger
|
|
60
|
+
|
|
61
|
+
on:
|
|
62
|
+
push:
|
|
63
|
+
branches: [main, 'feature/**']
|
|
64
|
+
|
|
65
|
+
jobs:
|
|
66
|
+
build:
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
steps:
|
|
69
|
+
- run: npm test
|
|
70
|
+
prevention:
|
|
71
|
+
- "Merge the source workflow to the default branch before adding a `workflow_run` consumer — test with both files on `main`."
|
|
72
|
+
- "The `workflows:` list matches on the workflow's `name:` field (the YAML `name:` key), not the filename."
|
|
73
|
+
- "Use `branches:` filter in `workflow_run` to prevent deploy from triggering on every feature branch run."
|
|
74
|
+
docs:
|
|
75
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_run"
|
|
76
|
+
label: "workflow_run trigger"
|
|
77
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow#triggering-a-workflow-from-a-workflow"
|
|
78
|
+
label: "Triggering a workflow from a workflow"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
id: yaml-syntax-009
|
|
2
|
+
title: "YAML Anchors and Aliases Rejected by Workflow Parser"
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- yaml
|
|
7
|
+
- anchors
|
|
8
|
+
- aliases
|
|
9
|
+
- dry
|
|
10
|
+
- reuse
|
|
11
|
+
- syntax
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "Anchors are not supported"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "could not parse the yaml file"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "mapping values are not allowed in this context"
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "The workflow is not valid. .github/workflows/ci.yml: Anchors are not supported."
|
|
21
|
+
- "could not parse the yaml file '.github/workflows/ci.yml'"
|
|
22
|
+
root_cause: |
|
|
23
|
+
GitHub Actions' workflow parser explicitly rejects YAML anchors (`&`) and aliases (`*`)
|
|
24
|
+
even though they are valid YAML 1.2 constructs. Developers familiar with Docker Compose
|
|
25
|
+
or Kubernetes manifests commonly attempt to DRY up repetitive step blocks using anchors
|
|
26
|
+
and merge keys (`<<: *defaults`), but the GitHub-hosted workflow validator rejects the
|
|
27
|
+
file on parse rather than silently ignoring the constructs.
|
|
28
|
+
|
|
29
|
+
The restriction is documented and intentional — GitHub's parser does not implement the
|
|
30
|
+
full YAML 1.2 merge-key extension.
|
|
31
|
+
fix: |
|
|
32
|
+
Remove all YAML anchors and aliases from workflow files. Use one of GitHub Actions'
|
|
33
|
+
native reuse mechanisms instead: composite actions for shared step sequences, reusable
|
|
34
|
+
workflows (`workflow_call`) for sharing entire job graphs, or a matrix strategy for
|
|
35
|
+
parameterized repetition.
|
|
36
|
+
fix_code:
|
|
37
|
+
- language: yaml
|
|
38
|
+
label: "WRONG — YAML anchor and merge key (rejected)"
|
|
39
|
+
code: |
|
|
40
|
+
.defaults: &defaults
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
timeout-minutes: 10
|
|
43
|
+
|
|
44
|
+
jobs:
|
|
45
|
+
build:
|
|
46
|
+
<<: *defaults
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
- run: npm ci && npm run build
|
|
50
|
+
|
|
51
|
+
test:
|
|
52
|
+
<<: *defaults
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
- run: npm test
|
|
56
|
+
- language: yaml
|
|
57
|
+
label: "CORRECT — composite action for shared steps"
|
|
58
|
+
code: |
|
|
59
|
+
# .github/actions/setup/action.yml
|
|
60
|
+
name: Setup
|
|
61
|
+
runs:
|
|
62
|
+
using: composite
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/checkout@v4
|
|
65
|
+
- uses: actions/setup-node@v4
|
|
66
|
+
with:
|
|
67
|
+
node-version: 20
|
|
68
|
+
cache: npm
|
|
69
|
+
|
|
70
|
+
# .github/workflows/ci.yml
|
|
71
|
+
jobs:
|
|
72
|
+
build:
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
timeout-minutes: 10
|
|
75
|
+
steps:
|
|
76
|
+
- uses: ./.github/actions/setup
|
|
77
|
+
- run: npm run build
|
|
78
|
+
|
|
79
|
+
test:
|
|
80
|
+
runs-on: ubuntu-latest
|
|
81
|
+
timeout-minutes: 10
|
|
82
|
+
steps:
|
|
83
|
+
- uses: ./.github/actions/setup
|
|
84
|
+
- run: npm test
|
|
85
|
+
prevention:
|
|
86
|
+
- "Use composite actions (`.github/actions/`) to share step sequences across jobs."
|
|
87
|
+
- "Use reusable workflows (`workflow_call`) to share entire job structures."
|
|
88
|
+
- "Lint workflow files with `actionlint` locally — it reports anchor usage before pushing."
|
|
89
|
+
docs:
|
|
90
|
+
- url: "https://docs.github.com/en/actions/sharing-automations/reusing-workflows"
|
|
91
|
+
label: "Reusing workflows"
|
|
92
|
+
- url: "https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-composite-action"
|
|
93
|
+
label: "Creating a composite action"
|
|
94
|
+
- url: "https://rhysd.github.io/actionlint/"
|
|
95
|
+
label: "actionlint — static checker for GitHub Actions"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
id: yaml-syntax-008
|
|
2
|
+
title: "Dynamic Matrix fromJSON Fails on Empty or Malformed JSON"
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- matrix
|
|
7
|
+
- fromJSON
|
|
8
|
+
- dynamic-matrix
|
|
9
|
+
- strategy
|
|
10
|
+
- expressions
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: "Error when evaluating 'strategy' for job"
|
|
13
|
+
flags: "i"
|
|
14
|
+
- regex: "Error parsing fromJson"
|
|
15
|
+
flags: "i"
|
|
16
|
+
- regex: "Error reading JToken from JsonReader"
|
|
17
|
+
flags: "i"
|
|
18
|
+
- regex: "matrix contains zero elements"
|
|
19
|
+
flags: "i"
|
|
20
|
+
- regex: "Unexpected type of value '', expected type: Sequence"
|
|
21
|
+
flags: "i"
|
|
22
|
+
error_messages:
|
|
23
|
+
- "Error when evaluating 'strategy' for job 'build'. .github/workflows/ci.yml (Line: 12, Col: 14): Error parsing fromJson"
|
|
24
|
+
- "Error reading JToken from JsonReader. Path '', line 0, position 0."
|
|
25
|
+
- "Unexpected type of value '', expected type: Sequence."
|
|
26
|
+
- "Error when evaluating 'strategy' for job 'test'. matrix contains zero elements"
|
|
27
|
+
root_cause: |
|
|
28
|
+
When using `fromJSON()` in a matrix strategy to build a dynamic matrix from a previous
|
|
29
|
+
job's output, three common failure modes occur:
|
|
30
|
+
|
|
31
|
+
1. **Empty string**: The upstream step produced no output (empty stdout or unset output
|
|
32
|
+
variable), so `fromJSON('')` receives an empty string and the JSON parser fails with
|
|
33
|
+
"Error reading JToken from JsonReader."
|
|
34
|
+
|
|
35
|
+
2. **Zero-element array**: The upstream step produces a valid JSON array but it is empty
|
|
36
|
+
(`[]`). GitHub Actions evaluates the strategy and then immediately fails the job
|
|
37
|
+
because it has nothing to schedule — "matrix contains zero elements."
|
|
38
|
+
|
|
39
|
+
3. **Malformed JSON**: Newlines, unescaped quotes, or shell word-splitting corrupts the
|
|
40
|
+
JSON string before it reaches fromJSON, producing parse errors such as
|
|
41
|
+
"Unexpected type of value '', expected type: Sequence."
|
|
42
|
+
|
|
43
|
+
All three root causes trace back to the step that generates the matrix JSON, not to
|
|
44
|
+
fromJSON itself. Using `jq` without `-c` (compact output) can embed literal newlines
|
|
45
|
+
that break the output variable; similarly, forgetting to set the output with
|
|
46
|
+
`echo "matrix=..." >> $GITHUB_OUTPUT` leaves the variable empty.
|
|
47
|
+
fix: |
|
|
48
|
+
1. Always produce compact, single-line JSON with `jq -c` and write it to
|
|
49
|
+
`$GITHUB_OUTPUT` (not `$GITHUB_ENV`) in the generator step.
|
|
50
|
+
2. Guard against an empty matrix by providing a fallback single-element array, or add
|
|
51
|
+
an `if` condition to skip the matrix job when the generator output is empty.
|
|
52
|
+
3. Echo and verify the generated JSON in a debug step before the matrix job runs.
|
|
53
|
+
fix_code:
|
|
54
|
+
- language: yaml
|
|
55
|
+
label: "Correct dynamic matrix pattern with empty-guard"
|
|
56
|
+
code: |
|
|
57
|
+
jobs:
|
|
58
|
+
generate:
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
outputs:
|
|
61
|
+
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
|
62
|
+
steps:
|
|
63
|
+
- id: set-matrix
|
|
64
|
+
run: |
|
|
65
|
+
# Produce compact JSON; default to ["none"] if list is empty
|
|
66
|
+
ITEMS=$(cat targets.txt | jq -Rc '[.]' | jq -sc 'add // ["none"]')
|
|
67
|
+
echo "matrix=${ITEMS}" >> $GITHUB_OUTPUT
|
|
68
|
+
|
|
69
|
+
build:
|
|
70
|
+
needs: generate
|
|
71
|
+
# Skip if only the fallback placeholder is present
|
|
72
|
+
if: ${{ needs.generate.outputs.matrix != '["none"]' }}
|
|
73
|
+
strategy:
|
|
74
|
+
matrix:
|
|
75
|
+
target: ${{ fromJSON(needs.generate.outputs.matrix) }}
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
steps:
|
|
78
|
+
- run: echo "Building ${{ matrix.target }}"
|
|
79
|
+
- language: yaml
|
|
80
|
+
label: "Debug step to verify JSON before matrix job"
|
|
81
|
+
code: |
|
|
82
|
+
- name: Verify matrix JSON
|
|
83
|
+
run: |
|
|
84
|
+
echo "Matrix value: ${{ needs.generate.outputs.matrix }}"
|
|
85
|
+
echo '${{ needs.generate.outputs.matrix }}' | jq .
|
|
86
|
+
prevention:
|
|
87
|
+
- "Always use `jq -c` (compact output) when producing JSON for matrix outputs to avoid embedded newlines."
|
|
88
|
+
- "Write matrix JSON to `$GITHUB_OUTPUT`, not `$GITHUB_ENV` — environment variables are not available as job outputs."
|
|
89
|
+
- "Add an empty-matrix guard (`if` condition or fallback value) so a zero-element array does not silently break your pipeline."
|
|
90
|
+
- "Add a debug step after the generator to echo and validate the JSON before the dependent job runs."
|
|
91
|
+
docs:
|
|
92
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow"
|
|
93
|
+
label: "Running variations of jobs in a workflow (matrix)"
|
|
94
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#fromjson"
|
|
95
|
+
label: "fromJSON expression function"
|
|
96
|
+
- url: "https://github.com/actions/runner/issues/2424"
|
|
97
|
+
label: "actions/runner#2424 — fromJSON parsing failures in nested workflows"
|
|
98
|
+
- url: "https://github.com/orgs/community/discussions/27096"
|
|
99
|
+
label: "Community: matrix contains zero elements"
|
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
id: yaml-syntax-007
|
|
2
|
-
title: "if: Conditionals Always Evaluating to true"
|
|
3
|
-
category: yaml-syntax
|
|
4
|
-
severity: silent-failure
|
|
5
|
-
tags:
|
|
6
|
-
- if
|
|
7
|
-
- expressions
|
|
8
|
-
- conditionals
|
|
9
|
-
- yaml
|
|
10
|
-
- truthy
|
|
11
|
-
patterns:
|
|
12
|
-
- regex: "Evaluating condition for step: .*"
|
|
13
|
-
flags: "i"
|
|
14
|
-
- regex: "Expanded: '.+\\n.+"
|
|
15
|
-
flags: "i"
|
|
16
|
-
- regex: "Result: true"
|
|
17
|
-
flags: "i"
|
|
18
|
-
error_messages:
|
|
19
|
-
- "Result: true"
|
|
20
|
-
root_cause: |
|
|
21
|
-
Using a block scalar such as `if: |` turns the condition into a multi-line string.
|
|
22
|
-
In GitHub Actions expressions, any non-empty string is truthy, so the step or job can
|
|
23
|
-
run even when the intended logical check should have been false.
|
|
24
|
-
|
|
25
|
-
A similar footgun happens when the expression is written with extra trailing text or
|
|
26
|
-
whitespace outside the `${{ }}` block, which changes the value from a boolean expression
|
|
27
|
-
into a plain string.
|
|
28
|
-
fix: |
|
|
29
|
-
Keep `if:` expressions on a single line and avoid YAML pipe scalars for conditionals.
|
|
30
|
-
Make sure the entire value is the expression itself, with no extra text after `${{ }}`.
|
|
31
|
-
fix_code:
|
|
32
|
-
- language: yaml
|
|
33
|
-
label: "Write if as a single-line expression"
|
|
34
|
-
code: |
|
|
35
|
-
jobs:
|
|
36
|
-
deploy:
|
|
37
|
-
runs-on: ubuntu-latest
|
|
38
|
-
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
|
|
39
|
-
steps:
|
|
40
|
-
- run: ./deploy.sh
|
|
41
|
-
prevention:
|
|
42
|
-
- "Never use `if: |` for GitHub Actions conditionals."
|
|
43
|
-
- "Turn on step debug logging when a condition seems inverted and inspect the `Expanded:` and `Result:` lines."
|
|
44
|
-
- "Keep boolean logic entirely inside a single `${{ }}` expression."
|
|
45
|
-
docs:
|
|
46
|
-
- url: "https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idif"
|
|
47
|
-
label: "jobs.<job_id>.if"
|
|
48
|
-
- url: "https://docs.github.com/en/actions/learn-github-actions/expressions"
|
|
49
|
-
label: "Expressions"
|
|
50
|
-
source:
|
|
51
|
-
article: "https://htek.dev/articles/github-actions-debugging-guide"
|
|
52
|
-
section: "if expressions that always evaluate true"
|
|
1
|
+
id: yaml-syntax-007
|
|
2
|
+
title: "if: Conditionals Always Evaluating to true"
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- if
|
|
7
|
+
- expressions
|
|
8
|
+
- conditionals
|
|
9
|
+
- yaml
|
|
10
|
+
- truthy
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: "Evaluating condition for step: .*"
|
|
13
|
+
flags: "i"
|
|
14
|
+
- regex: "Expanded: '.+\\n.+"
|
|
15
|
+
flags: "i"
|
|
16
|
+
- regex: "Result: true"
|
|
17
|
+
flags: "i"
|
|
18
|
+
error_messages:
|
|
19
|
+
- "Result: true"
|
|
20
|
+
root_cause: |
|
|
21
|
+
Using a block scalar such as `if: |` turns the condition into a multi-line string.
|
|
22
|
+
In GitHub Actions expressions, any non-empty string is truthy, so the step or job can
|
|
23
|
+
run even when the intended logical check should have been false.
|
|
24
|
+
|
|
25
|
+
A similar footgun happens when the expression is written with extra trailing text or
|
|
26
|
+
whitespace outside the `${{ }}` block, which changes the value from a boolean expression
|
|
27
|
+
into a plain string.
|
|
28
|
+
fix: |
|
|
29
|
+
Keep `if:` expressions on a single line and avoid YAML pipe scalars for conditionals.
|
|
30
|
+
Make sure the entire value is the expression itself, with no extra text after `${{ }}`.
|
|
31
|
+
fix_code:
|
|
32
|
+
- language: yaml
|
|
33
|
+
label: "Write if as a single-line expression"
|
|
34
|
+
code: |
|
|
35
|
+
jobs:
|
|
36
|
+
deploy:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
|
|
39
|
+
steps:
|
|
40
|
+
- run: ./deploy.sh
|
|
41
|
+
prevention:
|
|
42
|
+
- "Never use `if: |` for GitHub Actions conditionals."
|
|
43
|
+
- "Turn on step debug logging when a condition seems inverted and inspect the `Expanded:` and `Result:` lines."
|
|
44
|
+
- "Keep boolean logic entirely inside a single `${{ }}` expression."
|
|
45
|
+
docs:
|
|
46
|
+
- url: "https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idif"
|
|
47
|
+
label: "jobs.<job_id>.if"
|
|
48
|
+
- url: "https://docs.github.com/en/actions/learn-github-actions/expressions"
|
|
49
|
+
label: "Expressions"
|
|
50
|
+
source:
|
|
51
|
+
article: "https://htek.dev/articles/github-actions-debugging-guide"
|
|
52
|
+
section: "if expressions that always evaluate true"
|