@htekdev/actions-debugger 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/errors/caching-artifacts/cache-cross-os-archive-missing.yml +120 -0
- package/errors/permissions-auth/deploy-pages-missing-permissions.yml +144 -0
- package/errors/permissions-auth/fine-grained-pat-actions-scope-missing.yml +128 -0
- package/errors/runner-environment/checkout-persist-credentials-false-git-auth.yml +124 -0
- package/errors/silent-failures/checkout-fetch-depth-shallow-clone-breaks-history.yml +104 -0
- package/errors/triggers/repository-dispatch-event-type-mismatch.yml +116 -0
- package/errors/triggers/workflow-call-required-input-not-supplied.yml +115 -0
- package/errors/triggers/workflow-dispatch-filters-silently-ignored.yml +131 -0
- package/errors/triggers/workflow-dispatch-required-input-api-bypass.yml +114 -0
- package/errors/yaml-syntax/matrix-include-extra-standalone-jobs.yml +136 -0
- package/package.json +1 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
id: caching-artifacts-013
|
|
2
|
+
title: "Cross-OS Cache Miss — enableCrossOsArchive Not Set"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- cross-os
|
|
8
|
+
- windows
|
|
9
|
+
- linux
|
|
10
|
+
- cache-miss
|
|
11
|
+
- enableCrossOsArchive
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "Cache not found for input keys"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "enableCrossOsArchive.*false"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "cache miss.*windows"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "tar.*posix.*failed"
|
|
20
|
+
flags: "i"
|
|
21
|
+
error_messages:
|
|
22
|
+
- "Cache not found for input keys: <key>"
|
|
23
|
+
- "enableCrossOsArchive: false"
|
|
24
|
+
- "Cache saved successfully"
|
|
25
|
+
- "Post job cleanup."
|
|
26
|
+
root_cause: |
|
|
27
|
+
`actions/cache` uses different archive formats depending on the runner OS:
|
|
28
|
+
- Linux/macOS: GNU tar (gnutar) with zstd compression
|
|
29
|
+
- Windows: BSD tar bundled with Git for Windows (`C:\Program Files\Git\usr\bin\tar.exe`)
|
|
30
|
+
|
|
31
|
+
When a cache is created on Linux or macOS, the archive is in GNU tar format. When
|
|
32
|
+
Windows attempts to restore it (or vice versa), the different tar implementation
|
|
33
|
+
fails to decompress the archive, resulting in a silent cache miss.
|
|
34
|
+
|
|
35
|
+
The `enableCrossOsArchive` option (default: `false`) controls whether the cache is
|
|
36
|
+
stored in a cross-platform-compatible format. Setting it to `true` forces GNU tar
|
|
37
|
+
format on all platforms, enabling cache sharing across OSes.
|
|
38
|
+
|
|
39
|
+
This is particularly common in monorepos where:
|
|
40
|
+
- Node modules or package caches are written by a Linux job and expected to be
|
|
41
|
+
restored by a Windows job in the same workflow run
|
|
42
|
+
- A "seed cache" job runs on Linux and downstream jobs run on Windows
|
|
43
|
+
- The matrix includes multiple OS values sharing the same cache key
|
|
44
|
+
|
|
45
|
+
Tracked in actions/cache#1275 (Cache miss on Windows despite a successful
|
|
46
|
+
cache-write) — confirmed fixed by setting `enableCrossOsArchive: true`.
|
|
47
|
+
fix: |
|
|
48
|
+
Add `enableCrossOsArchive: true` to ALL cache steps (both the write and the restore
|
|
49
|
+
steps) that need to share caches across different OS runners.
|
|
50
|
+
|
|
51
|
+
IMPORTANT: The option must be set consistently on both the writing and reading jobs.
|
|
52
|
+
Setting it only on one side will not work.
|
|
53
|
+
|
|
54
|
+
If true cross-OS caching is not needed, ensure each OS creates its own native cache
|
|
55
|
+
by including `${{ runner.os }}` in the cache key.
|
|
56
|
+
fix_code:
|
|
57
|
+
- language: yaml
|
|
58
|
+
label: "Enable cross-OS archive on cache steps that share between Linux and Windows"
|
|
59
|
+
code: |
|
|
60
|
+
- name: Cache node modules (cross-OS compatible)
|
|
61
|
+
uses: actions/cache@v4
|
|
62
|
+
with:
|
|
63
|
+
path: node_modules
|
|
64
|
+
key: node-${{ hashFiles('**/package-lock.json') }}
|
|
65
|
+
enableCrossOsArchive: true # Required for Linux↔Windows cache sharing
|
|
66
|
+
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: "OS-specific cache keys (alternative — each OS gets its own cache)"
|
|
69
|
+
code: |
|
|
70
|
+
- name: Cache dependencies (OS-scoped key — no cross-OS needed)
|
|
71
|
+
uses: actions/cache@v4
|
|
72
|
+
with:
|
|
73
|
+
path: ~/.cache/pip
|
|
74
|
+
# Include runner.os so each OS writes its own cache entry
|
|
75
|
+
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
|
76
|
+
restore-keys: |
|
|
77
|
+
${{ runner.os }}-pip-
|
|
78
|
+
|
|
79
|
+
- language: yaml
|
|
80
|
+
label: "Matrix workflow with consistent enableCrossOsArchive on all jobs"
|
|
81
|
+
code: |
|
|
82
|
+
jobs:
|
|
83
|
+
seed-cache:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
- name: Seed shared cache
|
|
88
|
+
uses: actions/cache@v4
|
|
89
|
+
with:
|
|
90
|
+
path: node_modules
|
|
91
|
+
key: node-${{ hashFiles('**/package-lock.json') }}
|
|
92
|
+
enableCrossOsArchive: true
|
|
93
|
+
|
|
94
|
+
build:
|
|
95
|
+
needs: seed-cache
|
|
96
|
+
strategy:
|
|
97
|
+
matrix:
|
|
98
|
+
os: [ubuntu-latest, windows-latest]
|
|
99
|
+
runs-on: ${{ matrix.os }}
|
|
100
|
+
steps:
|
|
101
|
+
- uses: actions/checkout@v4
|
|
102
|
+
- name: Restore shared cache
|
|
103
|
+
uses: actions/cache@v4
|
|
104
|
+
with:
|
|
105
|
+
path: node_modules
|
|
106
|
+
key: node-${{ hashFiles('**/package-lock.json') }}
|
|
107
|
+
enableCrossOsArchive: true # Must match the write job
|
|
108
|
+
|
|
109
|
+
prevention:
|
|
110
|
+
- "Always include `runner.os` in your cache key unless you explicitly need cross-OS sharing."
|
|
111
|
+
- "When cross-OS sharing IS required, set `enableCrossOsArchive: true` on every step that reads or writes the shared cache."
|
|
112
|
+
- "Treat cross-OS cache sharing as an explicit opt-in, not the default."
|
|
113
|
+
- "Test cache restoration on all target OSes during initial workflow setup."
|
|
114
|
+
docs:
|
|
115
|
+
- url: "https://github.com/actions/cache/blob/main/README.md"
|
|
116
|
+
label: "actions/cache README: enableCrossOsArchive option"
|
|
117
|
+
- url: "https://github.com/actions/cache/blob/main/tips-and-workarounds.md"
|
|
118
|
+
label: "actions/cache: Tips and workarounds (cross-OS section)"
|
|
119
|
+
- url: "https://github.com/actions/cache/issues/1275"
|
|
120
|
+
label: "actions/cache#1275: Cache miss on Windows despite a successful cache-write"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
id: permissions-auth-009
|
|
2
|
+
title: "actions/deploy-pages Fails 403 — Missing pages: write or id-token: write Permissions"
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- deploy-pages
|
|
7
|
+
- pages
|
|
8
|
+
- permissions
|
|
9
|
+
- id-token
|
|
10
|
+
- oidc
|
|
11
|
+
- github-token
|
|
12
|
+
- 403
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: "Failed to create deployment.*status: 403.*pages: write"
|
|
15
|
+
flags: "i"
|
|
16
|
+
- regex: "Resource not accessible by integration"
|
|
17
|
+
flags: "i"
|
|
18
|
+
- regex: "Fetching artifact metadata failed.*Resource not accessible by integration"
|
|
19
|
+
flags: "i"
|
|
20
|
+
- regex: "Error: Error: Failed to create deployment \\(status: 403\\)"
|
|
21
|
+
flags: "i"
|
|
22
|
+
error_messages:
|
|
23
|
+
- "Error: Fetching artifact metadata failed. Is githubstatus.com reporting issues with API requests, Pages or Actions? Please re-run the deployment at a later time."
|
|
24
|
+
- "Error: HttpError: Resource not accessible by integration"
|
|
25
|
+
- "Error: Error: Failed to create deployment (status: 403) with build version abc123. Ensure GITHUB_TOKEN has permission \"pages: write\"."
|
|
26
|
+
- "Creating Pages deployment failed"
|
|
27
|
+
root_cause: |
|
|
28
|
+
`actions/deploy-pages` uses the GitHub Pages REST API, which requires an OIDC-issued
|
|
29
|
+
JWT token for authentication. This imposes three distinct permissions that must all be
|
|
30
|
+
present on the **deploy job** (not the build job):
|
|
31
|
+
|
|
32
|
+
1. `pages: write` — allows the GITHUB_TOKEN to create and update GitHub Pages deployments.
|
|
33
|
+
2. `id-token: write` — allows the runner to request an OIDC JWT from GitHub's token
|
|
34
|
+
endpoint. Without this, `deploy-pages` cannot obtain the token used to authenticate
|
|
35
|
+
the Pages API call, resulting in the misleading "Resource not accessible by integration"
|
|
36
|
+
HTTP 403 error.
|
|
37
|
+
3. `contents: read` — required to read the uploaded artifact. Often already set by
|
|
38
|
+
inheritance but must be explicit when a workflow-level `permissions:` block restricts
|
|
39
|
+
all tokens to read-only.
|
|
40
|
+
|
|
41
|
+
**Why this is confusing:**
|
|
42
|
+
- The error message says "Resource not accessible by integration" which looks like a generic
|
|
43
|
+
API permission problem, not a missing `id-token` scope.
|
|
44
|
+
- The `pages: write` permission alone is insufficient — `id-token: write` is equally
|
|
45
|
+
required and its absence produces the exact same error.
|
|
46
|
+
- Permissions set at the **workflow level** do not automatically apply to jobs that override
|
|
47
|
+
them. The deploy job must declare its own `permissions:` block.
|
|
48
|
+
- The artifact upload (via `actions/upload-pages-artifact`) happens in a SEPARATE job from
|
|
49
|
+
the deploy — permissions for upload and deploy must be set independently.
|
|
50
|
+
|
|
51
|
+
**Source:** Confirmed in `actions/deploy-pages` issues #285, #286 (Dec 2023) — over 16
|
|
52
|
+
reactions and widespread community impact.
|
|
53
|
+
fix: |
|
|
54
|
+
Add all three required permissions to the deploy job. The build job that runs
|
|
55
|
+
`upload-pages-artifact` does not need these permissions, but the deploy job does.
|
|
56
|
+
|
|
57
|
+
Also verify that GitHub Pages is enabled for the repository under
|
|
58
|
+
Settings → Pages → Source → "GitHub Actions".
|
|
59
|
+
fix_code:
|
|
60
|
+
- language: yaml
|
|
61
|
+
label: "Correct two-job Pages deployment workflow with required permissions"
|
|
62
|
+
code: |
|
|
63
|
+
name: Deploy GitHub Pages
|
|
64
|
+
|
|
65
|
+
on:
|
|
66
|
+
push:
|
|
67
|
+
branches: [main]
|
|
68
|
+
|
|
69
|
+
# Deny all permissions at workflow level; grant explicitly per job
|
|
70
|
+
permissions:
|
|
71
|
+
contents: read
|
|
72
|
+
|
|
73
|
+
jobs:
|
|
74
|
+
build:
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/checkout@v4
|
|
78
|
+
|
|
79
|
+
- name: Build site
|
|
80
|
+
run: npm run build # outputs to ./dist
|
|
81
|
+
|
|
82
|
+
- name: Upload Pages artifact
|
|
83
|
+
uses: actions/upload-pages-artifact@v3
|
|
84
|
+
with:
|
|
85
|
+
path: ./dist
|
|
86
|
+
|
|
87
|
+
deploy:
|
|
88
|
+
needs: build
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
permissions:
|
|
91
|
+
pages: write # ← Required: create/update Pages deployment
|
|
92
|
+
id-token: write # ← Required: obtain OIDC JWT for Pages API auth
|
|
93
|
+
contents: read # ← Required: read the uploaded artifact
|
|
94
|
+
environment:
|
|
95
|
+
name: github-pages
|
|
96
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
97
|
+
steps:
|
|
98
|
+
- name: Deploy to GitHub Pages
|
|
99
|
+
id: deployment
|
|
100
|
+
uses: actions/deploy-pages@v4
|
|
101
|
+
- language: yaml
|
|
102
|
+
label: "Single-job variant — all permissions in one job"
|
|
103
|
+
code: |
|
|
104
|
+
name: Deploy Pages (single job)
|
|
105
|
+
|
|
106
|
+
on:
|
|
107
|
+
push:
|
|
108
|
+
branches: [main]
|
|
109
|
+
|
|
110
|
+
jobs:
|
|
111
|
+
deploy:
|
|
112
|
+
runs-on: ubuntu-latest
|
|
113
|
+
permissions:
|
|
114
|
+
pages: write
|
|
115
|
+
id-token: write
|
|
116
|
+
contents: read
|
|
117
|
+
environment:
|
|
118
|
+
name: github-pages
|
|
119
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
120
|
+
steps:
|
|
121
|
+
- uses: actions/checkout@v4
|
|
122
|
+
- name: Build
|
|
123
|
+
run: npm run build
|
|
124
|
+
- uses: actions/upload-pages-artifact@v3
|
|
125
|
+
with:
|
|
126
|
+
path: ./dist
|
|
127
|
+
- id: deployment
|
|
128
|
+
uses: actions/deploy-pages@v4
|
|
129
|
+
prevention:
|
|
130
|
+
- "Always set `pages: write`, `id-token: write`, and `contents: read` on the deploy job — not the build job and not only at the workflow level."
|
|
131
|
+
- "Confirm GitHub Pages is enabled under repository Settings → Pages → Source → 'GitHub Actions' before running the workflow."
|
|
132
|
+
- "When using a workflow-level `permissions:` block that restricts defaults, remember job-level permissions do not inherit write scopes — they must be re-declared."
|
|
133
|
+
- "The `environment: github-pages` block is required for the Pages API to create a deployment — do not omit it."
|
|
134
|
+
docs:
|
|
135
|
+
- url: "https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site#publishing-with-a-custom-github-actions-workflow"
|
|
136
|
+
label: "GitHub Docs: Publishing GitHub Pages with Actions"
|
|
137
|
+
- url: "https://github.com/actions/deploy-pages"
|
|
138
|
+
label: "actions/deploy-pages — official action repository"
|
|
139
|
+
- url: "https://github.com/actions/deploy-pages/issues/285"
|
|
140
|
+
label: "actions/deploy-pages #285: Failing to fetch artifact metadata since 4.0.0"
|
|
141
|
+
- url: "https://github.com/actions/deploy-pages/issues/286"
|
|
142
|
+
label: "actions/deploy-pages #286: Bump to V4 broke the deploy step"
|
|
143
|
+
- url: "https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect"
|
|
144
|
+
label: "GitHub Docs: Security hardening with OpenID Connect"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
id: permissions-auth-010
|
|
2
|
+
title: "Fine-Grained PAT Missing Actions Permission — Workflow Dispatch and API Calls Fail 403"
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- fine-grained-pat
|
|
7
|
+
- pat
|
|
8
|
+
- personal-access-token
|
|
9
|
+
- actions
|
|
10
|
+
- workflow-dispatch
|
|
11
|
+
- 403
|
|
12
|
+
- api
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: "HTTP 403.*fine.?grained.*token"
|
|
15
|
+
flags: "i"
|
|
16
|
+
- regex: "Resource not accessible by integration"
|
|
17
|
+
flags: "i"
|
|
18
|
+
- regex: "Must have admin rights to Repository\\."
|
|
19
|
+
flags: "i"
|
|
20
|
+
- regex: "403.*workflow.*dispatch"
|
|
21
|
+
flags: "i"
|
|
22
|
+
error_messages:
|
|
23
|
+
- "HTTP 403: {\"message\":\"Resource not accessible by integration\",\"documentation_url\":\"https://docs.github.com/rest/actions/workflows\"}"
|
|
24
|
+
- "Error: error making HTTP request: 403 Resource not accessible by integration"
|
|
25
|
+
- "gh: Could not run workflow: HTTP 422 Unprocessable Entity"
|
|
26
|
+
- "RequestError [HttpError]: Resource not accessible by integration"
|
|
27
|
+
root_cause: |
|
|
28
|
+
GitHub fine-grained personal access tokens (introduced 2022, GA 2023) do NOT include
|
|
29
|
+
the "Actions" permission scope by default when created. This is different from classic
|
|
30
|
+
PATs where the `repo` scope implicitly granted access to Actions APIs.
|
|
31
|
+
|
|
32
|
+
When a fine-grained PAT is used to:
|
|
33
|
+
- Trigger workflows via REST API (`POST /repos/{owner}/{repo}/actions/workflows/{id}/dispatches`)
|
|
34
|
+
- List or cancel workflow runs
|
|
35
|
+
- Download workflow run logs
|
|
36
|
+
- Use `gh workflow run` / `gh run list` with the PAT as `GH_TOKEN`
|
|
37
|
+
|
|
38
|
+
...the API returns HTTP 403 "Resource not accessible by integration" because the
|
|
39
|
+
fine-grained token lacks the explicit `Actions: read` or `Actions: write` permission.
|
|
40
|
+
|
|
41
|
+
**Why this affects more users over time:**
|
|
42
|
+
- GitHub is progressively deprecating classic PATs in favor of fine-grained PATs.
|
|
43
|
+
- Organization policies can require fine-grained PATs only, blocking classic PAT usage.
|
|
44
|
+
- Automation pipelines that worked with classic `repo`-scoped PATs silently break when
|
|
45
|
+
migrated to fine-grained PATs if the Actions permission is not explicitly added.
|
|
46
|
+
- The error message "Resource not accessible by integration" is the same as the one
|
|
47
|
+
shown for missing `GITHUB_TOKEN` scopes, making diagnosis harder.
|
|
48
|
+
|
|
49
|
+
**Required permissions on the fine-grained PAT by use case:**
|
|
50
|
+
| Use case | Required permission |
|
|
51
|
+
|---|---|
|
|
52
|
+
| Trigger workflow (`workflow_dispatch`) | Actions: write |
|
|
53
|
+
| Cancel / re-run workflow run | Actions: write |
|
|
54
|
+
| List workflow runs / jobs | Actions: read |
|
|
55
|
+
| Download workflow run logs | Actions: read |
|
|
56
|
+
| Read workflow YAML definitions | Actions: read |
|
|
57
|
+
fix: |
|
|
58
|
+
Regenerate or edit the fine-grained PAT and add the **Actions** permission with the
|
|
59
|
+
appropriate access level:
|
|
60
|
+
|
|
61
|
+
- **Read-only** — for listing runs, reading logs, checking run status.
|
|
62
|
+
- **Read and write** — for dispatching workflows, cancelling runs, re-triggering jobs.
|
|
63
|
+
|
|
64
|
+
Navigate to: GitHub → Settings → Developer Settings → Personal access tokens →
|
|
65
|
+
Fine-grained tokens → (select token) → Permissions → Repository permissions →
|
|
66
|
+
Actions → set to "Read and write".
|
|
67
|
+
|
|
68
|
+
If the repository is in an organization, also ensure the organization has not restricted
|
|
69
|
+
fine-grained PAT usage to specific resources that exclude this repository.
|
|
70
|
+
fix_code:
|
|
71
|
+
- language: yaml
|
|
72
|
+
label: "Using a fine-grained PAT with Actions permission to trigger a workflow"
|
|
73
|
+
code: |
|
|
74
|
+
# Store the fine-grained PAT as a repository secret: DEPLOY_PAT
|
|
75
|
+
# The PAT must have: Actions: write, Contents: read (minimum)
|
|
76
|
+
|
|
77
|
+
name: Trigger Deployment
|
|
78
|
+
|
|
79
|
+
on:
|
|
80
|
+
workflow_dispatch:
|
|
81
|
+
|
|
82
|
+
jobs:
|
|
83
|
+
trigger:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- name: Dispatch downstream workflow
|
|
87
|
+
run: |
|
|
88
|
+
gh workflow run deploy.yml \
|
|
89
|
+
-f environment=production \
|
|
90
|
+
--repo org/downstream-repo
|
|
91
|
+
env:
|
|
92
|
+
GH_TOKEN: ${{ secrets.DEPLOY_PAT }} # Fine-grained PAT with Actions: write
|
|
93
|
+
- language: yaml
|
|
94
|
+
label: "Required PAT permissions checklist for common Actions API uses"
|
|
95
|
+
code: |
|
|
96
|
+
# Fine-grained PAT permission requirements:
|
|
97
|
+
#
|
|
98
|
+
# Trigger workflow dispatch → Actions: Write
|
|
99
|
+
# Cancel a workflow run → Actions: Write
|
|
100
|
+
# Re-run failed jobs → Actions: Write
|
|
101
|
+
# List workflow runs → Actions: Read
|
|
102
|
+
# Download run logs → Actions: Read
|
|
103
|
+
# Get workflow definition → Actions: Read
|
|
104
|
+
# Approve environment deployment → Actions: Write + Environments: Write
|
|
105
|
+
#
|
|
106
|
+
# Classic PAT 'repo' scope covers all of the above implicitly.
|
|
107
|
+
# Fine-grained PATs require explicit opt-in per permission category.
|
|
108
|
+
|
|
109
|
+
# To check if your PAT has the right permissions:
|
|
110
|
+
- name: Verify PAT has Actions permission
|
|
111
|
+
run: |
|
|
112
|
+
gh api /repos/${{ github.repository }}/actions/workflows \
|
|
113
|
+
--jq '.total_count'
|
|
114
|
+
env:
|
|
115
|
+
GH_TOKEN: ${{ secrets.DEPLOY_PAT }} # Returns 403 if Actions: read is missing
|
|
116
|
+
prevention:
|
|
117
|
+
- "When migrating from classic PATs to fine-grained PATs, explicitly audit which permission categories the classic `repo` scope was implicitly covering and add each one."
|
|
118
|
+
- "Add `Actions: read` at minimum to any fine-grained PAT that will be used in GitHub Actions pipelines, even for read-only operations."
|
|
119
|
+
- "Store fine-grained PATs as org-level or repository secrets and add a comment documenting which permissions the PAT requires."
|
|
120
|
+
- "Use a dedicated machine account (bot user) for automation PATs so permissions can be audited independently of individual developer accounts."
|
|
121
|
+
- "If an organization policy requires fine-grained PATs, update all automation onboarding docs to include the Actions permission explicitly."
|
|
122
|
+
docs:
|
|
123
|
+
- url: "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token"
|
|
124
|
+
label: "GitHub Docs: Creating a fine-grained personal access token"
|
|
125
|
+
- url: "https://docs.github.com/en/rest/actions/workflows#create-a-workflow-dispatch-event"
|
|
126
|
+
label: "REST API: Create a workflow dispatch event"
|
|
127
|
+
- url: "https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization"
|
|
128
|
+
label: "GitHub Docs: PAT policy for organizations"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
id: runner-environment-032
|
|
2
|
+
title: "persist-credentials: false Breaks Subsequent Git Push / Authentication"
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- checkout
|
|
7
|
+
- persist-credentials
|
|
8
|
+
- git-push
|
|
9
|
+
- authentication
|
|
10
|
+
- credential-helper
|
|
11
|
+
- git-auto-commit
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "fatal:.*could not read Username.*No such device"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "fatal: Authentication failed for.*github\\.com"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "remote:.*Invalid username or password"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "persist-credentials.*false"
|
|
20
|
+
flags: "i"
|
|
21
|
+
- regex: "error: The requested URL returned error: 403"
|
|
22
|
+
flags: "i"
|
|
23
|
+
error_messages:
|
|
24
|
+
- "fatal: could not read Username for 'https://github.com': No such device or address"
|
|
25
|
+
- "fatal: Authentication failed for 'https://github.com/owner/repo.git/'"
|
|
26
|
+
- "remote: Invalid username or password."
|
|
27
|
+
- "error: The requested URL returned error: 403"
|
|
28
|
+
- "remote: Support for password authentication was removed"
|
|
29
|
+
root_cause: |
|
|
30
|
+
When `actions/checkout` runs with `persist-credentials: false`, it:
|
|
31
|
+
1. Checks out the repository
|
|
32
|
+
2. Explicitly **removes** the git credential helper that was configured for GITHUB_TOKEN auth
|
|
33
|
+
3. Leaves the working directory with no way to authenticate subsequent git operations
|
|
34
|
+
|
|
35
|
+
Any `git push`, `git pull`, or `git fetch` call that runs AFTER this checkout will
|
|
36
|
+
fail with an authentication error because there is no credential helper configured.
|
|
37
|
+
|
|
38
|
+
This pattern is often introduced deliberately for security (to avoid GITHUB_TOKEN
|
|
39
|
+
persistence), but developers forget that it also breaks any action or step that needs
|
|
40
|
+
to push changes back to the repository — such as:
|
|
41
|
+
- `stefanzweifel/git-auto-commit-action` (fails with "could not read Username")
|
|
42
|
+
- Manual `git push origin HEAD` steps
|
|
43
|
+
- Semantic-release, release-please, and other auto-committing tools
|
|
44
|
+
- Actions that amend, tag, or push version bumps
|
|
45
|
+
|
|
46
|
+
The issue is also triggered transitively: if a composite action internally calls
|
|
47
|
+
`actions/checkout` with `persist-credentials: false`, the credential helper is
|
|
48
|
+
removed from the runner's git config, breaking git auth for all subsequent steps.
|
|
49
|
+
|
|
50
|
+
Tracked across multiple issues: stefanzweifel/git-auto-commit-action#356,
|
|
51
|
+
stefanzweifel/git-auto-commit-action#397, anthropics/claude-code-action#1236.
|
|
52
|
+
fix: |
|
|
53
|
+
**Option 1 (simplest): Remove persist-credentials: false**
|
|
54
|
+
If you set it out of habit or from a template, just remove it. GITHUB_TOKEN credentials
|
|
55
|
+
stored by actions/checkout are scoped to the runner and do not persist beyond the
|
|
56
|
+
workflow run anyway.
|
|
57
|
+
|
|
58
|
+
**Option 2: Re-configure credentials after checkout**
|
|
59
|
+
If you need `persist-credentials: false` for security reasons (e.g., to prevent
|
|
60
|
+
GITHUB_TOKEN from being used by untrusted code), re-add credentials only for the
|
|
61
|
+
steps that need to push.
|
|
62
|
+
|
|
63
|
+
**Option 3: Use SSH instead of HTTPS**
|
|
64
|
+
Configure SSH deploy keys and use an SSH remote URL to avoid HTTPS credential
|
|
65
|
+
issues entirely.
|
|
66
|
+
fix_code:
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: "Remove persist-credentials: false (simplest fix)"
|
|
69
|
+
code: |
|
|
70
|
+
- uses: actions/checkout@v4
|
|
71
|
+
with:
|
|
72
|
+
# REMOVE this line — credentials are scoped to the runner by default
|
|
73
|
+
# persist-credentials: false
|
|
74
|
+
|
|
75
|
+
- name: Commit and push changes
|
|
76
|
+
run: |
|
|
77
|
+
git config user.name "github-actions[bot]"
|
|
78
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
79
|
+
git add .
|
|
80
|
+
git commit -m "chore: auto-update generated files [skip ci]"
|
|
81
|
+
git push
|
|
82
|
+
|
|
83
|
+
- language: yaml
|
|
84
|
+
label: "Re-configure credentials after persist-credentials: false (security-first workflows)"
|
|
85
|
+
code: |
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
with:
|
|
88
|
+
persist-credentials: false # Untrusted code runs between here and push
|
|
89
|
+
|
|
90
|
+
# ... run untrusted/third-party code here ...
|
|
91
|
+
|
|
92
|
+
- name: Re-configure GITHUB_TOKEN for push
|
|
93
|
+
run: |
|
|
94
|
+
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
|
|
95
|
+
# Now git push will work again
|
|
96
|
+
|
|
97
|
+
- name: Push changes
|
|
98
|
+
run: git push
|
|
99
|
+
|
|
100
|
+
- language: yaml
|
|
101
|
+
label: "Use PAT or app token to push (avoids GITHUB_TOKEN limitations)"
|
|
102
|
+
code: |
|
|
103
|
+
- uses: actions/checkout@v4
|
|
104
|
+
with:
|
|
105
|
+
token: ${{ secrets.MY_PAT }} # Use a PAT that has write access
|
|
106
|
+
persist-credentials: true # Default: true — keep this to allow push
|
|
107
|
+
|
|
108
|
+
- name: Push changes
|
|
109
|
+
run: git push
|
|
110
|
+
|
|
111
|
+
prevention:
|
|
112
|
+
- "Do not add `persist-credentials: false` unless you have a specific security reason (e.g., running untrusted fork code)."
|
|
113
|
+
- "If any step after `actions/checkout` needs to push commits or tags, ensure credentials are persisted or re-configured."
|
|
114
|
+
- "When using `git-auto-commit-action` or similar, check upstream docs for compatibility with `persist-credentials: false`."
|
|
115
|
+
- "Prefer `token: ${{ secrets.GITHUB_TOKEN }}` with `persist-credentials: true` over re-configuring remote URLs manually."
|
|
116
|
+
docs:
|
|
117
|
+
- url: "https://github.com/actions/checkout#usage"
|
|
118
|
+
label: "actions/checkout: persist-credentials input"
|
|
119
|
+
- url: "https://github.com/stefanzweifel/git-auto-commit-action/discussions/356"
|
|
120
|
+
label: "git-auto-commit-action#356: persist-credentials: false compatibility"
|
|
121
|
+
- url: "https://github.com/stefanzweifel/git-auto-commit-action/issues/397"
|
|
122
|
+
label: "git-auto-commit-action#397: fatal: could not read Username (checkout v5)"
|
|
123
|
+
- url: "https://github.com/anthropics/claude-code-action/issues/1236"
|
|
124
|
+
label: "claude-code-action#1236: fails when persist-credentials: false"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
id: silent-failures-013
|
|
2
|
+
title: "Shallow Clone (fetch-depth: 1) Silently Breaks Git History Operations"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- checkout
|
|
7
|
+
- fetch-depth
|
|
8
|
+
- shallow-clone
|
|
9
|
+
- git-describe
|
|
10
|
+
- changelog
|
|
11
|
+
- versioning
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "fatal:.*No names found.*cannot describe anything"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "fatal:.*no tag can describe"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "git describe.*failed with exit code 128"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "fetch-depth.*1"
|
|
20
|
+
flags: "i"
|
|
21
|
+
error_messages:
|
|
22
|
+
- "fatal: No names found, cannot describe anything."
|
|
23
|
+
- "fatal: no tag can describe ''"
|
|
24
|
+
- "error: process completed with exit code 128"
|
|
25
|
+
- "git describe --tags --abbrev=0 failed"
|
|
26
|
+
root_cause: |
|
|
27
|
+
`actions/checkout` defaults to `fetch-depth: 1`, which creates a shallow clone containing
|
|
28
|
+
only the most recent commit. This means:
|
|
29
|
+
- No commit history beyond the latest commit is available
|
|
30
|
+
- No tags are fetched (unless `fetch-tags: true` is set separately)
|
|
31
|
+
- Git operations that need history context fail silently or produce wrong results
|
|
32
|
+
|
|
33
|
+
Affected operations include:
|
|
34
|
+
- `git describe --tags` — fails with "No names found, cannot describe anything"
|
|
35
|
+
- `git log --oneline HEAD~10..HEAD` — returns nothing or errors
|
|
36
|
+
- Semantic versioning tools (semantic-release, standard-version, release-please)
|
|
37
|
+
- Changelog generators that diff HEAD against previous tags
|
|
38
|
+
- `git rev-list --count HEAD` — returns "1" instead of full commit count
|
|
39
|
+
- GitVersion, MinVer, and similar tag-based version calculators
|
|
40
|
+
|
|
41
|
+
The failure is silent in many cases: the step appears to succeed, but produces an
|
|
42
|
+
empty string, "0.0.0", or a fallback version instead of the expected semver.
|
|
43
|
+
|
|
44
|
+
Tracked in actions/checkout#217 (RFC: fetch-depth: 1 and not cloning tags are dangerous
|
|
45
|
+
defaults) with 22 thumbs-up reactions.
|
|
46
|
+
fix: |
|
|
47
|
+
**Option 1 (recommended for most cases): Fetch full history**
|
|
48
|
+
Set `fetch-depth: 0` to fetch all commits and tags.
|
|
49
|
+
|
|
50
|
+
**Option 2: Fetch only enough history**
|
|
51
|
+
For large repos, fetch only the depth needed (e.g., last 50 commits). Use
|
|
52
|
+
`fetch-tags: true` (available in actions/checkout@v4) to fetch all tags without
|
|
53
|
+
the full commit history.
|
|
54
|
+
|
|
55
|
+
**Option 3: Unshallow after checkout**
|
|
56
|
+
Fetch the necessary history lazily with `git fetch --unshallow` or
|
|
57
|
+
`git fetch --tags --unshallow` as a subsequent step.
|
|
58
|
+
fix_code:
|
|
59
|
+
- language: yaml
|
|
60
|
+
label: "Full history checkout (simplest fix)"
|
|
61
|
+
code: |
|
|
62
|
+
- uses: actions/checkout@v4
|
|
63
|
+
with:
|
|
64
|
+
fetch-depth: 0 # 0 = full history; default 1 = shallow
|
|
65
|
+
|
|
66
|
+
- name: Generate changelog
|
|
67
|
+
run: git log --oneline $(git describe --tags --abbrev=0 @^)..@ --no-merges
|
|
68
|
+
|
|
69
|
+
- language: yaml
|
|
70
|
+
label: "Fetch tags only without full history (faster for large repos)"
|
|
71
|
+
code: |
|
|
72
|
+
- uses: actions/checkout@v4
|
|
73
|
+
with:
|
|
74
|
+
fetch-depth: 0 # Required for tag-based versioning
|
|
75
|
+
fetch-tags: true # Explicitly fetch all tags (actions/checkout v4.1.1+)
|
|
76
|
+
|
|
77
|
+
- language: yaml
|
|
78
|
+
label: "Unshallow lazily if full history is not needed upfront"
|
|
79
|
+
code: |
|
|
80
|
+
- uses: actions/checkout@v4
|
|
81
|
+
# fetch-depth: 1 (default — shallow clone)
|
|
82
|
+
|
|
83
|
+
- name: Fetch tags for versioning
|
|
84
|
+
run: |
|
|
85
|
+
git fetch --tags --force
|
|
86
|
+
# Or for full history:
|
|
87
|
+
# git fetch --unshallow
|
|
88
|
+
|
|
89
|
+
- name: Get version from tags
|
|
90
|
+
run: git describe --tags --abbrev=0
|
|
91
|
+
|
|
92
|
+
prevention:
|
|
93
|
+
- "Add `fetch-depth: 0` to any checkout step that precedes git history, tag, or versioning operations."
|
|
94
|
+
- "Set `fetch-tags: true` in actions/checkout@v4 when using tag-based versioning tools."
|
|
95
|
+
- "When using semantic-release, release-please, or GitVersion, always use `fetch-depth: 0`."
|
|
96
|
+
- "Test locally with `git clone --depth 1` to reproduce the shallow clone environment before debugging in CI."
|
|
97
|
+
- "Audit all checkout steps in release workflows — shallow clones are fine for build/test but break release automation."
|
|
98
|
+
docs:
|
|
99
|
+
- url: "https://github.com/actions/checkout#usage"
|
|
100
|
+
label: "actions/checkout: fetch-depth and fetch-tags inputs"
|
|
101
|
+
- url: "https://github.com/actions/checkout/issues/217"
|
|
102
|
+
label: "actions/checkout#217: RFC — fetch-depth: 1 and not cloning tags are dangerous defaults"
|
|
103
|
+
- url: "https://stackoverflow.com/questions/66349002/get-latest-tag-git-describe-tags-when-repo-is-cloned-with-depth-1"
|
|
104
|
+
label: "Stack Overflow: git describe fails when cloned with depth=1"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
id: triggers-010
|
|
2
|
+
title: "repository_dispatch Event Type Not in types: Filter — Workflow Silently Skipped"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- repository_dispatch
|
|
7
|
+
- event-type
|
|
8
|
+
- types-filter
|
|
9
|
+
- triggers
|
|
10
|
+
- api
|
|
11
|
+
- silent
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "repository_dispatch"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "types:\\s*\\[.*\\]"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "client_payload"
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "No runs found for workflow"
|
|
21
|
+
- "Workflow not triggered by repository_dispatch event"
|
|
22
|
+
root_cause: |
|
|
23
|
+
A `repository_dispatch` workflow only fires when the dispatched `event_type` value
|
|
24
|
+
matches one of the values in the `types:` filter. If the event type sent via the API
|
|
25
|
+
does not match, GitHub silently drops it — no error, no notification, the workflow simply
|
|
26
|
+
never starts.
|
|
27
|
+
|
|
28
|
+
**Common failure scenarios:**
|
|
29
|
+
|
|
30
|
+
1. **Typo or case mismatch**: `event_type: "Deploy_Staging"` dispatched but workflow
|
|
31
|
+
has `types: [deploy_staging]` (underscore vs case sensitivity). Event types are
|
|
32
|
+
case-sensitive.
|
|
33
|
+
|
|
34
|
+
2. **Omitting the types: filter entirely — workflow fires for ALL events** (opposite
|
|
35
|
+
problem): Without a `types:` filter, the workflow runs for every `repository_dispatch`
|
|
36
|
+
event, including internal automation events not intended to trigger it.
|
|
37
|
+
|
|
38
|
+
3. **API payload uses wrong field name**: The REST API requires the `event_type` field
|
|
39
|
+
under the JSON body. Some clients accidentally nest it differently or omit it entirely,
|
|
40
|
+
causing the dispatch to use the default empty event type which matches nothing.
|
|
41
|
+
|
|
42
|
+
4. **Cross-workflow automation chain**: Workflow A dispatches event type `build-complete`.
|
|
43
|
+
Workflow B listens for `build_complete` (underscore). The chain silently breaks with no
|
|
44
|
+
logs in either workflow.
|
|
45
|
+
|
|
46
|
+
**API payload shape:**
|
|
47
|
+
```
|
|
48
|
+
POST /repos/{owner}/{repo}/dispatches
|
|
49
|
+
{
|
|
50
|
+
"event_type": "deploy_staging", ← Must exactly match types: filter
|
|
51
|
+
"client_payload": { ... }
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
fix: |
|
|
55
|
+
Verify that the `event_type` string in the API POST body matches exactly (case-sensitive)
|
|
56
|
+
one of the values declared in the workflow's `types:` filter.
|
|
57
|
+
|
|
58
|
+
To debug: add a catch-all workflow with no `types:` filter (matches all repository_dispatch
|
|
59
|
+
events) to confirm the event is arriving at all, then compare the `github.event.action`
|
|
60
|
+
value to what your workflow expects.
|
|
61
|
+
fix_code:
|
|
62
|
+
- language: yaml
|
|
63
|
+
label: "Workflow with correctly declared repository_dispatch types filter"
|
|
64
|
+
code: |
|
|
65
|
+
on:
|
|
66
|
+
repository_dispatch:
|
|
67
|
+
types:
|
|
68
|
+
- deploy_staging # ← Must match event_type exactly (case-sensitive)
|
|
69
|
+
- deploy_production
|
|
70
|
+
- run_smoke_tests
|
|
71
|
+
|
|
72
|
+
jobs:
|
|
73
|
+
deploy:
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v4
|
|
77
|
+
- name: Show dispatched event
|
|
78
|
+
run: |
|
|
79
|
+
echo "Event: ${{ github.event.action }}"
|
|
80
|
+
echo "Payload: ${{ toJSON(github.event.client_payload) }}"
|
|
81
|
+
- language: yaml
|
|
82
|
+
label: "Debugging workflow — catch ALL repository_dispatch events"
|
|
83
|
+
code: |
|
|
84
|
+
# Temporarily add this workflow to diagnose missing dispatches
|
|
85
|
+
on:
|
|
86
|
+
repository_dispatch: # No types: filter = receives everything
|
|
87
|
+
|
|
88
|
+
jobs:
|
|
89
|
+
debug:
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
steps:
|
|
92
|
+
- name: Log event details
|
|
93
|
+
run: |
|
|
94
|
+
echo "event_type / action: ${{ github.event.action }}"
|
|
95
|
+
echo "client_payload: ${{ toJSON(github.event.client_payload) }}"
|
|
96
|
+
- language: yaml
|
|
97
|
+
label: "Dispatching via REST API — correct payload structure"
|
|
98
|
+
code: |
|
|
99
|
+
# Using gh api to dispatch correctly
|
|
100
|
+
gh api \
|
|
101
|
+
--method POST \
|
|
102
|
+
-H "Accept: application/vnd.github+json" \
|
|
103
|
+
/repos/ORG/REPO/dispatches \
|
|
104
|
+
-f event_type="deploy_staging" \ # ← Matches types: [deploy_staging]
|
|
105
|
+
-F client_payload='{"version":"v1.2.3","env":"staging"}'
|
|
106
|
+
prevention:
|
|
107
|
+
- "Treat `event_type` values as constants — define them in a shared location (e.g., a constants file or workflow comment) used by both the dispatcher and the listener."
|
|
108
|
+
- "Add an `echo` step that logs `github.event.action` at the start of every repository_dispatch workflow to confirm the correct event type was received."
|
|
109
|
+
- "Use a catch-all debug workflow (no `types:` filter) when setting up a new dispatch chain for the first time."
|
|
110
|
+
- "Avoid changing event type strings in active automation chains — add new types instead of renaming existing ones."
|
|
111
|
+
- "Event types are case-sensitive — stick to snake_case by convention to avoid case mismatch bugs."
|
|
112
|
+
docs:
|
|
113
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#repository_dispatch"
|
|
114
|
+
label: "GitHub Actions: repository_dispatch event"
|
|
115
|
+
- url: "https://docs.github.com/en/rest/repos/repos#create-a-repository-dispatch-event"
|
|
116
|
+
label: "REST API: Create a repository dispatch event"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
id: triggers-009
|
|
2
|
+
title: "Reusable Workflow Required Input Not Supplied — workflow_call Fails at Runtime"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- workflow_call
|
|
7
|
+
- reusable-workflow
|
|
8
|
+
- inputs
|
|
9
|
+
- required
|
|
10
|
+
- validation
|
|
11
|
+
- caller
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "Input required and not supplied:\\s*\\w+"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "Required input '\\w+' not provided"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "Error: Input required and not supplied"
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "Error: Input required and not supplied: deploy_env"
|
|
21
|
+
- "Input required and not supplied: version"
|
|
22
|
+
- "Required input 'environment' not provided by caller"
|
|
23
|
+
root_cause: |
|
|
24
|
+
Unlike `workflow_dispatch` (which treats `required: true` as UI-only), the GitHub Actions
|
|
25
|
+
runner DOES validate required inputs for `on.workflow_call` reusable workflows. If a caller
|
|
26
|
+
workflow invokes a reusable workflow without passing a declared required input, the runner
|
|
27
|
+
emits `Error: Input required and not supplied: {input_name}` and fails the job immediately.
|
|
28
|
+
|
|
29
|
+
**Common scenarios:**
|
|
30
|
+
1. **Adding a required input to an existing reusable workflow** without updating all callers.
|
|
31
|
+
Callers that worked before immediately break because the new required input is missing.
|
|
32
|
+
|
|
33
|
+
2. **Calling a reusable workflow from a matrix job** where one matrix variation doesn't
|
|
34
|
+
apply the needed input conditionally.
|
|
35
|
+
|
|
36
|
+
3. **Caller passes the input only under certain conditions** (via `if:`) — when the condition
|
|
37
|
+
is false, the input is omitted entirely rather than being passed as an empty string.
|
|
38
|
+
|
|
39
|
+
4. **Typo in the input name** — caller passes `deploy-env` but the reusable workflow expects
|
|
40
|
+
`deploy_env` (hyphens vs underscores). The declared input is not satisfied.
|
|
41
|
+
|
|
42
|
+
**Note:** The error fires at job evaluation time, not step execution time, so the entire job
|
|
43
|
+
is skipped rather than reaching the failing step.
|
|
44
|
+
fix: |
|
|
45
|
+
Ensure every caller workflow passes all required inputs declared in the reusable workflow's
|
|
46
|
+
`on.workflow_call.inputs` block.
|
|
47
|
+
|
|
48
|
+
When adding a new required input to a reusable workflow, update all callers before merging.
|
|
49
|
+
Use `required: false` with a default value if backward compatibility must be preserved.
|
|
50
|
+
|
|
51
|
+
If the input is only sometimes needed, declare it as `required: false` and check it
|
|
52
|
+
at runtime inside the reusable workflow using an early-exit validation step.
|
|
53
|
+
fix_code:
|
|
54
|
+
- language: yaml
|
|
55
|
+
label: "Reusable workflow with required input declaration"
|
|
56
|
+
code: |
|
|
57
|
+
# .github/workflows/deploy-reusable.yml
|
|
58
|
+
on:
|
|
59
|
+
workflow_call:
|
|
60
|
+
inputs:
|
|
61
|
+
environment:
|
|
62
|
+
description: "Target environment"
|
|
63
|
+
required: true # Runner enforces this for all callers
|
|
64
|
+
type: string
|
|
65
|
+
version:
|
|
66
|
+
description: "Version tag"
|
|
67
|
+
required: false
|
|
68
|
+
default: "latest"
|
|
69
|
+
type: string
|
|
70
|
+
|
|
71
|
+
jobs:
|
|
72
|
+
deploy:
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/checkout@v4
|
|
76
|
+
- run: ./deploy.sh "${{ inputs.environment }}" "${{ inputs.version }}"
|
|
77
|
+
- language: yaml
|
|
78
|
+
label: "Caller workflow — always pass all required inputs"
|
|
79
|
+
code: |
|
|
80
|
+
# .github/workflows/release.yml
|
|
81
|
+
on:
|
|
82
|
+
push:
|
|
83
|
+
tags: ["v*"]
|
|
84
|
+
|
|
85
|
+
jobs:
|
|
86
|
+
deploy:
|
|
87
|
+
uses: ./.github/workflows/deploy-reusable.yml
|
|
88
|
+
with:
|
|
89
|
+
environment: production # ✅ Required input provided
|
|
90
|
+
version: ${{ github.ref_name }}
|
|
91
|
+
- language: yaml
|
|
92
|
+
label: "Backward-compatible: change required to optional with default"
|
|
93
|
+
code: |
|
|
94
|
+
# When adding new inputs to existing reusable workflows, use required: false
|
|
95
|
+
# with a default to avoid breaking existing callers.
|
|
96
|
+
on:
|
|
97
|
+
workflow_call:
|
|
98
|
+
inputs:
|
|
99
|
+
environment:
|
|
100
|
+
required: true
|
|
101
|
+
type: string
|
|
102
|
+
notify_slack: # New input — safe to add with required: false
|
|
103
|
+
required: false
|
|
104
|
+
default: "false"
|
|
105
|
+
type: string
|
|
106
|
+
prevention:
|
|
107
|
+
- "When adding a new `required: true` input to a reusable workflow, search all callers with `grep -r 'uses:.*deploy-reusable'` and update them simultaneously."
|
|
108
|
+
- "Prefer `required: false` with a sensible default when backward compatibility matters — validate the value inside the reusable workflow if needed."
|
|
109
|
+
- "Use consistent naming conventions (all underscores or all hyphens) for input names to avoid typo-related mismatches between callers and the called workflow."
|
|
110
|
+
- "Test reusable workflow changes in a draft PR that also updates all callers."
|
|
111
|
+
docs:
|
|
112
|
+
- url: "https://docs.github.com/en/actions/sharing-automations/reusing-workflows#using-inputs-and-secrets-in-a-reusable-workflow"
|
|
113
|
+
label: "GitHub Docs: Inputs in reusable workflows"
|
|
114
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_call"
|
|
115
|
+
label: "GitHub Actions: workflow_call event"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
id: triggers-011
|
|
2
|
+
title: "workflow_dispatch branches/paths Filters Silently Ignored or Cause Validation Error"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: warning
|
|
5
|
+
tags:
|
|
6
|
+
- workflow_dispatch
|
|
7
|
+
- branches
|
|
8
|
+
- paths
|
|
9
|
+
- filters
|
|
10
|
+
- trigger
|
|
11
|
+
- manual-dispatch
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "Unexpected value 'branches'"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "workflow_dispatch.*branches.*paths"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "The workflow is not valid.*Unexpected value"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "on\\.workflow_dispatch.*branches"
|
|
20
|
+
flags: "i"
|
|
21
|
+
error_messages:
|
|
22
|
+
- "The workflow is not valid. .github/workflows/deploy.yml (Line: X, Col: Y): Unexpected value 'branches'"
|
|
23
|
+
- "Unexpected value 'branches'"
|
|
24
|
+
- "Unexpected value 'tags'"
|
|
25
|
+
- "Unexpected value 'paths'"
|
|
26
|
+
root_cause: |
|
|
27
|
+
`workflow_dispatch` is a manual trigger — it runs when a user (or the API) explicitly
|
|
28
|
+
triggers the workflow. Because it is not event-driven by a push or pull request, the
|
|
29
|
+
`branches`, `paths`, `tags`, and `paths-ignore` filters that apply to push/pull_request
|
|
30
|
+
events are **not valid** for `workflow_dispatch`.
|
|
31
|
+
|
|
32
|
+
Two failure modes exist:
|
|
33
|
+
|
|
34
|
+
**Mode 1: Validation error (branches, tags)**
|
|
35
|
+
Adding `branches` or `tags` under `on.workflow_dispatch` now produces a schema
|
|
36
|
+
validation error: "Unexpected value 'branches'" or "Unexpected value 'tags'".
|
|
37
|
+
GitHub used to silently ignore these keys (pre-2022), leading to copy-paste templates
|
|
38
|
+
with these keys still floating around. The workflow may fail to queue at all.
|
|
39
|
+
|
|
40
|
+
**Mode 2: Silent ignore (paths)**
|
|
41
|
+
The `paths` filter under `on.workflow_dispatch` was silently ignored historically.
|
|
42
|
+
The workflow runs regardless of which files changed (or didn't change), because
|
|
43
|
+
workflow_dispatch has no file-change context to filter on.
|
|
44
|
+
|
|
45
|
+
Developers commonly copy a push-triggered workflow and add workflow_dispatch without
|
|
46
|
+
removing the push-specific filters, producing this mistake:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
on:
|
|
50
|
+
push:
|
|
51
|
+
branches: [main]
|
|
52
|
+
paths: ['src/**']
|
|
53
|
+
workflow_dispatch:
|
|
54
|
+
branches: [main] # ← INVALID for workflow_dispatch
|
|
55
|
+
paths: ['src/**'] # ← silently ignored for workflow_dispatch
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Tracked in github/docs#34884 ("documentation on workflow_dispatch is not
|
|
59
|
+
correct/complete") and blog post by Jon Gallant (2022): "workflow_dispatch never
|
|
60
|
+
supported branches, but GH silently ignored it."
|
|
61
|
+
fix: |
|
|
62
|
+
Remove `branches`, `paths`, `tags`, and `paths-ignore` from the `workflow_dispatch`
|
|
63
|
+
block entirely. These filters only apply to `push`, `pull_request`, and
|
|
64
|
+
`pull_request_target` triggers.
|
|
65
|
+
|
|
66
|
+
If you want manual dispatch to only be available on specific branches, use the
|
|
67
|
+
GitHub Actions UI branch selector at runtime — it allows choosing which branch to
|
|
68
|
+
run the workflow on during manual dispatch without needing a filter in the YAML.
|
|
69
|
+
|
|
70
|
+
If you need path-based conditional logic in a manually-triggered workflow, use
|
|
71
|
+
`dorny/paths-filter` or a custom `git diff` step to check which files changed after
|
|
72
|
+
the workflow starts.
|
|
73
|
+
fix_code:
|
|
74
|
+
- language: yaml
|
|
75
|
+
label: "Remove invalid filters from workflow_dispatch block"
|
|
76
|
+
code: |
|
|
77
|
+
on:
|
|
78
|
+
push:
|
|
79
|
+
branches: [main]
|
|
80
|
+
paths: ['src/**'] # ← valid here for push
|
|
81
|
+
workflow_dispatch:
|
|
82
|
+
# ← Do NOT add branches/paths/tags here — they are not supported
|
|
83
|
+
# Use inputs if you need runtime parameterization:
|
|
84
|
+
inputs:
|
|
85
|
+
environment:
|
|
86
|
+
description: "Target environment"
|
|
87
|
+
required: true
|
|
88
|
+
default: "staging"
|
|
89
|
+
type: choice
|
|
90
|
+
options: [staging, production]
|
|
91
|
+
|
|
92
|
+
- language: yaml
|
|
93
|
+
label: "Path-based conditional logic inside a manually-dispatched workflow"
|
|
94
|
+
code: |
|
|
95
|
+
on:
|
|
96
|
+
workflow_dispatch:
|
|
97
|
+
|
|
98
|
+
jobs:
|
|
99
|
+
check-and-deploy:
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
steps:
|
|
102
|
+
- uses: actions/checkout@v4
|
|
103
|
+
with:
|
|
104
|
+
fetch-depth: 2
|
|
105
|
+
|
|
106
|
+
- name: Check changed paths
|
|
107
|
+
id: filter
|
|
108
|
+
uses: dorny/paths-filter@v3
|
|
109
|
+
with:
|
|
110
|
+
filters: |
|
|
111
|
+
src:
|
|
112
|
+
- 'src/**'
|
|
113
|
+
|
|
114
|
+
- name: Deploy (only if src changed)
|
|
115
|
+
if: steps.filter.outputs.src == 'true'
|
|
116
|
+
run: echo "Deploying..."
|
|
117
|
+
|
|
118
|
+
prevention:
|
|
119
|
+
- "Never copy `branches`, `paths`, or `tags` filters from a `push` block into a `workflow_dispatch` block."
|
|
120
|
+
- "Treat `workflow_dispatch` as a parameter-based trigger, not a filter-based one — use `inputs` instead."
|
|
121
|
+
- "If the GitHub Actions linter flags 'Unexpected value branches', remove it from the workflow_dispatch block."
|
|
122
|
+
- "Use the branch selector in the GitHub Actions UI for branch scoping at runtime."
|
|
123
|
+
docs:
|
|
124
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_dispatch"
|
|
125
|
+
label: "GitHub Docs: workflow_dispatch event trigger"
|
|
126
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpathspaths-ignore"
|
|
127
|
+
label: "GitHub Docs: paths filter (push/pull_request only)"
|
|
128
|
+
- url: "https://github.com/github/docs/issues/34884"
|
|
129
|
+
label: "github/docs#34884: documentation on workflow_dispatch is not correct/complete"
|
|
130
|
+
- url: "https://blog.jongallant.com/2022/04/github-actions-failing-with-unexpected-value-branches"
|
|
131
|
+
label: "Jon Gallant: workflow_dispatch never supported branches (2022)"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
id: triggers-008
|
|
2
|
+
title: "workflow_dispatch required: true Silently Bypassed via REST API"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- workflow_dispatch
|
|
7
|
+
- inputs
|
|
8
|
+
- required
|
|
9
|
+
- rest-api
|
|
10
|
+
- gh-cli
|
|
11
|
+
- validation
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "workflow_dispatch.*inputs.*required.*true"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "inputs\\.\\w+.*==.*''"
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Input required and not supplied: {input_name}"
|
|
19
|
+
root_cause: |
|
|
20
|
+
The `required: true` flag on `workflow_dispatch` inputs is enforced **only in the GitHub
|
|
21
|
+
UI** — it renders the field as mandatory in the "Run workflow" dialog. When a workflow
|
|
22
|
+
is triggered via the REST API (`POST /repos/{owner}/{repo}/actions/workflows/{id}/dispatches`)
|
|
23
|
+
or `gh workflow run`, the `required` flag is NOT validated server-side. The workflow is
|
|
24
|
+
dispatched and runs with the input set to an empty string `""` rather than the declared
|
|
25
|
+
default or an error.
|
|
26
|
+
|
|
27
|
+
This creates a subtle silent failure: developers assume `required: true` prevents invalid
|
|
28
|
+
automation, but any API caller can trigger the workflow with no inputs at all. Steps that
|
|
29
|
+
depend on the input receive an empty string without any indication something is wrong.
|
|
30
|
+
|
|
31
|
+
**Contrast with `workflow_call` (reusable workflows):** Required inputs on `on.workflow_call`
|
|
32
|
+
ARE validated by the runner — a missing required input produces `Error: Input required and
|
|
33
|
+
not supplied: {input_name}` and the job fails immediately. This inconsistency between the
|
|
34
|
+
two dispatch types frequently surprises developers.
|
|
35
|
+
|
|
36
|
+
**GitHub API behavior (documented):** The REST API returns HTTP 204 (success) even when
|
|
37
|
+
required inputs are omitted. This is by design — GitHub treats `required: true` as a UI
|
|
38
|
+
hint only.
|
|
39
|
+
fix: |
|
|
40
|
+
Treat `required: true` as UI-only and add explicit runtime validation at the top of your
|
|
41
|
+
workflow or job for any input that is truly required.
|
|
42
|
+
|
|
43
|
+
Use an `if:` guard or a validation step that fails the workflow immediately with a clear
|
|
44
|
+
error message when a required input is empty:
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
- name: Validate required inputs
|
|
48
|
+
run: |
|
|
49
|
+
if [ -z "${{ inputs.environment }}" ]; then
|
|
50
|
+
echo "::error::Input 'environment' is required but was not provided."
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For `gh workflow run` dispatch, newer versions of the CLI (v2.40+) will prompt for
|
|
56
|
+
required inputs interactively, but non-interactive (CI) invocations must pass `-f key=value`
|
|
57
|
+
explicitly.
|
|
58
|
+
fix_code:
|
|
59
|
+
- language: yaml
|
|
60
|
+
label: "Validate required inputs at runtime with an early-exit step"
|
|
61
|
+
code: |
|
|
62
|
+
on:
|
|
63
|
+
workflow_dispatch:
|
|
64
|
+
inputs:
|
|
65
|
+
environment:
|
|
66
|
+
description: "Target environment (required)"
|
|
67
|
+
required: true
|
|
68
|
+
type: choice
|
|
69
|
+
options: [staging, production]
|
|
70
|
+
version:
|
|
71
|
+
description: "Semantic version tag to deploy (required)"
|
|
72
|
+
required: true
|
|
73
|
+
type: string
|
|
74
|
+
|
|
75
|
+
jobs:
|
|
76
|
+
deploy:
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
steps:
|
|
79
|
+
# Guard against API bypasses — required: true is UI-only
|
|
80
|
+
- name: Validate required inputs
|
|
81
|
+
run: |
|
|
82
|
+
MISSING=""
|
|
83
|
+
[ -z "${{ inputs.environment }}" ] && MISSING="$MISSING environment"
|
|
84
|
+
[ -z "${{ inputs.version }}" ] && MISSING="$MISSING version"
|
|
85
|
+
if [ -n "$MISSING" ]; then
|
|
86
|
+
echo "::error::Missing required inputs:$MISSING"
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
- uses: actions/checkout@v4
|
|
91
|
+
- name: Deploy
|
|
92
|
+
run: ./deploy.sh "${{ inputs.environment }}" "${{ inputs.version }}"
|
|
93
|
+
- language: yaml
|
|
94
|
+
label: "Triggering with gh workflow run — always pass required inputs explicitly"
|
|
95
|
+
code: |
|
|
96
|
+
# ✅ Correct: pass all required inputs
|
|
97
|
+
gh workflow run deploy.yml \
|
|
98
|
+
-f environment=staging \
|
|
99
|
+
-f version=v1.2.3
|
|
100
|
+
|
|
101
|
+
# ❌ Wrong: omits required inputs — workflow still runs, inputs are empty strings
|
|
102
|
+
gh workflow run deploy.yml
|
|
103
|
+
prevention:
|
|
104
|
+
- "Never rely on `required: true` alone for server-side enforcement — add a runtime validation step."
|
|
105
|
+
- "Document in your workflow that inputs must be passed explicitly when triggering via API or CI pipelines."
|
|
106
|
+
- "For security-critical workflows (deploy, release), use a job-level `if:` condition to block the entire job when an input is empty."
|
|
107
|
+
- "Consider reusable workflow (`workflow_call`) if you need server-enforced required inputs."
|
|
108
|
+
docs:
|
|
109
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_dispatch"
|
|
110
|
+
label: "GitHub Actions: workflow_dispatch event"
|
|
111
|
+
- url: "https://docs.github.com/en/rest/actions/workflows#create-a-workflow-dispatch-event"
|
|
112
|
+
label: "REST API: Create a workflow dispatch event"
|
|
113
|
+
- url: "https://cli.github.com/manual/gh_workflow_run"
|
|
114
|
+
label: "gh workflow run — GitHub CLI manual"
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
id: yaml-syntax-017
|
|
2
|
+
title: "Matrix include Entry Without Matching Combination Creates Unexpected Standalone Jobs"
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: warning
|
|
5
|
+
tags:
|
|
6
|
+
- matrix
|
|
7
|
+
- include
|
|
8
|
+
- strategy
|
|
9
|
+
- extra-jobs
|
|
10
|
+
- standalone
|
|
11
|
+
- job-count
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "strategy.*matrix.*include"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "matrix.*include.*extra.*job"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "include.*os.*version"
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "strategy.matrix.include"
|
|
21
|
+
- "unexpected job combination in matrix"
|
|
22
|
+
root_cause: |
|
|
23
|
+
GitHub Actions matrix `include` entries serve two purposes:
|
|
24
|
+
1. **Add variables to existing combinations** — when the entry shares at least one
|
|
25
|
+
key-value pair with an existing matrix combination, it adds/overrides variables
|
|
26
|
+
for that specific combination only.
|
|
27
|
+
2. **Create new standalone jobs** — when the entry does NOT match any existing matrix
|
|
28
|
+
combination, GitHub Actions treats it as an entirely NEW matrix job on its own.
|
|
29
|
+
|
|
30
|
+
This second behavior surprises developers who expect `include` to only add metadata
|
|
31
|
+
to existing jobs. Example that creates an unexpected extra job:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
strategy:
|
|
35
|
+
matrix:
|
|
36
|
+
os: [ubuntu-latest, windows-latest]
|
|
37
|
+
node: [18, 20]
|
|
38
|
+
include:
|
|
39
|
+
- os: macos-latest # ← No matching {os: macos-latest} in matrix
|
|
40
|
+
node: 20
|
|
41
|
+
experimental: true # ← This creates a BRAND NEW 5th job, not expected
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The matrix produces: 4 jobs from combinations (ubuntu×18, ubuntu×20,
|
|
45
|
+
windows×18, windows×20) PLUS an extra 5th job for (macos-latest × 20 × experimental).
|
|
46
|
+
|
|
47
|
+
This can also cause subtle bugs when a typo in an `include` value fails to match
|
|
48
|
+
the existing combination, silently creating an extra duplicate-like job:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
include:
|
|
52
|
+
- os: ubuntu-latest # os key matches!
|
|
53
|
+
node: 18
|
|
54
|
+
runs-long: true # Variable added to ubuntu-latest×18 job ✓
|
|
55
|
+
- os: ubuntu # TYPO: doesn't match "ubuntu-latest" → new standalone job ✗
|
|
56
|
+
node: 18
|
|
57
|
+
experimental: true
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Tracked in github/docs#23322 ("Documentation for jobs matrix strategy seems incorrect").
|
|
61
|
+
fix: |
|
|
62
|
+
**To add variables to existing combinations:** Ensure at least one key in the
|
|
63
|
+
`include` entry exactly matches an existing combination value. The match is
|
|
64
|
+
case-sensitive.
|
|
65
|
+
|
|
66
|
+
**To intentionally add a new job:** This is valid behavior — just document it clearly
|
|
67
|
+
in the workflow and be aware of the extra job in your branch protection rules.
|
|
68
|
+
|
|
69
|
+
**To prevent accidental extra jobs:** Review the job count after adding include entries.
|
|
70
|
+
The total should be: (product of all matrix dimensions) + (number of include entries
|
|
71
|
+
that don't match any combination).
|
|
72
|
+
fix_code:
|
|
73
|
+
- language: yaml
|
|
74
|
+
label: "include entry that correctly adds a variable to an existing combination"
|
|
75
|
+
code: |
|
|
76
|
+
strategy:
|
|
77
|
+
matrix:
|
|
78
|
+
os: [ubuntu-latest, windows-latest]
|
|
79
|
+
node: [18, 20]
|
|
80
|
+
include:
|
|
81
|
+
# This MATCHES ubuntu-latest×18 — adds 'experimental: true' to that job only
|
|
82
|
+
- os: ubuntu-latest # ← must exactly match the matrix value
|
|
83
|
+
node: 18 # ← must exactly match the matrix value
|
|
84
|
+
experimental: true
|
|
85
|
+
|
|
86
|
+
# This MATCHES all ubuntu-latest jobs (node 18 AND 20)
|
|
87
|
+
# because os key matches and node is not specified
|
|
88
|
+
- os: ubuntu-latest
|
|
89
|
+
runs-slow: true # added to ubuntu-latest×18 AND ubuntu-latest×20
|
|
90
|
+
|
|
91
|
+
- language: yaml
|
|
92
|
+
label: "Intentional standalone job via include (extra OS not in base matrix)"
|
|
93
|
+
code: |
|
|
94
|
+
strategy:
|
|
95
|
+
matrix:
|
|
96
|
+
os: [ubuntu-latest, windows-latest]
|
|
97
|
+
node: [18, 20]
|
|
98
|
+
include:
|
|
99
|
+
# Intentional extra job — macos is not in the base matrix
|
|
100
|
+
# Documents clearly that this creates job #5 (macos×20)
|
|
101
|
+
- os: macos-latest
|
|
102
|
+
node: 20
|
|
103
|
+
experimental: true
|
|
104
|
+
# Total jobs: 4 (base combinations) + 1 (macos standalone) = 5
|
|
105
|
+
|
|
106
|
+
- language: yaml
|
|
107
|
+
label: "Verify total job count matches expectations with exclude"
|
|
108
|
+
code: |
|
|
109
|
+
strategy:
|
|
110
|
+
matrix:
|
|
111
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
112
|
+
node: [18, 20]
|
|
113
|
+
exclude:
|
|
114
|
+
# Exclude expensive macos×18 — not needed
|
|
115
|
+
- os: macos-latest
|
|
116
|
+
node: 18
|
|
117
|
+
include:
|
|
118
|
+
# Add experimental flag to ubuntu-latest×20 only
|
|
119
|
+
- os: ubuntu-latest
|
|
120
|
+
node: 20
|
|
121
|
+
experimental: true
|
|
122
|
+
# Total jobs: (3×2) - 1 excluded = 5 jobs; 0 new from include (it matches existing)
|
|
123
|
+
|
|
124
|
+
prevention:
|
|
125
|
+
- "Always verify the total expected job count after adding `include` entries."
|
|
126
|
+
- "Ensure `include` key-value pairs exactly match (case-sensitive) the base matrix values."
|
|
127
|
+
- "Use a comment in the workflow to document the expected total number of jobs."
|
|
128
|
+
- "If a typo causes an unexpected extra job, it will show up as a job with no matching `if:` context — watch for jobs that run but shouldn't."
|
|
129
|
+
- "Use `exclude` to remove specific combinations instead of relying solely on `include` overrides."
|
|
130
|
+
docs:
|
|
131
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow#expanding-or-adding-matrix-configurations"
|
|
132
|
+
label: "GitHub Docs: Expanding or adding matrix configurations with include"
|
|
133
|
+
- url: "https://github.com/github/docs/issues/23322"
|
|
134
|
+
label: "github/docs#23322: Documentation for jobs matrix strategy seems incorrect"
|
|
135
|
+
- url: "https://stackoverflow.com/questions/78821409/github-actions-matrix-job-running-despite-false-conditions"
|
|
136
|
+
label: "Stack Overflow: GitHub Actions matrix job running despite false conditions"
|
package/package.json
CHANGED