@htekdev/actions-debugger 1.0.102 → 1.0.104
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-key-missing-runner-arch-x64-arm64-bleed.yml +101 -0
- package/errors/caching-artifacts/upload-artifact-v4-hidden-files-excluded-by-default.yml +82 -0
- package/errors/concurrency-timing/github-run-id-concurrency-group-disables-cancel-in-progress.yml +90 -0
- package/errors/concurrency-timing/pull-request-target-base-ref-concurrency-serializes-all-prs.yml +78 -0
- package/errors/concurrency-timing/workflow-dispatch-schedule-shared-concurrency-kills-scheduled-run.yml +87 -0
- package/errors/known-unsolved/matrix-job-limit-256-silent-truncation.yml +116 -0
- package/errors/silent-failures/github-event-before-zeros-new-branch.yml +99 -0
- package/errors/silent-failures/pull-request-target-checks-out-base-not-head.yml +97 -0
- package/package.json +1 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
id: caching-artifacts-058
|
|
2
|
+
title: 'actions/cache key using runner.os bleeds between x64 and ARM64 Linux runners — wrong architecture binaries restored'
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- cache-key
|
|
7
|
+
- runner-arch
|
|
8
|
+
- arm64
|
|
9
|
+
- x64
|
|
10
|
+
- architecture
|
|
11
|
+
- cross-arch
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'exec format error'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'cannot execute binary file.*Exec format error'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'ELF.*wrong ELF class'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- "bash: /path/to/binary: cannot execute binary file: Exec format error"
|
|
21
|
+
- "error while loading shared libraries: wrong ELF class: ELFCLASS32"
|
|
22
|
+
- "qemu: uncaught target signal 11 (Segmentation fault)"
|
|
23
|
+
root_cause: |
|
|
24
|
+
runner.os returns "Linux" for both ubuntu-24.04 (x86_64) and ubuntu-24.04-arm
|
|
25
|
+
(arm64/aarch64) runners. A cache key built with only ${{ runner.os }} will match
|
|
26
|
+
caches created on EITHER architecture, silently restoring the wrong binaries.
|
|
27
|
+
|
|
28
|
+
Example: a workflow compiles a Rust binary or installs native npm packages
|
|
29
|
+
(esbuild, rollup, sharp) on an x86_64 runner, then the same cache key is used on
|
|
30
|
+
an ARM64 runner. The cached x86_64 ELF binary is restored and immediately fails
|
|
31
|
+
with "Exec format error" — or worse, a segfault with no obvious cause.
|
|
32
|
+
|
|
33
|
+
This affects:
|
|
34
|
+
- Compiled binaries cached in the build layer
|
|
35
|
+
- Native npm modules (esbuild, @swc/core, sharp, canvas)
|
|
36
|
+
- Python packages with C extensions (.so files)
|
|
37
|
+
- Go, Rust, C++ build artifacts
|
|
38
|
+
- Any runner.os-keyed cache shared across an architecture matrix
|
|
39
|
+
|
|
40
|
+
The silent aspect: the cache HITS (green cache-hit output), no error is reported
|
|
41
|
+
until the cached binary is actually executed, often many steps later.
|
|
42
|
+
fix: |
|
|
43
|
+
Add runner.arch to any cache key that stores architecture-dependent content.
|
|
44
|
+
runner.arch returns "X64" or "ARM64" on Linux runners, disambiguating them.
|
|
45
|
+
|
|
46
|
+
Change:
|
|
47
|
+
key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.lock') }}
|
|
48
|
+
|
|
49
|
+
To:
|
|
50
|
+
key: ${{ runner.os }}-${{ runner.arch }}-build-${{ hashFiles('**/Cargo.lock') }}
|
|
51
|
+
|
|
52
|
+
This applies to all cache keys for compiled artifacts, native modules, or
|
|
53
|
+
any platform-specific binary output. Pure source-code or platform-agnostic
|
|
54
|
+
caches (e.g., Go module downloads, pip source distributions) do not need runner.arch.
|
|
55
|
+
fix_code:
|
|
56
|
+
- language: yaml
|
|
57
|
+
label: 'WRONG — runner.os only, bleeds between x64 and ARM64'
|
|
58
|
+
code: |
|
|
59
|
+
- uses: actions/cache@v4
|
|
60
|
+
with:
|
|
61
|
+
path: target/release
|
|
62
|
+
# BAD: runner.os = "Linux" for both x64 and ARM64
|
|
63
|
+
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
64
|
+
- language: yaml
|
|
65
|
+
label: 'FIX — include runner.arch in cache key'
|
|
66
|
+
code: |
|
|
67
|
+
- uses: actions/cache@v4
|
|
68
|
+
with:
|
|
69
|
+
path: target/release
|
|
70
|
+
# GOOD: runner.arch = "X64" or "ARM64", prevents cross-arch cache hits
|
|
71
|
+
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
72
|
+
- language: yaml
|
|
73
|
+
label: 'Full matrix example with architecture-aware cache keys'
|
|
74
|
+
code: |
|
|
75
|
+
jobs:
|
|
76
|
+
build:
|
|
77
|
+
strategy:
|
|
78
|
+
matrix:
|
|
79
|
+
runner: [ubuntu-24.04, ubuntu-24.04-arm]
|
|
80
|
+
runs-on: ${{ matrix.runner }}
|
|
81
|
+
steps:
|
|
82
|
+
- uses: actions/checkout@v4
|
|
83
|
+
- uses: actions/cache@v4
|
|
84
|
+
with:
|
|
85
|
+
path: |
|
|
86
|
+
~/.cargo/registry
|
|
87
|
+
target/
|
|
88
|
+
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
89
|
+
- run: cargo build --release
|
|
90
|
+
prevention:
|
|
91
|
+
- 'Always include runner.arch in cache keys when the cached content contains compiled or architecture-specific binaries'
|
|
92
|
+
- 'Audit existing cache keys whenever adding ARM64 runners to a workflow that already runs on x86_64'
|
|
93
|
+
- 'Use a restore-key that does NOT include arch only as a fallback, not as a primary key'
|
|
94
|
+
- 'Test fresh (clear cache) runs on each architecture to verify binaries execute correctly'
|
|
95
|
+
docs:
|
|
96
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/contexts#runner-context'
|
|
97
|
+
label: 'GitHub Actions runner context — runner.os and runner.arch'
|
|
98
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows'
|
|
99
|
+
label: 'Caching dependencies to speed up workflows'
|
|
100
|
+
- url: 'https://github.com/actions/cache'
|
|
101
|
+
label: 'actions/cache — cache key examples and best practices'
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
id: caching-artifacts-059
|
|
2
|
+
title: 'actions/upload-artifact@v4 excludes hidden files by default — dotfiles silently missing from artifacts'
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- upload-artifact
|
|
7
|
+
- v4
|
|
8
|
+
- hidden-files
|
|
9
|
+
- dotfiles
|
|
10
|
+
- include-hidden-files
|
|
11
|
+
- silent-omission
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'uses:\s*actions/upload-artifact@v4'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'include-hidden-files:\s*false'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- 'No files were found with the provided path'
|
|
19
|
+
- 'Uploading 0 files'
|
|
20
|
+
- 'Found no files matching'
|
|
21
|
+
root_cause: |
|
|
22
|
+
actions/upload-artifact@v4 introduced the include-hidden-files input, which defaults
|
|
23
|
+
to false. Any file or directory whose name begins with a dot (.) is silently excluded
|
|
24
|
+
from the artifact upload. The action completes with a success status and reports the
|
|
25
|
+
number of files uploaded — only inspecting that count reveals that dotfiles were
|
|
26
|
+
skipped.
|
|
27
|
+
|
|
28
|
+
Common files and directories affected:
|
|
29
|
+
.env, .env.production, .env.local — environment configuration
|
|
30
|
+
.npmrc, .nvmrc, .node-version — Node.js toolchain config
|
|
31
|
+
.htaccess — Apache web server config
|
|
32
|
+
.browserslistrc, .babelrc — build tool configuration
|
|
33
|
+
.next/, .nuxt/, .svelte-kit/ — framework build output directories
|
|
34
|
+
.vitepress/, .docusaurus/ — documentation build output
|
|
35
|
+
|
|
36
|
+
Workflows migrated from v3 to v4 silently break: the artifact is created and
|
|
37
|
+
downloaded without any error, but dependent steps fail when the expected dotfiles
|
|
38
|
+
are absent. Framework deployments (Next.js, Nuxt, SvelteKit) that upload their build
|
|
39
|
+
output are most frequently affected since their output directories are hidden.
|
|
40
|
+
fix: |
|
|
41
|
+
Set include-hidden-files: true on the upload step whenever dotfiles or dot-directories
|
|
42
|
+
must be preserved in the artifact:
|
|
43
|
+
|
|
44
|
+
- uses: actions/upload-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: build-output
|
|
47
|
+
path: ./dist
|
|
48
|
+
include-hidden-files: true
|
|
49
|
+
|
|
50
|
+
Alternatively, explicitly list only the required hidden files in the path input to
|
|
51
|
+
avoid uploading the entire directory with hidden file inclusion.
|
|
52
|
+
fix_code:
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: 'Add include-hidden-files: true to preserve dotfiles and dot-directories'
|
|
55
|
+
code: |
|
|
56
|
+
- name: Upload build artifact
|
|
57
|
+
uses: actions/upload-artifact@v4
|
|
58
|
+
with:
|
|
59
|
+
name: build-output
|
|
60
|
+
path: ./.next # Next.js build output is a hidden directory
|
|
61
|
+
include-hidden-files: true # required — excluded by default in v4
|
|
62
|
+
- language: yaml
|
|
63
|
+
label: 'Alternative: explicitly list hidden files instead of directory glob'
|
|
64
|
+
code: |
|
|
65
|
+
- name: Upload build artifacts including dotfiles
|
|
66
|
+
uses: actions/upload-artifact@v4
|
|
67
|
+
with:
|
|
68
|
+
name: build-output
|
|
69
|
+
path: |
|
|
70
|
+
./dist
|
|
71
|
+
./.env.production
|
|
72
|
+
./.nvmrc
|
|
73
|
+
./.npmrc
|
|
74
|
+
prevention:
|
|
75
|
+
- 'Set include-hidden-files: true when uploading directories known to contain dotfiles or dot-directories (.next/, .nuxt/, .svelte-kit/, etc.)'
|
|
76
|
+
- 'After migrating from upload-artifact@v3 to @v4, inspect the artifact file count in the run log — a drop indicates hidden files were excluded'
|
|
77
|
+
- 'Add a post-upload verification step: download the artifact in a subsequent job and assert the expected dotfiles exist'
|
|
78
|
+
docs:
|
|
79
|
+
- url: 'https://github.com/actions/upload-artifact/blob/main/README.md'
|
|
80
|
+
label: 'actions/upload-artifact README: include-hidden-files input'
|
|
81
|
+
- url: 'https://github.com/actions/upload-artifact/blob/main/docs/migration-v3-to-v4.md'
|
|
82
|
+
label: 'Migration guide: upload-artifact v3 to v4'
|
package/errors/concurrency-timing/github-run-id-concurrency-group-disables-cancel-in-progress.yml
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
id: concurrency-timing-048
|
|
2
|
+
title: 'github.run_id in concurrency group key disables cancel-in-progress — every run is unique, unlimited parallelism'
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- cancel-in-progress
|
|
8
|
+
- github-run-id
|
|
9
|
+
- unique-key
|
|
10
|
+
- parallelism
|
|
11
|
+
- anti-pattern
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'group:.*\$\{\{\s*github\.run_id\s*\}\}'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'group:.*github\.run_id'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- 'Old workflow runs are not being cancelled despite cancel-in-progress: true'
|
|
19
|
+
- 'Multiple concurrent runs executing simultaneously'
|
|
20
|
+
root_cause: |
|
|
21
|
+
github.run_id is always unique per workflow run — it is the numeric identifier
|
|
22
|
+
assigned at run creation time. When used in the concurrency group key, every run
|
|
23
|
+
evaluates to a different group name. No two runs ever share the same group, so
|
|
24
|
+
cancel-in-progress: true has nothing to cancel. All runs proceed simultaneously.
|
|
25
|
+
|
|
26
|
+
This anti-pattern typically originates from developers who experienced unwanted
|
|
27
|
+
concurrency cancellations and "fixed" the problem by adding a unique value to the
|
|
28
|
+
group key. The immediate cancellation symptom disappears but a more serious problem
|
|
29
|
+
is introduced: on high-velocity branches, dozens of in-progress CI runs pile up
|
|
30
|
+
simultaneously, exhausting the available runner pool and increasing costs.
|
|
31
|
+
|
|
32
|
+
Related: github.sha in the group key has the same effect (concurrency-timing-030),
|
|
33
|
+
but github.run_id is more severe. github.sha can repeat when a commit is re-run;
|
|
34
|
+
github.run_id is unconditionally unique per run and per re-run attempt, so
|
|
35
|
+
cancel-in-progress is permanently disabled with no exceptions.
|
|
36
|
+
fix: |
|
|
37
|
+
Remove github.run_id from the concurrency group key. Use github.ref (for branch-scoped
|
|
38
|
+
deduplication) or github.head_ref (for PR-scoped deduplication). If the original
|
|
39
|
+
goal was to prevent manual dispatch runs from being cancelled, scope the unique key
|
|
40
|
+
to workflow_dispatch events only using a conditional expression:
|
|
41
|
+
|
|
42
|
+
group: >-
|
|
43
|
+
${{ github.workflow }}-
|
|
44
|
+
${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
|
|
45
|
+
fix_code:
|
|
46
|
+
- language: yaml
|
|
47
|
+
label: 'Wrong: github.run_id makes every run unique — cancel-in-progress is dead code'
|
|
48
|
+
code: |
|
|
49
|
+
# WRONG: every run gets its own unique group — cancel-in-progress never fires
|
|
50
|
+
concurrency:
|
|
51
|
+
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.run_id }}
|
|
52
|
+
cancel-in-progress: true # never cancels anything
|
|
53
|
+
|
|
54
|
+
jobs:
|
|
55
|
+
build:
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v4
|
|
59
|
+
- language: yaml
|
|
60
|
+
label: 'Correct: branch-scoped group key enables cancel-in-progress'
|
|
61
|
+
code: |
|
|
62
|
+
# CORRECT: all pushes to same branch share one group
|
|
63
|
+
concurrency:
|
|
64
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
65
|
+
cancel-in-progress: true
|
|
66
|
+
|
|
67
|
+
jobs:
|
|
68
|
+
build:
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/checkout@v4
|
|
72
|
+
- language: yaml
|
|
73
|
+
label: 'Advanced: isolate manual dispatch runs while deduplicating automated runs'
|
|
74
|
+
code: |
|
|
75
|
+
# Manual dispatch runs are never cancelled; push/PR runs cancel old runs on same branch
|
|
76
|
+
concurrency:
|
|
77
|
+
group: >-
|
|
78
|
+
${{ github.workflow }}-
|
|
79
|
+
${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
|
|
80
|
+
cancel-in-progress: true
|
|
81
|
+
prevention:
|
|
82
|
+
- 'Never add github.run_id to a concurrency group key when the goal is to cancel old runs — it defeats cancel-in-progress entirely'
|
|
83
|
+
- 'Verify cancel-in-progress by triggering two rapid pushes and confirming the first run is cancelled in the Actions UI'
|
|
84
|
+
- 'If cancellation of specific trigger types is unwanted, use event_name-conditional keys instead of unique values'
|
|
85
|
+
- 'Monitor runner utilization — unbounded parallel runs will exhaust available runners and increase billing'
|
|
86
|
+
docs:
|
|
87
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-concurrency'
|
|
88
|
+
label: 'GitHub Docs: Using concurrency'
|
|
89
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/contexts#github-context'
|
|
90
|
+
label: 'GitHub Docs: github context — run_id'
|
package/errors/concurrency-timing/pull-request-target-base-ref-concurrency-serializes-all-prs.yml
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
id: concurrency-timing-046
|
|
2
|
+
title: 'pull_request_target github.ref resolves to base branch — all PRs targeting same branch share one concurrency slot'
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- pull_request_target
|
|
8
|
+
- github-ref
|
|
9
|
+
- base-branch
|
|
10
|
+
- serialization
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'on:\s*\n\s+pull_request_target:'
|
|
13
|
+
flags: 'ms'
|
|
14
|
+
- regex: 'group:.*github\.ref'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
error_messages:
|
|
17
|
+
- 'This run was automatically queued'
|
|
18
|
+
- 'Waiting for a pending job to complete'
|
|
19
|
+
root_cause: |
|
|
20
|
+
On pull_request events, github.ref is the head branch ref (e.g., refs/heads/my-feature).
|
|
21
|
+
On pull_request_target events, however, github.ref is the BASE branch ref (e.g.,
|
|
22
|
+
refs/heads/main) — the branch the PR targets. This is a security design choice that
|
|
23
|
+
prevents untrusted fork code from influencing the event ref.
|
|
24
|
+
|
|
25
|
+
When a concurrency group key uses github.ref in a pull_request_target workflow, all
|
|
26
|
+
open PRs targeting the same base branch (main, develop, etc.) evaluate to the identical
|
|
27
|
+
group key. Only one PR's CI can run at a time; all others queue behind it. On repos
|
|
28
|
+
with many open PRs, this effectively serializes all PR workflows into a single lane
|
|
29
|
+
with potentially multi-hour wait times.
|
|
30
|
+
|
|
31
|
+
The correct per-PR identifier in pull_request_target workflows is
|
|
32
|
+
github.event.pull_request.number — unique per PR regardless of base branch.
|
|
33
|
+
fix: |
|
|
34
|
+
Replace github.ref with github.event.pull_request.number in concurrency group keys
|
|
35
|
+
for pull_request_target workflows. PR numbers are unique per repository and stable
|
|
36
|
+
across new commits to the same PR:
|
|
37
|
+
|
|
38
|
+
concurrency:
|
|
39
|
+
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
|
40
|
+
cancel-in-progress: true
|
|
41
|
+
fix_code:
|
|
42
|
+
- language: yaml
|
|
43
|
+
label: 'Wrong: github.ref = base branch on pull_request_target — all PRs share one slot'
|
|
44
|
+
code: |
|
|
45
|
+
# WRONG: github.ref = refs/heads/main for ALL PRs targeting main
|
|
46
|
+
on:
|
|
47
|
+
pull_request_target:
|
|
48
|
+
|
|
49
|
+
concurrency:
|
|
50
|
+
group: ${{ github.workflow }}-${{ github.ref }} # same for every PR!
|
|
51
|
+
cancel-in-progress: true
|
|
52
|
+
- language: yaml
|
|
53
|
+
label: 'Correct: use github.event.pull_request.number for per-PR isolation'
|
|
54
|
+
code: |
|
|
55
|
+
# CORRECT: PR number is unique per PR, stable across pushes to same PR
|
|
56
|
+
on:
|
|
57
|
+
pull_request_target:
|
|
58
|
+
|
|
59
|
+
concurrency:
|
|
60
|
+
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
|
61
|
+
cancel-in-progress: true
|
|
62
|
+
|
|
63
|
+
jobs:
|
|
64
|
+
lint:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v4
|
|
68
|
+
with:
|
|
69
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
70
|
+
prevention:
|
|
71
|
+
- 'In pull_request_target workflows, never use github.ref as the sole differentiator in concurrency groups — it is the base branch ref'
|
|
72
|
+
- 'Use github.event.pull_request.number to scope concurrency to individual PRs'
|
|
73
|
+
- 'Audit concurrency groups when migrating workflows from pull_request to pull_request_target'
|
|
74
|
+
docs:
|
|
75
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request_target'
|
|
76
|
+
label: 'GitHub Docs: pull_request_target event'
|
|
77
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-concurrency'
|
|
78
|
+
label: 'GitHub Docs: Using concurrency'
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
id: concurrency-timing-047
|
|
2
|
+
title: 'workflow_dispatch and schedule sharing a concurrency group — manual dispatch silently cancels queued scheduled run'
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- workflow_dispatch
|
|
8
|
+
- schedule
|
|
9
|
+
- cancel-in-progress
|
|
10
|
+
- scheduled-job
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'schedule:.*\n.*workflow_dispatch:|workflow_dispatch:.*\n.*schedule:'
|
|
13
|
+
flags: 'ms'
|
|
14
|
+
- regex: 'group:\s*.*github\.workflow'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
error_messages:
|
|
17
|
+
- 'This run was automatically cancelled'
|
|
18
|
+
- 'Canceling since a higher priority waiting run was found'
|
|
19
|
+
root_cause: |
|
|
20
|
+
When a workflow is triggered by both schedule and workflow_dispatch and uses a shared
|
|
21
|
+
concurrency group key that does not include github.event_name (e.g.,
|
|
22
|
+
group: ${{ github.workflow }}-${{ github.ref }}), all runs — regardless of trigger
|
|
23
|
+
type — map to the same concurrency slot.
|
|
24
|
+
|
|
25
|
+
When cancel-in-progress: true is set, any manual workflow_dispatch run immediately
|
|
26
|
+
cancels the scheduled run that may be actively executing. A developer who triggers
|
|
27
|
+
a manual run can silently kill a scheduled maintenance job, nightly report, or backup
|
|
28
|
+
workflow with no warning or notification.
|
|
29
|
+
|
|
30
|
+
When cancel-in-progress: false, GitHub only keeps one pending run per concurrency
|
|
31
|
+
slot. A queued scheduled run is silently displaced when the dispatch run arrives.
|
|
32
|
+
The scheduled run is discarded with no error, no notification, and no log entry.
|
|
33
|
+
|
|
34
|
+
This affects maintenance workflows, report generators, data sync jobs, and backup
|
|
35
|
+
workflows where workflow_dispatch is added for "emergency manual runs" after the
|
|
36
|
+
initial scheduled implementation.
|
|
37
|
+
fix: |
|
|
38
|
+
Include github.event_name in the concurrency group key to give each trigger type
|
|
39
|
+
its own independent concurrency slot:
|
|
40
|
+
|
|
41
|
+
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
|
42
|
+
|
|
43
|
+
This ensures that manual dispatch runs and scheduled runs never compete for the
|
|
44
|
+
same slot and cannot cancel each other.
|
|
45
|
+
fix_code:
|
|
46
|
+
- language: yaml
|
|
47
|
+
label: 'Wrong: schedule and workflow_dispatch share the same concurrency slot'
|
|
48
|
+
code: |
|
|
49
|
+
on:
|
|
50
|
+
schedule:
|
|
51
|
+
- cron: '0 3 * * *' # nightly at 03:00 UTC
|
|
52
|
+
workflow_dispatch:
|
|
53
|
+
|
|
54
|
+
# WRONG: both trigger types evaluate to the same group key
|
|
55
|
+
concurrency:
|
|
56
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
57
|
+
cancel-in-progress: true # dispatch run kills the in-progress scheduled run
|
|
58
|
+
- language: yaml
|
|
59
|
+
label: 'Correct: include github.event_name to isolate trigger types'
|
|
60
|
+
code: |
|
|
61
|
+
on:
|
|
62
|
+
schedule:
|
|
63
|
+
- cron: '0 3 * * *'
|
|
64
|
+
workflow_dispatch:
|
|
65
|
+
|
|
66
|
+
# CORRECT: schedule and workflow_dispatch get separate concurrency slots
|
|
67
|
+
concurrency:
|
|
68
|
+
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
|
69
|
+
cancel-in-progress: true
|
|
70
|
+
|
|
71
|
+
jobs:
|
|
72
|
+
nightly:
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/checkout@v4
|
|
76
|
+
- run: ./nightly-tasks.sh
|
|
77
|
+
prevention:
|
|
78
|
+
- 'Always include ${{ github.event_name }} in the concurrency group for workflows with multiple trigger event types'
|
|
79
|
+
- 'After adding workflow_dispatch to an existing scheduled workflow, verify scheduled runs are not silently displaced'
|
|
80
|
+
- 'Monitor the Actions tab run history — consecutive scheduled run gaps may indicate silent cancellations'
|
|
81
|
+
docs:
|
|
82
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-concurrency'
|
|
83
|
+
label: 'GitHub Docs: Using concurrency'
|
|
84
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule'
|
|
85
|
+
label: 'GitHub Docs: schedule event'
|
|
86
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_dispatch'
|
|
87
|
+
label: 'GitHub Docs: workflow_dispatch event'
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
id: known-unsolved-056
|
|
2
|
+
title: 'GitHub Actions matrix is silently capped at 256 jobs — excess jobs are dropped with no warning'
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: limitation
|
|
5
|
+
tags:
|
|
6
|
+
- matrix
|
|
7
|
+
- job-limit
|
|
8
|
+
- 256
|
|
9
|
+
- large-matrix
|
|
10
|
+
- silent-truncation
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'matrix.*256|256.*matrix.*job'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'some jobs.*not.*generated.*matrix'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
error_messages:
|
|
17
|
+
- "The matrix contains more jobs than the 256 job limit"
|
|
18
|
+
root_cause: |
|
|
19
|
+
GitHub Actions enforces a hard limit of 256 jobs per matrix expansion. When a
|
|
20
|
+
matrix generates more than 256 combinations, GitHub silently drops all jobs beyond
|
|
21
|
+
the first 256 — no error is raised, no warning appears in the workflow run UI,
|
|
22
|
+
and no notification is sent to the repository owner.
|
|
23
|
+
|
|
24
|
+
The truncation is silent and deterministic: GitHub processes matrix entries in
|
|
25
|
+
the order they are defined and stops at 256. If your matrix has 300 combinations,
|
|
26
|
+
jobs 257–300 simply do not run, but the workflow completes successfully.
|
|
27
|
+
|
|
28
|
+
Common scenarios that hit this limit:
|
|
29
|
+
- Large monorepo test suites that fan out one job per package/service (50+ packages
|
|
30
|
+
× multiple OS/version combinations)
|
|
31
|
+
- Parameterized test sharding where both shard-count and OS are in the matrix
|
|
32
|
+
- Dynamic matrices built from directory listings or API calls
|
|
33
|
+
- Security scanning jobs that matrix over many dependency versions
|
|
34
|
+
|
|
35
|
+
GitHub Actions documentation mentions the 256 limit but does not surface it as
|
|
36
|
+
an error at runtime, making it easy to miss for months.
|
|
37
|
+
|
|
38
|
+
Note: Enterprise plans have the same 256-job limit per matrix. There is no paid
|
|
39
|
+
tier that raises this cap.
|
|
40
|
+
fix: |
|
|
41
|
+
There is no way to raise the 256-job limit. Workarounds:
|
|
42
|
+
|
|
43
|
+
1. Restructure the matrix to combine dimensions:
|
|
44
|
+
Instead of os × version × shard (3×4×30=360), use a pre-computed list of
|
|
45
|
+
only the combinations you actually need (often far fewer than the full product).
|
|
46
|
+
|
|
47
|
+
2. Shard within a single job instead of across jobs:
|
|
48
|
+
Use a single "test" job that runs a subset of tests in each runner process,
|
|
49
|
+
controlled by TOTAL_SHARDS / SHARD_INDEX environment variables, without
|
|
50
|
+
a separate matrix job per shard.
|
|
51
|
+
|
|
52
|
+
3. Split into multiple workflows:
|
|
53
|
+
Use workflow_call to invoke sub-workflows, each handling a subset of jobs.
|
|
54
|
+
Each workflow has its own 256-job budget.
|
|
55
|
+
|
|
56
|
+
4. Use a dynamic include-only matrix:
|
|
57
|
+
Build the matrix list externally (e.g., a script) and output it as JSON,
|
|
58
|
+
then ensure the JSON has at most 256 entries. Add a CI check that fails if
|
|
59
|
+
the list exceeds 255 (leaving headroom for include additions).
|
|
60
|
+
fix_code:
|
|
61
|
+
- language: yaml
|
|
62
|
+
label: 'Pre-compute matrix list to stay under 256 (dynamic matrix pattern)'
|
|
63
|
+
code: |
|
|
64
|
+
jobs:
|
|
65
|
+
setup:
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
outputs:
|
|
68
|
+
matrix: ${{ steps.build-matrix.outputs.matrix }}
|
|
69
|
+
steps:
|
|
70
|
+
- uses: actions/checkout@v4
|
|
71
|
+
- id: build-matrix
|
|
72
|
+
run: |
|
|
73
|
+
# Build matrix, enforce limit
|
|
74
|
+
MATRIX=$(find packages -name package.json -maxdepth 2 | \
|
|
75
|
+
head -256 | jq -R -s -c 'split("\n") | map(select(. != ""))')
|
|
76
|
+
echo "matrix={\"pkg\":$MATRIX}" >> "$GITHUB_OUTPUT"
|
|
77
|
+
|
|
78
|
+
test:
|
|
79
|
+
needs: setup
|
|
80
|
+
strategy:
|
|
81
|
+
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- run: echo "Testing ${{ matrix.pkg }}"
|
|
85
|
+
- language: yaml
|
|
86
|
+
label: 'Split large matrix across two workflows via workflow_call'
|
|
87
|
+
code: |
|
|
88
|
+
# .github/workflows/test-batch-a.yml
|
|
89
|
+
on:
|
|
90
|
+
workflow_call:
|
|
91
|
+
jobs:
|
|
92
|
+
test:
|
|
93
|
+
strategy:
|
|
94
|
+
matrix:
|
|
95
|
+
pkg: [svc-a, svc-b, svc-c] # First 128 services
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
steps:
|
|
98
|
+
- run: echo "Testing ${{ matrix.pkg }}"
|
|
99
|
+
|
|
100
|
+
# .github/workflows/ci.yml
|
|
101
|
+
on: [push]
|
|
102
|
+
jobs:
|
|
103
|
+
batch-a:
|
|
104
|
+
uses: ./.github/workflows/test-batch-a.yml
|
|
105
|
+
batch-b:
|
|
106
|
+
uses: ./.github/workflows/test-batch-b.yml
|
|
107
|
+
prevention:
|
|
108
|
+
- 'Monitor your matrix job count as the codebase grows — add a pre-flight check that fails CI if the computed matrix exceeds 255'
|
|
109
|
+
- 'Prefer horizontal test sharding within a single job over per-package matrix expansion for large monorepos'
|
|
110
|
+
- 'Document the matrix budget in your CONTRIBUTING.md so contributors understand the constraint'
|
|
111
|
+
- 'Use include: sparingly — each include entry adds to the 256-job budget'
|
|
112
|
+
docs:
|
|
113
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow#about-matrix-strategies'
|
|
114
|
+
label: 'GitHub Actions — About matrix strategies (256-job limit documented)'
|
|
115
|
+
- url: 'https://docs.github.com/en/actions/administering-github-actions/usage-limits-billing-and-administration#usage-limits'
|
|
116
|
+
label: 'GitHub Actions usage limits — job matrix limits'
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
id: silent-failures-092
|
|
2
|
+
title: 'github.event.before is all zeros on first push to a new branch — git diff fails with unknown revision'
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- push-event
|
|
7
|
+
- before-sha
|
|
8
|
+
- new-branch
|
|
9
|
+
- git-diff
|
|
10
|
+
- changed-files
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'fatal: ambiguous argument.*0{40}'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'unknown revision.*0{10,}.*working tree'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'fatal: bad object 0{40}'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
error_messages:
|
|
19
|
+
- "fatal: ambiguous argument '0000000000000000000000000000000000000000': unknown revision or path not in the working tree"
|
|
20
|
+
- "fatal: bad object 0000000000000000000000000000000000000000"
|
|
21
|
+
root_cause: |
|
|
22
|
+
When a branch is pushed to GitHub for the first time, there is no previous state.
|
|
23
|
+
GitHub sets github.event.before to the all-zeros SHA (40 zeros) as a sentinel
|
|
24
|
+
meaning "this is a brand-new branch with no prior history."
|
|
25
|
+
|
|
26
|
+
Any workflow step that uses this SHA in git commands fails because 40 zeros is not
|
|
27
|
+
a valid Git object reference. Common patterns that break:
|
|
28
|
+
git diff ${{ github.event.before }} ${{ github.sha }} --name-only
|
|
29
|
+
git log ${{ github.event.before }}..${{ github.sha }}
|
|
30
|
+
|
|
31
|
+
This silently breaks:
|
|
32
|
+
- Changed-file detection scripts that decide which jobs to run
|
|
33
|
+
- Automated changelog generators that compare before→after SHAs
|
|
34
|
+
- CI selective-run logic based on modified paths
|
|
35
|
+
- Commit-message linters that inspect newly added commits only
|
|
36
|
+
|
|
37
|
+
The failure is non-obvious because the workflow may succeed on subsequent pushes
|
|
38
|
+
to the same branch (where before is a real SHA), leading developers to believe
|
|
39
|
+
the issue was transient or environmental.
|
|
40
|
+
fix: |
|
|
41
|
+
Guard against the all-zeros SHA before performing git operations:
|
|
42
|
+
|
|
43
|
+
Option 1 — Skip the step/job on new-branch pushes with an if: condition:
|
|
44
|
+
if: github.event.before != '0000000000000000000000000000000000000000'
|
|
45
|
+
|
|
46
|
+
Option 2 — Fall back to the initial commit when before is zeros:
|
|
47
|
+
BEFORE="${{ github.event.before }}"
|
|
48
|
+
if [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
|
|
49
|
+
BEFORE=$(git rev-list --max-parents=0 HEAD)
|
|
50
|
+
fi
|
|
51
|
+
git diff "$BEFORE" "${{ github.sha }}" --name-only
|
|
52
|
+
|
|
53
|
+
Option 3 — Use tj-actions/changed-files which handles this case automatically.
|
|
54
|
+
fix_code:
|
|
55
|
+
- language: yaml
|
|
56
|
+
label: 'Guard with if: to skip diff on new-branch first push'
|
|
57
|
+
code: |
|
|
58
|
+
jobs:
|
|
59
|
+
diff-check:
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
# Skip when there is no before commit (first push to new branch)
|
|
62
|
+
if: github.event.before != '0000000000000000000000000000000000000000'
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/checkout@v4
|
|
65
|
+
with:
|
|
66
|
+
fetch-depth: 0
|
|
67
|
+
- name: Get changed files
|
|
68
|
+
run: git diff ${{ github.event.before }} ${{ github.sha }} --name-only
|
|
69
|
+
- language: yaml
|
|
70
|
+
label: 'Shell fallback — use initial commit when before SHA is zeros'
|
|
71
|
+
code: |
|
|
72
|
+
- name: Get changed files (handles new branch)
|
|
73
|
+
run: |
|
|
74
|
+
BEFORE="${{ github.event.before }}"
|
|
75
|
+
if [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
|
|
76
|
+
# New branch — fall back to root commit
|
|
77
|
+
BEFORE=$(git rev-list --max-parents=0 HEAD)
|
|
78
|
+
fi
|
|
79
|
+
git diff "$BEFORE" "${{ github.sha }}" --name-only
|
|
80
|
+
- language: yaml
|
|
81
|
+
label: 'Use tj-actions/changed-files (handles new-branch automatically)'
|
|
82
|
+
code: |
|
|
83
|
+
- uses: tj-actions/changed-files@v44
|
|
84
|
+
id: changed
|
|
85
|
+
with:
|
|
86
|
+
since_last_remote_commit: true
|
|
87
|
+
- run: echo "${{ steps.changed.outputs.all_changed_files }}"
|
|
88
|
+
prevention:
|
|
89
|
+
- 'Always check github.event.before against the 40-zero sentinel before using it in git commands'
|
|
90
|
+
- 'Use maintained actions such as tj-actions/changed-files that already handle new-branch edge cases'
|
|
91
|
+
- 'Add fetch-depth: 0 to actions/checkout so full git history is available for diff operations'
|
|
92
|
+
- 'Test workflows on a branch that has never had CI run before to catch first-push failures early'
|
|
93
|
+
docs:
|
|
94
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#push'
|
|
95
|
+
label: 'GitHub Actions — push event payload (before/after SHA fields)'
|
|
96
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/contexts#github-context'
|
|
97
|
+
label: 'GitHub context reference — github.event.before'
|
|
98
|
+
- url: 'https://github.com/tj-actions/changed-files'
|
|
99
|
+
label: 'tj-actions/changed-files — handles new-branch and force-push edge cases'
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
id: silent-failures-093
|
|
2
|
+
title: 'pull_request_target workflow checks out base branch by default — PR head changes are not tested'
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- pull-request-target
|
|
7
|
+
- checkout
|
|
8
|
+
- base-branch
|
|
9
|
+
- head-ref
|
|
10
|
+
- fork-pr
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'pull_request_target.*actions/checkout'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'HEAD is now at.*base.*not.*head'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
error_messages:
|
|
17
|
+
- "Actions attempted to checkout a merge commit for a pull request but the merge commit was unavailable"
|
|
18
|
+
root_cause: |
|
|
19
|
+
When a workflow is triggered by pull_request_target, the GITHUB_SHA and the default
|
|
20
|
+
checkout ref point to the BASE branch of the PR — not the PR head commit.
|
|
21
|
+
|
|
22
|
+
This is intentional security behavior: pull_request_target runs in the context of
|
|
23
|
+
the base repository (with full secrets access), so GitHub deliberately avoids
|
|
24
|
+
running untrusted code from fork PRs by defaulting to the safe base branch state.
|
|
25
|
+
|
|
26
|
+
The result is a silent failure: CI appears to run and pass, but it is actually
|
|
27
|
+
testing the base branch code, not the contributor's changes. No error is emitted.
|
|
28
|
+
|
|
29
|
+
Developers commonly hit this when:
|
|
30
|
+
- Migrating from pull_request to pull_request_target for Dependabot secrets access
|
|
31
|
+
- Setting up labeler/auto-assign bots that also want to run tests
|
|
32
|
+
- Building required-status checks that use pull_request_target for fork support
|
|
33
|
+
|
|
34
|
+
Even with actions/checkout@v4, the default ref for pull_request_target is
|
|
35
|
+
github.sha which resolves to the base branch HEAD — not the merge commit.
|
|
36
|
+
fix: |
|
|
37
|
+
Explicitly check out the PR head ref when you need to test the contributor's code:
|
|
38
|
+
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
with:
|
|
41
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
42
|
+
|
|
43
|
+
WARNING: This executes untrusted code from fork contributors in an environment
|
|
44
|
+
with full repository secrets. Only do this after careful security review:
|
|
45
|
+
- Gate on github.event.pull_request.head.repo.fork to distinguish fork vs internal
|
|
46
|
+
- Never expose secrets to steps that run contributor code
|
|
47
|
+
- Consider using a separate workflow_run trigger instead for fork PRs that need secrets
|
|
48
|
+
|
|
49
|
+
For safe automation (labelers, assignment bots), use pull_request_target but do NOT
|
|
50
|
+
check out the head ref — these actions only need the event payload, not the code.
|
|
51
|
+
fix_code:
|
|
52
|
+
- language: yaml
|
|
53
|
+
label: 'Check out PR head for internal PRs only (safe pattern)'
|
|
54
|
+
code: |
|
|
55
|
+
on:
|
|
56
|
+
pull_request_target:
|
|
57
|
+
types: [opened, synchronize]
|
|
58
|
+
|
|
59
|
+
jobs:
|
|
60
|
+
test:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
# Only run contributor code for PRs from the same repo, not forks
|
|
63
|
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@v4
|
|
66
|
+
with:
|
|
67
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
68
|
+
- run: npm test
|
|
69
|
+
- language: yaml
|
|
70
|
+
label: 'Safe bot usage — do NOT check out head (labeler example)'
|
|
71
|
+
code: |
|
|
72
|
+
on:
|
|
73
|
+
pull_request_target:
|
|
74
|
+
types: [opened]
|
|
75
|
+
|
|
76
|
+
jobs:
|
|
77
|
+
label:
|
|
78
|
+
runs-on: ubuntu-latest
|
|
79
|
+
permissions:
|
|
80
|
+
pull-requests: write
|
|
81
|
+
steps:
|
|
82
|
+
# No checkout needed — labeler only reads the event payload
|
|
83
|
+
- uses: actions/labeler@v5
|
|
84
|
+
with:
|
|
85
|
+
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
86
|
+
prevention:
|
|
87
|
+
- 'Always explicitly set ref: when using actions/checkout with pull_request_target triggers'
|
|
88
|
+
- 'Never check out the PR head commit in a pull_request_target workflow that has access to secrets unless you have verified the source repo'
|
|
89
|
+
- 'Prefer pull_request for standard CI — use pull_request_target only when secrets access is required from fork workflows'
|
|
90
|
+
- 'Read the GitHub security hardening guide before using pull_request_target with any code execution steps'
|
|
91
|
+
docs:
|
|
92
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request_target'
|
|
93
|
+
label: 'GitHub Actions — pull_request_target event (security context and checkout behavior)'
|
|
94
|
+
- url: 'https://securitylab.github.com/research/github-actions-preventing-pwn-requests/'
|
|
95
|
+
label: 'GitHub Security Lab — Preventing pwn requests with pull_request_target'
|
|
96
|
+
- url: 'https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections'
|
|
97
|
+
label: 'GitHub Actions security hardening guide'
|
package/package.json
CHANGED