@htekdev/actions-debugger 1.0.103 → 1.0.105

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.
@@ -0,0 +1,104 @@
1
+ id: caching-artifacts-060
2
+ title: 'actions/cache Primary key Uses Undocumented Prefix Matching — Stale Cache Restored on Partial Hit'
3
+ category: caching-artifacts
4
+ severity: silent-failure
5
+ tags:
6
+ - cache
7
+ - actions/cache
8
+ - key
9
+ - prefix-match
10
+ - stale-cache
11
+ - cache-hit
12
+ - restore-keys
13
+ patterns:
14
+ - regex: 'Cache restored from key:'
15
+ flags: 'i'
16
+ - regex: 'cache-hit.*true'
17
+ flags: 'i'
18
+ - regex: 'uses:\s*actions/cache@v[34]'
19
+ flags: 'i'
20
+ error_messages:
21
+ - "Cache restored from key: npm-cache-linux"
22
+ - "cache-hit: true"
23
+ - "Restored cache from key"
24
+ root_cause: |
25
+ GitHub Actions documentation states that `actions/cache`'s primary `key:`
26
+ parameter requires an **exact match** to trigger a cache hit. In practice,
27
+ the cache service backend uses **prefix matching** for primary keys, identical
28
+ to how `restore-keys:` works. This undocumented behavior was confirmed by the
29
+ GitHub Actions team in actions/cache#1433:
30
+
31
+ > "I've checked the internal service implementation and it appears that our
32
+ > documentation is incorrect. `key` uses prefix-matching similar to how
33
+ > `restore-keys` works."
34
+
35
+ **What this means in practice:**
36
+ - If an existing cached entry has key `npm-cache-linux` and the current run
37
+ uses key `npm-cache-linux-abc123`, the primary `key` lookup returns a hit on
38
+ `npm-cache-linux` (the existing key is a prefix of the requested key).
39
+ - The cache is restored silently with `cache-hit: true`, even though the
40
+ actual content may be outdated or from a completely different run.
41
+ - Downstream jobs that check `cache-hit == 'true'` to skip install steps will
42
+ skip them, causing builds to use stale dependencies.
43
+
44
+ This is especially problematic when:
45
+ 1. A broad, short key (e.g., `linux-npm`) was saved long ago and a new, more
46
+ specific key (e.g., `linux-npm-${{ hashFiles('package-lock.json') }}`)
47
+ happens to have the old key as a prefix.
48
+ 2. CI changes add a version suffix to cache keys, but old caches from before
49
+ the change are still present and match as prefixes.
50
+ fix: |
51
+ **Option 1 (recommended): Use unique, non-colliding key prefixes**
52
+ Design your cache key so that no broader key from a previous run can be a prefix
53
+ of your current key. Include the hash of lock files or use a version token that
54
+ changes with major dependency updates.
55
+
56
+ **Option 2: Rotate keys by appending a version token**
57
+ Prefix with a cache version that you bump whenever you want to guarantee a cold
58
+ start, preventing old keys from matching.
59
+
60
+ **Option 3: Use `restore-keys:` intentionally**
61
+ If you want prefix fallback, make it explicit with `restore-keys:` and verify
62
+ what you're getting. Check `cache-hit` output and run install even on partial
63
+ hits when correctness matters.
64
+
65
+ **Option 4: Delete stale caches via API**
66
+ Use `gh extension install actions/gh-actions-cache` or the REST API to delete
67
+ old broad-key caches that would match as prefixes, preventing unexpected hits.
68
+ fix_code:
69
+ - language: yaml
70
+ label: 'Use versioned prefix to prevent stale prefix matches'
71
+ code: |
72
+ - uses: actions/cache@v4
73
+ with:
74
+ path: ~/.npm
75
+ # v2 prefix ensures old "v1" caches never match as prefixes
76
+ key: v2-npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
77
+ restore-keys: |
78
+ v2-npm-${{ runner.os }}-
79
+ - language: yaml
80
+ label: 'Re-install deps on partial cache hit to avoid stale cache'
81
+ code: |
82
+ - uses: actions/cache@v4
83
+ id: npm-cache
84
+ with:
85
+ path: ~/.npm
86
+ key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
87
+ restore-keys: npm-${{ runner.os }}-
88
+
89
+ - name: Install dependencies
90
+ # Always install when not an exact hit to avoid stale partial cache
91
+ if: steps.npm-cache.outputs.cache-hit != 'true'
92
+ run: npm ci
93
+ prevention:
94
+ - 'Always include a lockfile hash in cache keys: `key: npm-${{ runner.os }}-${{ hashFiles(''**/package-lock.json'') }}`'
95
+ - 'Prefix cache keys with a version token (e.g., `v1-`, `v2-`) so you can easily invalidate all old caches by bumping the version.'
96
+ - 'Do not rely solely on `cache-hit == true` to skip install steps; consider re-running `npm install --prefer-offline` even on hits to handle partial prefix matches.'
97
+ - 'Periodically audit and delete stale cache entries via the GitHub REST API or `gh actions-cache` extension.'
98
+ docs:
99
+ - url: 'https://github.com/actions/cache/issues/1433'
100
+ label: 'actions/cache#1433: Cache key uses prefix matching — documentation is incorrect'
101
+ - url: 'https://github.com/actions/cache/issues/1385'
102
+ label: 'actions/cache#1385: Unexpected cache hit from prefix match'
103
+ - url: 'https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows'
104
+ label: 'GitHub Docs: Caching dependencies to speed up workflows'
@@ -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'
@@ -0,0 +1,137 @@
1
+ id: concurrency-timing-049
2
+ title: 'Batch workflow_dispatch Runs Silently Cancel Each Other via Static Concurrency Group'
3
+ category: concurrency-timing
4
+ severity: silent-failure
5
+ tags:
6
+ - concurrency
7
+ - workflow-dispatch
8
+ - batch
9
+ - cancel
10
+ - static-group
11
+ - lost-run
12
+ - dispatch-api
13
+ patterns:
14
+ - regex: 'This run was cancelled'
15
+ flags: 'i'
16
+ - regex: 'concurrency:\s*\n\s*group:\s*[''"]?\$\{\{\s*github\.workflow\s*\}\}'
17
+ flags: 'im'
18
+ - regex: 'workflow_dispatch.*concurrency|concurrency.*workflow_dispatch'
19
+ flags: 'ims'
20
+ error_messages:
21
+ - "This run was cancelled."
22
+ - "Run has been cancelled because it is in a concurrency group with a newer run."
23
+ - "Canceling since a more recent run was started"
24
+ root_cause: |
25
+ When a workflow uses a **static concurrency group** — one that does not
26
+ incorporate any per-run unique value like `github.run_id` or a user-supplied
27
+ input — and multiple `workflow_dispatch` runs are triggered in rapid succession
28
+ (e.g., from a batch API script, a label webhook fan-out, or automation), GitHub
29
+ Actions cancels all but the last run.
30
+
31
+ GitHub Actions enforces a limit of **1 running + 1 pending** run per concurrency
32
+ group. Each new dispatch replaces the existing pending run:
33
+
34
+ 1. Run A starts → becomes "running"
35
+ 2. Run B dispatched → becomes "pending" (waits for A)
36
+ 3. Run C dispatched → **cancels Run B** (B was pending), C becomes "pending"
37
+ 4. Run D dispatched → **cancels Run C**, D becomes "pending"
38
+ 5. Only Run D eventually runs after Run A finishes.
39
+
40
+ This produces silent data loss when each dispatch carries different `inputs:`
41
+ (e.g., different artifact IDs, tenant IDs, or tags to process). All runs except
42
+ the last are silently discarded with "This run was cancelled."
43
+
44
+ Commonly triggered by:
45
+ - A webhook handler that dispatches one workflow run per event, with bursts of
46
+ events arriving simultaneously.
47
+ - Automation scripts that loop over a list and call `gh workflow run` for each item.
48
+ - A cron job that fans out to per-repo dispatches sharing the same workflow name.
49
+
50
+ Note: this is distinct from `concurrency-timing-040` (push + workflow_dispatch
51
+ sharing a group) — this issue occurs with ONLY workflow_dispatch runs sharing a
52
+ static group with no event-name differentiation.
53
+ fix: |
54
+ **Option 1 (recommended): Include a unique input value in the concurrency group**
55
+ If each dispatch has a unique identifier as an input (e.g., a tenant ID, artifact
56
+ ID, or item key), include it in the concurrency group so different dispatches
57
+ get independent groups.
58
+
59
+ **Option 2: Include github.run_id in the concurrency group**
60
+ Using `github.run_id` makes every run its own concurrency group — effectively
61
+ disabling concurrency entirely. Use only if all runs are important and must
62
+ complete.
63
+
64
+ **Option 3: Use cancel-in-progress: false (no-queue mode)**
65
+ With `cancel-in-progress: false`, new pending runs do not cancel existing pending
66
+ ones. The 1-running + 1-pending limit still applies, but the *last* queued run
67
+ (not the most recent dispatch) survives. This still means high-burst dispatches
68
+ lose most runs.
69
+
70
+ **Option 4: Use a queue-based architecture**
71
+ For high-volume fan-out, avoid workflow_dispatch entirely. Instead, push items to
72
+ a queue (SQS, GitHub Issues, database) and have a single polling workflow process
73
+ items from the queue sequentially or in bounded parallel batches.
74
+ fix_code:
75
+ - language: yaml
76
+ label: 'Include unique input in concurrency group to isolate per-dispatch runs'
77
+ code: |
78
+ on:
79
+ workflow_dispatch:
80
+ inputs:
81
+ item_id:
82
+ description: 'Unique identifier for this dispatch'
83
+ required: true
84
+ type: string
85
+
86
+ concurrency:
87
+ # Each unique item_id gets its own concurrency slot — no cross-dispatch cancellation
88
+ group: ${{ github.workflow }}-${{ inputs.item_id }}
89
+ cancel-in-progress: true
90
+ - language: yaml
91
+ label: 'Use github.run_id to guarantee all dispatches run independently'
92
+ code: |
93
+ on:
94
+ workflow_dispatch:
95
+
96
+ concurrency:
97
+ # Every run is its own group — effectively disables concurrency queueing
98
+ group: ${{ github.workflow }}-${{ github.run_id }}
99
+ cancel-in-progress: false
100
+ - language: yaml
101
+ label: 'Fan-out batch dispatch via matrix instead of separate workflow runs'
102
+ code: |
103
+ # Instead of dispatching N separate workflow runs (which cancel each other),
104
+ # dispatch ONE run with a matrix input to process all items in parallel.
105
+ on:
106
+ workflow_dispatch:
107
+ inputs:
108
+ item_ids:
109
+ description: 'JSON array of item IDs to process'
110
+ required: true
111
+ type: string
112
+
113
+ concurrency:
114
+ group: ${{ github.workflow }}-${{ github.ref }}
115
+ cancel-in-progress: false
116
+
117
+ jobs:
118
+ process-items:
119
+ strategy:
120
+ matrix:
121
+ item_id: ${{ fromJSON(inputs.item_ids) }}
122
+ runs-on: ubuntu-latest
123
+ steps:
124
+ - name: Process item
125
+ run: echo "Processing ${{ matrix.item_id }}"
126
+ prevention:
127
+ - 'Never use a static concurrency group (e.g., `group: ${{ github.workflow }}`) for workflows dispatched in batch — include a unique per-dispatch key.'
128
+ - 'When dispatching multiple runs from a script, add a unique ID as an input and include it in the concurrency group.'
129
+ - 'For high-volume fan-out workloads, use a single matrix job instead of N separate workflow_dispatch runs.'
130
+ - 'Monitor workflow cancellation counts in the Actions UI — unexpected "This run was cancelled" on dispatch events signals a concurrency group collision.'
131
+ docs:
132
+ - url: 'https://github.com/github/gh-aw/issues/19467'
133
+ label: 'github/gh-aw#19467: Batch workflow_dispatch runs cancel each other via static concurrency group'
134
+ - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-concurrency'
135
+ label: 'GitHub Docs: Using concurrency — 1 running + 1 pending limit per group'
136
+ - url: 'https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/manually-running-a-workflow'
137
+ label: 'GitHub Docs: Manually running a workflow'
@@ -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'
@@ -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,99 @@
1
+ id: runner-environment-174
2
+ title: 'Sparse Checkout Config Persists Across Workflow Runs on Self-Hosted Runners'
3
+ category: runner-environment
4
+ severity: silent-failure
5
+ tags:
6
+ - sparse-checkout
7
+ - self-hosted
8
+ - git-config
9
+ - workspace-reuse
10
+ - checkout
11
+ - non-ephemeral
12
+ patterns:
13
+ - regex: 'core\.sparseCheckout\s*=\s*true'
14
+ flags: 'i'
15
+ - regex: 'sparse.checkout.*persist|persist.*sparse.checkout'
16
+ flags: 'i'
17
+ - regex: 'git sparse-checkout disable'
18
+ flags: 'i'
19
+ error_messages:
20
+ - "Run actions/checkout@v4"
21
+ - "Checkout was successful but expected files are missing from working directory"
22
+ - "sparse-checkout: /run/git/config: sparseCheckout = true"
23
+ root_cause: |
24
+ On non-ephemeral self-hosted runners, the workspace directory (e.g.
25
+ `/home/runner/work/my-repo/my-repo`) is reused across multiple workflow runs
26
+ on the same runner machine. When `actions/checkout` runs with `sparse-checkout:`
27
+ options, it sets `core.sparseCheckout = true` in the repository's `.git/config`
28
+ file.
29
+
30
+ In older versions of `actions/checkout` (before the fix in commit aadec89,
31
+ merged ~2025), the `disableSparseCheckout()` method reset `core.sparseCheckout`
32
+ only in `.git/config.worktree` (used in git worktrees), but NOT in the main
33
+ `.git/config`. As a result:
34
+
35
+ - **Workflow run N** uses `sparse-checkout:` → sets `core.sparseCheckout = true`
36
+ in `.git/config`.
37
+ - **Workflow run N+1** on the same runner does NOT use `sparse-checkout:`, but
38
+ inherits the stale `core.sparseCheckout = true` setting from the reused workspace.
39
+ - `actions/checkout` reports success, but the working directory contains only
40
+ the files that matched the previous run's sparse-checkout patterns. All other
41
+ files silently absent.
42
+
43
+ This is distinct from the same-job sticky cone mode issue (sf-008), which covers
44
+ two `actions/checkout` calls within a single job sharing sparse state. This issue
45
+ manifests across entirely separate workflow runs on a shared workspace.
46
+
47
+ Confirmed in actions/checkout#2249 (published Aug 2025) and actions/checkout#1992.
48
+ fix: |
49
+ **Option 1 (recommended): Upgrade to actions/checkout v4.2.0+**
50
+ The bug was fixed in commit aadec89. Pinning to a patched release ensures
51
+ `disableSparseCheckout()` explicitly sets `core.sparseCheckout = false` in
52
+ `.git/config` when no `sparse-checkout:` input is provided.
53
+
54
+ **Option 2: Add an explicit `git sparse-checkout disable` step**
55
+ Add this step at the beginning of workflows that do NOT use sparse-checkout
56
+ on a runner that may previously have run sparse-checkout workflows:
57
+
58
+ **Option 3: Use ephemeral self-hosted runners**
59
+ Configure runners so each job gets a fresh workspace (e.g., ARC ephemeral mode,
60
+ EC2 runners with fresh AMIs). Ephemeral runners eliminate all cross-run workspace
61
+ contamination.
62
+
63
+ **Option 4: Configure runner to clean workspace between runs**
64
+ Set `ACTIONS_RUNNER_CLEAN_WORKSPACE=true` in the runner environment (where
65
+ supported) to force workspace cleanup between job runs.
66
+ fix_code:
67
+ - language: yaml
68
+ label: 'Upgrade to patched actions/checkout version'
69
+ code: |
70
+ steps:
71
+ - uses: actions/checkout@v4 # Use v4.2.0+ for the sparse-checkout fix
72
+ # No sparse-checkout options — will correctly restore full tree
73
+ - language: yaml
74
+ label: 'Explicit sparse-checkout reset before checkout on shared runners'
75
+ code: |
76
+ steps:
77
+ - name: Reset sparse-checkout (safety guard for shared self-hosted runners)
78
+ shell: bash
79
+ run: |
80
+ if git -C "${{ github.workspace }}" rev-parse --git-dir > /dev/null 2>&1; then
81
+ git -C "${{ github.workspace }}" sparse-checkout disable 2>/dev/null || true
82
+ git -C "${{ github.workspace }}" config --unset core.sparseCheckout 2>/dev/null || true
83
+ fi
84
+
85
+ - uses: actions/checkout@v4
86
+ prevention:
87
+ - 'On self-hosted runners, always pin `actions/checkout` to v4.2.0+ which fixes the sparse-checkout persistence bug.'
88
+ - 'Add a `git sparse-checkout disable` guard step at the start of any workflow that runs on a non-ephemeral self-hosted runner.'
89
+ - 'Use ephemeral runners (fresh environment per job) to eliminate all cross-run workspace contamination.'
90
+ - 'Do not mix sparse-checkout and full-checkout workflows on the same non-ephemeral runner without resetting `.git/config` between runs.'
91
+ docs:
92
+ - url: 'https://github.com/actions/checkout/issues/2249'
93
+ label: 'actions/checkout#2249: Sparse-checkout configuration persists across workflows on self-hosted runners'
94
+ - url: 'https://github.com/actions/checkout/issues/1992'
95
+ label: 'actions/checkout#1992: Sparscheckout breaks cached repository on self-hosted runners'
96
+ - url: 'https://github.com/actions/checkout/commit/aadec899646c8e0f34c52d9219c2faac36626b55'
97
+ label: 'Fix commit aadec89: Explicitly disable sparseCheckout in .git/config when not using sparse-checkout'
98
+ - url: 'https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners'
99
+ label: 'GitHub Docs: About GitHub-hosted runners (ephemeral vs self-hosted)'
@@ -0,0 +1,99 @@
1
+ id: yaml-syntax-066
2
+ title: 'hashFiles() Newline Delimiter in Single String Returns Empty — Use Multiple Arguments'
3
+ category: yaml-syntax
4
+ severity: silent-failure
5
+ tags:
6
+ - hashfiles
7
+ - expression
8
+ - cache-key
9
+ - empty-string
10
+ - multifile
11
+ - glob
12
+ - newline-delimiter
13
+ patterns:
14
+ - regex: 'hashFiles\([''"].*\\n.*[''"]'
15
+ flags: 'i'
16
+ - regex: 'hashFiles\s*\(\s*[''"][^''",]+\\n[^''",]+[''"]'
17
+ flags: 'i'
18
+ error_messages:
19
+ - "${{ hashFiles('src/**\npackage.json') }}"
20
+ - "${{ hashFiles('**/*.lock\n**/*.toml') }}"
21
+ - "hashFiles result: ''"
22
+ root_cause: |
23
+ The `@actions/glob` internal implementation documents that file patterns can be
24
+ separated by newline (`\n`) characters when passed as a single string. This causes
25
+ developers to write `hashFiles('file.ts\nfile.tsx')` expecting it to hash both
26
+ files.
27
+
28
+ However, inside a GitHub Actions expression (`${{ ... }}`), string literals do
29
+ NOT interpret escape sequences. The `\n` in `'file.ts\nfile.tsx'` is a literal
30
+ two-character sequence: backslash + "n". The glob function then looks for a file
31
+ literally named `file.ts\nfile.tsx` — which does not exist — and returns an empty
32
+ string `''`.
33
+
34
+ **Result:**
35
+ - `cache-key: ${{ hashFiles('package.json\nyarn.lock') }}` → cache key ends with
36
+ an empty string, causing ALL runs to collide on the same constant prefix key.
37
+ - `if: hashFiles('src/**\ntest/**') != ''` → always false (no hash returned), so
38
+ cache/condition logic silently skips.
39
+ - No error is raised; the expression just evaluates to `''` silently.
40
+
41
+ Confirmed in actions/runner#3467 and actions/runner#4049.
42
+
43
+ Note: `hashFiles()` DOES accept multiple comma-separated string arguments, and
44
+ each argument is a full glob pattern. The variadic call form works correctly.
45
+ fix: |
46
+ Use multiple comma-separated arguments to `hashFiles()` instead of a
47
+ newline-delimited single string.
48
+
49
+ `hashFiles('pattern1', 'pattern2', ...)` correctly hashes all files matching
50
+ any of the provided glob patterns.
51
+
52
+ If you are building a dynamic list of patterns from a context value or step
53
+ output, write the patterns to a file and hash that file, or hash each pattern
54
+ in separate expressions and combine.
55
+ fix_code:
56
+ - language: yaml
57
+ label: 'Use multiple arguments (correct) instead of newline-delimited string (broken)'
58
+ code: |
59
+ # BROKEN — \n is a literal backslash-n, not a newline; returns empty string
60
+ # key: npm-${{ hashFiles('package.json\npackage-lock.json') }}
61
+
62
+ # CORRECT — comma-separated arguments, hashes both files
63
+ - uses: actions/cache@v4
64
+ with:
65
+ path: ~/.npm
66
+ key: npm-${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json') }}
67
+ - language: yaml
68
+ label: 'Multiple lock files hashed with variadic hashFiles()'
69
+ code: |
70
+ - uses: actions/cache@v4
71
+ with:
72
+ path: |
73
+ ~/.npm
74
+ ~/.cache/pip
75
+ key: multi-${{ runner.os }}-${{ hashFiles('**/package-lock.json', '**/requirements*.txt', '**/Pipfile.lock') }}
76
+ restore-keys: |
77
+ multi-${{ runner.os }}-
78
+ - language: yaml
79
+ label: 'Conditional step using hashFiles() with multiple arguments'
80
+ code: |
81
+ # BROKEN — always evaluates to '' because \n is not a newline escape
82
+ # if: ${{ hashFiles('src/**\ntest/**') != '' }}
83
+
84
+ # CORRECT
85
+ - name: Check if source files changed
86
+ if: ${{ hashFiles('src/**', 'test/**') != '' }}
87
+ run: echo "Source files found"
88
+ prevention:
89
+ - 'Never use `\n` as a pattern delimiter inside a `hashFiles()` call in a workflow expression — it is a literal backslash-n, not a newline.'
90
+ - 'Use multiple comma-separated arguments: `hashFiles(''pattern1'', ''pattern2'')` to hash files from multiple glob patterns.'
91
+ - 'When a `hashFiles()` result is used as a cache key suffix, validate it is non-empty by running the workflow once and checking the logged cache key.'
92
+ - 'If you suspect an empty hash, add a debug step: `run: echo "hash=${{ hashFiles(''**/*.lock'') }}"` to verify the value before using it in a key.'
93
+ docs:
94
+ - url: 'https://github.com/actions/runner/issues/3467'
95
+ label: 'actions/runner#3467: hashFiles does not follow docs — newline delimiter in string returns empty'
96
+ - url: 'https://github.com/actions/runner/issues/4049'
97
+ label: 'actions/runner#4049: hashFiles newline pattern delimiter not interpreted in expressions'
98
+ - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#hashfiles'
99
+ label: 'GitHub Docs: hashFiles function — variadic arguments'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@htekdev/actions-debugger",
3
- "version": "1.0.103",
3
+ "version": "1.0.105",
4
4
  "description": "65+ real GitHub Actions errors, queryable by agents. CLI + MCP server + Copilot skills + error database.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",