@htekdev/actions-debugger 1.0.28 → 1.0.29

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,78 @@
1
+ id: 'caching-artifacts-028'
2
+ title: 'hashFiles() returns empty string when no files match pattern, causing cache key collision across all runs'
3
+ category: caching-artifacts
4
+ severity: silent-failure
5
+ tags:
6
+ - hashfiles
7
+ - cache-key
8
+ - empty-string
9
+ - collision
10
+ - lock-file
11
+ - monorepo
12
+ patterns:
13
+ - regex: 'hashFiles\('
14
+ flags: 'i'
15
+ error_messages:
16
+ - 'Cache hit for key:'
17
+ - 'hashFiles result is empty string'
18
+ root_cause: |
19
+ When hashFiles('**/package-lock.json') finds no matching files, it returns an empty
20
+ string instead of failing. A cache key like:
21
+ ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
22
+ evaluates to:
23
+ Linux-node-
24
+ (no hash suffix). All workflow runs — regardless of their actual dependency state —
25
+ then share this single cache key. The first run to complete saves its node_modules
26
+ under this key; every subsequent run gets a stale cache hit with potentially outdated
27
+ or wrong dependencies.
28
+
29
+ The cache-hit output shows 'true' and the step succeeds with no warning. Developers
30
+ see unexpectedly fast runs (cache always hits) but may encounter subtle dependency
31
+ staleness bugs.
32
+
33
+ Behavior varies across versions: actions/toolkit prior to 1.9.0 threw an exception
34
+ on empty results; later versions silently return empty string, making the collapsing
35
+ key the default behavior for repositories without the expected lock file.
36
+ fix: |
37
+ Guard hashFiles() with a fallback value so the cache key is never incomplete when
38
+ no matching files exist. Using || github.sha or || github.run_id ensures each run
39
+ gets a unique key when no lock file is present, preventing stale cache collisions.
40
+ fix_code:
41
+ - language: yaml
42
+ label: 'Add fallback to hashFiles to prevent empty cache key'
43
+ code: |
44
+ - uses: actions/cache@v4
45
+ with:
46
+ path: ~/.npm
47
+ # Fallback to github.sha when no lock file exists — prevents key collision
48
+ key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') || github.sha }}
49
+ restore-keys: |
50
+ ${{ runner.os }}-node-
51
+ - language: yaml
52
+ label: 'Verify cache is populated before relying on it'
53
+ code: |
54
+ - uses: actions/cache@v4
55
+ id: npm-cache
56
+ with:
57
+ path: ~/.npm
58
+ key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') || github.sha }}
59
+
60
+ - name: Confirm cache restored correctly
61
+ if: steps.npm-cache.outputs.cache-hit == 'true'
62
+ run: |
63
+ if [ ! -d ~/.npm ]; then
64
+ echo "Cache hit claimed but directory missing — likely empty-key collision"
65
+ exit 1
66
+ fi
67
+ prevention:
68
+ - 'Always add || github.sha fallback after hashFiles() in cache keys'
69
+ - 'Use actions built-in caching (setup-node cache: npm) which handles missing lock files safely'
70
+ - 'In monorepos without a root-level lock file, construct keys from per-package hash patterns'
71
+ - 'Test cache behavior in branches or forks where lock files might not yet exist'
72
+ docs:
73
+ - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#hashfiles'
74
+ label: 'GitHub Docs: hashFiles() function'
75
+ - url: 'https://github.com/actions/cache/issues/1175'
76
+ label: 'actions/cache#1175: hashFiles empty result causes key collision'
77
+ - url: 'https://github.com/actions/toolkit/blob/main/packages/glob/README.md'
78
+ label: 'actions/toolkit: glob — hashFiles behavior on no match'
@@ -0,0 +1,80 @@
1
+ id: 'caching-artifacts-027'
2
+ title: 'restore-keys fallback matches cross-OS or cross-architecture cache, restoring incompatible binaries'
3
+ category: caching-artifacts
4
+ severity: silent-failure
5
+ tags:
6
+ - cache
7
+ - restore-keys
8
+ - cross-platform
9
+ - architecture
10
+ - arm64
11
+ - runner-os
12
+ patterns:
13
+ - regex: 'restore-keys:'
14
+ flags: 'i'
15
+ error_messages:
16
+ - 'Cache restored from key'
17
+ - 'Exec format error'
18
+ - 'cannot execute binary file: Exec format error'
19
+ root_cause: |
20
+ restore-keys performs prefix matching against ALL cached entries in the repository,
21
+ regardless of operating system or CPU architecture. When a restore-keys prefix is
22
+ shorter than the primary cache key and omits runner.os or runner.arch, a cache
23
+ saved on one platform can be silently restored on a different one.
24
+
25
+ Example: primary key Linux-x64-node-abc123, restore-keys Linux-node- will match
26
+ a Linux ARM64 cache saved as Linux-arm64-node-xyz789. The ARM64 node_modules
27
+ contains native addon binaries (esbuild, sqlite3, etc.) compiled for ARM64; when
28
+ restored on an x64 runner, they fail at runtime with "Exec format error."
29
+
30
+ This became a widespread issue after GitHub introduced macOS ARM64 (M1/M2) runners
31
+ in 2023 and Linux ARM64 runners in 2024. Teams adding new runner architectures
32
+ to existing matrix builds often expose this silently.
33
+
34
+ The cache-hit output evaluates to 'true' even for cross-architecture restores,
35
+ providing no indication that the restored content may be incompatible.
36
+ fix: |
37
+ Always include runner.os AND runner.arch in every level of restore-keys, mirroring
38
+ whatever isolation is present in the primary cache key. No restore-keys prefix should
39
+ ever be shorter than the architecture scope of the primary key.
40
+ fix_code:
41
+ - language: yaml
42
+ label: 'Include runner.os and runner.arch in all restore-keys levels'
43
+ code: |
44
+ - uses: actions/cache@v4
45
+ with:
46
+ path: ~/.npm
47
+ # Primary key includes full OS and architecture isolation
48
+ key: ${{ runner.os }}-${{ runner.arch }}-node-${{ hashFiles('**/package-lock.json') }}
49
+ # Every fallback level maintains OS plus architecture isolation
50
+ restore-keys: |
51
+ ${{ runner.os }}-${{ runner.arch }}-node-
52
+ ${{ runner.os }}-${{ runner.arch }}-
53
+ - language: yaml
54
+ label: 'Matrix build with per-arch cache keys'
55
+ code: |
56
+ strategy:
57
+ matrix:
58
+ os: [ubuntu-latest, macos-latest, windows-latest]
59
+ arch: [x64, arm64]
60
+ steps:
61
+ - uses: actions/cache@v4
62
+ with:
63
+ path: |
64
+ ~/.cargo/registry
65
+ target/
66
+ key: ${{ matrix.os }}-${{ matrix.arch }}-rust-${{ hashFiles('**/Cargo.lock') }}
67
+ restore-keys: |
68
+ ${{ matrix.os }}-${{ matrix.arch }}-rust-
69
+ prevention:
70
+ - 'Always include runner.os AND runner.arch in every level of restore-keys'
71
+ - 'Audit cache configurations when adding new runner OS or arch combinations to matrix builds'
72
+ - 'Add a verification step after cache restore to confirm a native binary executes correctly'
73
+ - 'When migrating from x64-only to multi-arch, update all restore-keys at the same time'
74
+ docs:
75
+ - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows#matching-a-cache-key'
76
+ label: 'GitHub Docs: Matching a cache key'
77
+ - url: 'https://github.com/actions/cache#inputs'
78
+ label: 'actions/cache README: restore-keys input'
79
+ - url: 'https://github.com/actions/cache/issues/1660'
80
+ label: 'actions/cache#1660: restore-keys cross-architecture match'
@@ -0,0 +1,86 @@
1
+ id: 'concurrency-timing-023'
2
+ title: 'Cleanup jobs with if: cancelled() do not run when workflow is canceled by concurrency group'
3
+ category: concurrency-timing
4
+ severity: silent-failure
5
+ tags:
6
+ - concurrency
7
+ - cancelled
8
+ - cleanup
9
+ - cancel-in-progress
10
+ - if-condition
11
+ patterns:
12
+ - regex: 'cancel-in-progress:\s*true'
13
+ flags: 'i'
14
+ - regex: 'if:\s*cancelled\(\)'
15
+ flags: 'i'
16
+ error_messages:
17
+ - 'This run has been cancelled.'
18
+ - 'Job cancelled by a newer workflow run'
19
+ root_cause: |
20
+ When cancel-in-progress: true cancels a workflow run because a new run was queued in the same
21
+ concurrency group, GitHub cancels the entire workflow run at the infrastructure level before
22
+ individual job-level if: conditions are evaluated. As a result, jobs with if: cancelled() or
23
+ if: always() defined to run after a canceled parent job are themselves canceled before they
24
+ can be dispatched to a runner.
25
+
26
+ This is distinct from a job failing or being manually canceled: concurrency-group cancellation
27
+ is an external platform signal. In practice, cleanup jobs that rely on if: cancelled() may
28
+ start briefly but are killed mid-execution if they happen to be in-flight when the cancel
29
+ propagates.
30
+ fix: |
31
+ Use a separate workflow triggered by workflow_run with types: [completed] as the cleanup
32
+ trigger rather than relying on in-workflow if: cancelled() jobs. The workflow_run approach
33
+ fires reliably regardless of how the parent workflow ended.
34
+
35
+ If the in-workflow approach is required, use if: always() rather than if: cancelled() and
36
+ ensure the cleanup job starts quickly (lightweight first step) to reduce the window during
37
+ which the cancellation signal can reach it.
38
+ fix_code:
39
+ - language: yaml
40
+ label: 'Reliable cleanup via separate workflow_run trigger'
41
+ code: |
42
+ # .github/workflows/cleanup.yml
43
+ on:
44
+ workflow_run:
45
+ workflows: ['CI']
46
+ types: [completed]
47
+
48
+ jobs:
49
+ cleanup:
50
+ runs-on: ubuntu-latest
51
+ if: >-
52
+ ${{ github.event.workflow_run.conclusion == 'cancelled' ||
53
+ github.event.workflow_run.conclusion == 'failure' }}
54
+ steps:
55
+ - name: Run cleanup
56
+ run: echo "Cleaning up after ${{ github.event.workflow_run.conclusion }}"
57
+ - language: yaml
58
+ label: 'Best-effort if:always() with fast first step'
59
+ code: |
60
+ jobs:
61
+ build:
62
+ runs-on: ubuntu-latest
63
+ steps:
64
+ - run: ./run-tests.sh
65
+
66
+ cleanup:
67
+ needs: build
68
+ if: always()
69
+ runs-on: ubuntu-latest
70
+ steps:
71
+ - name: Signal start immediately
72
+ run: echo "Cleanup starting"
73
+ - name: Do cleanup
74
+ run: ./cleanup.sh
75
+ prevention:
76
+ - 'Do not rely solely on if: cancelled() for critical cleanup when cancel-in-progress: true is active'
77
+ - 'Use a separate workflow_run: completed trigger for guaranteed post-run cleanup logic'
78
+ - 'Use if: always() instead of if: cancelled() for broader coverage'
79
+ - 'Keep cleanup steps inside the main job where possible — step-level if: always() is more reliable than job-level when concurrency cancels the run'
80
+ docs:
81
+ - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-concurrency'
82
+ label: 'GitHub Docs: Using concurrency'
83
+ - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_run'
84
+ label: 'GitHub Docs: workflow_run event'
85
+ - url: 'https://github.com/orgs/community/discussions/13655'
86
+ label: 'GitHub Community: cleanup jobs not running after concurrency cancellation'
@@ -0,0 +1,72 @@
1
+ id: 'concurrency-timing-024'
2
+ title: 'timeout-minutes applies to job execution only, not queue wait time — jobs can wait indefinitely'
3
+ category: concurrency-timing
4
+ severity: warning
5
+ tags:
6
+ - timeout
7
+ - queue
8
+ - self-hosted
9
+ - runner
10
+ - wait-time
11
+ patterns:
12
+ - regex: 'timeout-minutes:\s*\d+'
13
+ flags: 'i'
14
+ error_messages:
15
+ - 'The job running on runner has exceeded the maximum execution time of'
16
+ - 'The operation was canceled.'
17
+ root_cause: |
18
+ timeout-minutes only counts elapsed time from when a job actually begins executing on
19
+ a runner — not from when it enters the queue. A job waiting for an available runner
20
+ slot (including jobs waiting in a concurrency group queue) can sit pending for hours
21
+ or indefinitely without any timeout being applied.
22
+
23
+ This is particularly impactful with:
24
+ - Self-hosted runners under heavy load with limited runner capacity
25
+ - Concurrency groups with cancel-in-progress: false that accumulate queued jobs
26
+ - Repository-level runner quotas on GitHub-hosted runners during peak usage
27
+
28
+ Developers are often surprised that a job with timeout-minutes: 30 waited 4+ hours
29
+ before starting, then proceeded to run for its full 30-minute budget.
30
+ fix: |
31
+ There is no native queue-timeout setting in GitHub Actions. Recommended workarounds:
32
+
33
+ 1. Set cancel-in-progress: true in concurrency groups to drop stale queued jobs
34
+ when newer commits arrive, preventing queue accumulation.
35
+ 2. Monitor queue depth using the GitHub REST API /repos/{owner}/{repo}/actions/runs
36
+ and set up external alerting for runs stuck in 'queued' status too long.
37
+ 3. Ensure adequate self-hosted runner pool capacity relative to expected parallelism.
38
+ 4. Use github-hosted runners for time-sensitive jobs to avoid self-hosted queue depth issues.
39
+ fix_code:
40
+ - language: yaml
41
+ label: 'Prevent queue accumulation with cancel-in-progress'
42
+ code: |
43
+ jobs:
44
+ build:
45
+ runs-on: self-hosted
46
+ timeout-minutes: 30 # Only counts execution time, NOT queue wait time
47
+ concurrency:
48
+ group: ${{ github.workflow }}-${{ github.ref }}
49
+ cancel-in-progress: true # Drop stale queued jobs on new push
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+ - run: ./build.sh
53
+ - language: yaml
54
+ label: 'External queue monitoring via API'
55
+ code: |
56
+ # Monitor for stuck queued runs via GitHub API
57
+ # GET /repos/{owner}/{repo}/actions/runs?status=queued
58
+ # Alert if any run has been queued for more than N minutes
59
+ # (implement in a separate monitoring workflow or external system)
60
+ prevention:
61
+ - 'Do not assume timeout-minutes prevents jobs from waiting indefinitely in the runner queue'
62
+ - 'Use cancel-in-progress: true for CI workflows to prevent queue accumulation'
63
+ - 'Size self-hosted runner pools to handle expected peak concurrency'
64
+ - 'Monitor workflow run queue depth separately via the GitHub REST API'
65
+ - 'Document queue wait behavior in team CI runbooks so on-call engineers know what to expect'
66
+ docs:
67
+ - url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes'
68
+ label: 'GitHub Docs: timeout-minutes'
69
+ - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-concurrency'
70
+ label: 'GitHub Docs: Using concurrency'
71
+ - url: 'https://docs.github.com/en/rest/actions/workflow-runs'
72
+ label: 'GitHub REST API: Workflow runs'
@@ -0,0 +1,88 @@
1
+ id: 'triggers-026'
2
+ title: 'workflow_run does not fire when triggering workflow is skipped by paths or branches filter'
3
+ category: triggers
4
+ severity: silent-failure
5
+ tags:
6
+ - workflow-run
7
+ - skipped
8
+ - paths-filter
9
+ - branches-filter
10
+ - trigger-chain
11
+ patterns:
12
+ - regex: 'workflow_run'
13
+ flags: 'i'
14
+ - regex: 'types:\s*\[.*completed.*\]'
15
+ flags: 'i'
16
+ error_messages:
17
+ - 'This run was triggered by a workflow_run event but the parent workflow was not found'
18
+ root_cause: |
19
+ When a workflow is skipped because its on.push.paths or on.push.branches filter does not
20
+ match the pushed commit, GitHub does not create a workflow run record and therefore does
21
+ not emit a workflow_run completion event. A downstream workflow that listens for
22
+ on.workflow_run: [UpstreamWorkflow] with types: [completed] silently never fires.
23
+
24
+ This breaks fan-out CI/CD architectures where a primary workflow is gated by path/branch
25
+ filters, and secondary workflows (deploy, notify, publish) depend on its completion.
26
+ When the paths filter causes the primary workflow to be skipped entirely, the downstream
27
+ chain is dropped with no error message.
28
+
29
+ The issue affects both on.push.paths and on.push.branches filters. It does not affect
30
+ workflows that run but exit early via an if: condition on a job — only skipped runs
31
+ (which never appear in the GitHub Actions run list) cause the downstream gap.
32
+ fix: |
33
+ Replace trigger-level on.push.paths filtering with in-workflow job-level path detection
34
+ using an action like dorny/paths-filter. This ensures the upstream workflow always
35
+ creates a run (triggering the workflow_run event), while individual jobs are skipped
36
+ when paths do not match.
37
+ fix_code:
38
+ - language: yaml
39
+ label: 'Replace trigger-level paths filter with in-workflow detection'
40
+ code: |
41
+ # upstream.yml
42
+ # BAD: on.push.paths silently skips the run — workflow_run downstream never fires
43
+ # on:
44
+ # push:
45
+ # paths: ['src/**']
46
+
47
+ # GOOD: Always run, detect paths inside the workflow
48
+ on: [push]
49
+
50
+ jobs:
51
+ detect-changes:
52
+ runs-on: ubuntu-latest
53
+ outputs:
54
+ src-changed: ${{ steps.filter.outputs.src }}
55
+ steps:
56
+ - uses: actions/checkout@v4
57
+ - uses: dorny/paths-filter@v3
58
+ id: filter
59
+ with:
60
+ filters: |
61
+ src:
62
+ - 'src/**'
63
+
64
+ build:
65
+ needs: detect-changes
66
+ if: ${{ needs.detect-changes.outputs.src-changed == 'true' }}
67
+ runs-on: ubuntu-latest
68
+ steps:
69
+ - uses: actions/checkout@v4
70
+ - run: ./build.sh
71
+
72
+ # downstream.yml — now reliably fires on every push
73
+ # on:
74
+ # workflow_run:
75
+ # workflows: ['Upstream CI']
76
+ # types: [completed]
77
+ prevention:
78
+ - 'Do not combine on.push.paths/branches filters with workflow_run downstream dependencies'
79
+ - 'Use dorny/paths-filter or tj-actions/changed-files inside always-running workflows instead'
80
+ - 'Test the full trigger chain end-to-end by pushing commits that both match and do not match the filter'
81
+ - 'Document the skipped-runs gap in team CI docs for anyone building workflow_run chains'
82
+ docs:
83
+ - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_run'
84
+ label: 'GitHub Docs: workflow_run — triggering workflow must run on default branch'
85
+ - url: 'https://github.com/dorny/paths-filter'
86
+ label: 'dorny/paths-filter — job-level path filtering'
87
+ - url: 'https://github.com/orgs/community/discussions/23710'
88
+ label: 'GitHub Community: workflow_run not triggered when upstream workflow is skipped'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@htekdev/actions-debugger",
3
- "version": "1.0.28",
3
+ "version": "1.0.29",
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",