@htekdev/actions-debugger 1.0.86 → 1.0.88

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,94 @@
1
+ id: known-unsolved-051
2
+ title: 'No workflow-level `timeout-minutes` — only job-level and step-level timeouts; runaway workflows consume runner hours with no global cap'
3
+ category: known-unsolved
4
+ severity: limitation
5
+ tags:
6
+ - timeout
7
+ - workflow-level
8
+ - runner-hours
9
+ - billing
10
+ - limitation
11
+ - runaway-workflow
12
+ patterns:
13
+ - regex: 'timeout.minutes'
14
+ flags: 'i'
15
+ - regex: 'workflow.*timed? ?out|timed? ?out.*workflow'
16
+ flags: 'i'
17
+ error_messages:
18
+ - "No workflow-level timeout — only job-level timeout-minutes is supported"
19
+ - "Unexpected key 'timeout-minutes' at workflow level — property is only valid on jobs"
20
+ root_cause: |
21
+ GitHub Actions supports `timeout-minutes:` only at the JOB level and STEP level.
22
+ There is no `timeout-minutes:` field at the workflow level (the level containing
23
+ `on:`, `env:`, and `jobs:`). If placed at the workflow level, actionlint reports
24
+ "unexpected key 'timeout-minutes'" and the key is silently ignored at runtime
25
+ with no effect on execution.
26
+
27
+ The default job timeout is 360 minutes (6 hours). A workflow with multiple jobs,
28
+ each using the default timeout, can run for many times 360 minutes total. There is
29
+ no built-in way to enforce a global "if this entire workflow hasn't finished in X
30
+ hours, cancel everything."
31
+
32
+ Common scenarios where this causes unexpected runner hour consumption:
33
+ - A self-hosted runner goes offline mid-job; the job hangs until its per-job timeout
34
+ - A workflow waiting on a deployment environment review that is never approved
35
+ - A matrix job with a misconfigured container that hangs on startup
36
+ - A network call in a step that never times out (no step-level timeout set)
37
+
38
+ This is a long-requested GitHub Actions feature with hundreds of community upvotes.
39
+ fix: |
40
+ Workarounds (no official workflow-level timeout exists):
41
+
42
+ 1. SET CONSERVATIVE JOB TIMEOUTS: Add explicit `timeout-minutes:` to every job
43
+ with a value appropriate for that job's actual expected runtime plus a buffer.
44
+ Do not rely on the 360-minute default.
45
+
46
+ 2. SET STEP-LEVEL TIMEOUTS: Add `timeout-minutes:` to individual steps that perform
47
+ network calls, long-running builds, or any operation that can hang.
48
+
49
+ 3. WATCHDOG WORKFLOW: A separate workflow triggered by `workflow_run` can monitor
50
+ in-progress runs and cancel them via the API if they exceed a threshold duration.
51
+
52
+ 4. ENVIRONMENT REVIEW EXPIRY: For deployment workflows stuck on required reviewers,
53
+ configure an expiry window under Settings > Environments > [env] >
54
+ Deployment protection rules > Timeout.
55
+ fix_code:
56
+ - language: yaml
57
+ label: 'Set explicit per-job timeouts as a workaround'
58
+ code: |
59
+ jobs:
60
+ build:
61
+ runs-on: ubuntu-latest
62
+ timeout-minutes: 15 # Set per-job: build should not exceed 15 min
63
+ steps:
64
+ - uses: actions/checkout@v4
65
+ - run: npm ci && npm run build
66
+
67
+ test:
68
+ needs: build
69
+ runs-on: ubuntu-latest
70
+ timeout-minutes: 20 # Integration tests: 20 min max
71
+ steps:
72
+ - name: Run tests
73
+ timeout-minutes: 15 # Step-level: individual test run cap
74
+ run: npm run test:integration
75
+
76
+ deploy:
77
+ needs: test
78
+ runs-on: ubuntu-latest
79
+ timeout-minutes: 10 # Deployment itself should be fast
80
+ environment: production
81
+ steps:
82
+ - run: ./scripts/deploy.sh
83
+ prevention:
84
+ - 'Always set `timeout-minutes:` on every job — do not rely on the 360-minute default for normal CI workflows.'
85
+ - 'Add `timeout-minutes:` to individual steps that perform network I/O, external API calls, or any blocking operation.'
86
+ - 'Monitor unexpected Actions billing spikes: they often indicate a stuck workflow running against the 6-hour default job timeout.'
87
+ - 'Set deployment environment review expiry windows to prevent workflows from hanging indefinitely awaiting approval.'
88
+ docs:
89
+ - url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes'
90
+ label: 'Workflow syntax: jobs.<job_id>.timeout-minutes'
91
+ - url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepstimeout-minutes'
92
+ label: 'Workflow syntax: jobs.<job_id>.steps[*].timeout-minutes'
93
+ - url: 'https://github.com/orgs/community/discussions/16058'
94
+ label: 'GitHub Community: Workflow-level timeout-minutes (feature request)'
@@ -0,0 +1,97 @@
1
+ id: runner-environment-154
2
+ title: "macOS ARM64 runner: Rosetta 2 not pre-installed — x86_64 binaries fail"
3
+ category: runner-environment
4
+ severity: error
5
+ tags:
6
+ - macos
7
+ - arm64
8
+ - rosetta
9
+ - apple-silicon
10
+ - binary-compatibility
11
+ patterns:
12
+ - regex: 'Bad CPU type in executable'
13
+ flags: i
14
+ - regex: 'Exec format error'
15
+ flags: i
16
+ - regex: 'is not supported on this machine \(CPU type'
17
+ flags: i
18
+ - regex: 'rosetta.*not.*install|softwareupdate.*rosetta'
19
+ flags: i
20
+ error_messages:
21
+ - "zsh: bad CPU type in executable: /usr/local/bin/mytool"
22
+ - "Exec format error"
23
+ - "/usr/local/bin/node: Bad CPU type in executable"
24
+ - "The file '/path/to/binary' is not supported on this machine (CPU type 16777223)"
25
+ root_cause: |
26
+ GitHub-hosted macOS runners on Apple Silicon hardware (macos-14, macos-15, and the `macos-latest`
27
+ label when pointing to ARM64 images) do NOT have Rosetta 2 pre-installed. Rosetta 2 is Apple's
28
+ x86_64-to-ARM64 translation layer that allows Intel-compiled binaries to run on Apple Silicon.
29
+
30
+ Without Rosetta 2, any x86_64 binary executed on the ARM64 runner fails immediately with
31
+ "Bad CPU type in executable" or "Exec format error". Common sources of x86_64 binaries:
32
+ - Pre-compiled tool downloads targeting darwin-x64
33
+ - npm packages with native addons built for x86_64
34
+ - Homebrew formulae that only have x86_64 bottles
35
+ - Custom build scripts that hardcode `arch -x86_64`
36
+ - Docker images using `linux/amd64` platform
37
+
38
+ This is distinct from Linux ARM64 runners (ubuntu-24.04-arm) where the error manifests
39
+ differently and no Rosetta equivalent exists. On macOS, Rosetta 2 can be installed as a
40
+ one-time setup step.
41
+ fix: |
42
+ Option 1 — Install Rosetta 2 at job start (quick workaround):
43
+ Add `softwareupdate --install-rosetta --agree-to-license` as the first step. This allows
44
+ x86_64 binaries to run via translation.
45
+
46
+ Option 2 — Use ARM64-native binaries (preferred):
47
+ Update download scripts to detect `arch` and fetch the correct binary:
48
+ - Use `$(uname -m)` to branch between arm64 and x86_64 download URLs
49
+ - Use `arch -arm64 brew install` for Homebrew packages
50
+ - For npm: ensure native addons are rebuilt for the current architecture
51
+
52
+ Option 3 — Pin to Intel macOS runner:
53
+ Use `runs-on: macos-13` which is the last Intel-based macOS runner image. Note that
54
+ macos-13 is scheduled for deprecation; this is a temporary workaround only.
55
+ fix_code:
56
+ - language: yaml
57
+ label: "Install Rosetta 2 as first step (workaround)"
58
+ code: |
59
+ jobs:
60
+ build:
61
+ runs-on: macos-latest # ARM64 Apple Silicon
62
+ steps:
63
+ - name: Install Rosetta 2
64
+ run: softwareupdate --install-rosetta --agree-to-license
65
+ - uses: actions/checkout@v4
66
+ - name: Download tool
67
+ run: |
68
+ # x86_64 binary will now work via Rosetta translation
69
+ curl -L https://example.com/tool-darwin-x64 -o tool
70
+ chmod +x tool
71
+ ./tool
72
+ - language: yaml
73
+ label: "Detect architecture and fetch the correct binary (preferred)"
74
+ code: |
75
+ steps:
76
+ - name: Download architecture-appropriate binary
77
+ run: |
78
+ ARCH=$(uname -m)
79
+ if [ "$ARCH" = "arm64" ]; then
80
+ BINARY_URL="https://example.com/tool-darwin-arm64"
81
+ else
82
+ BINARY_URL="https://example.com/tool-darwin-x64"
83
+ fi
84
+ curl -L "$BINARY_URL" -o tool
85
+ chmod +x tool
86
+ ./tool
87
+ prevention:
88
+ - "Always test workflows on both `macos-13` (Intel) and `macos-14`+ (ARM64) runners"
89
+ - "Check download scripts for hardcoded `darwin-x64` or `darwin-amd64` architecture strings"
90
+ - "Prefer tools distributed via Homebrew — many formulae now have native ARM64 bottles"
91
+ - "Use `${{ runner.arch }}` in workflow expressions to branch on architecture (`X64` vs `ARM64`)"
92
+ - "Audit npm packages with native addons — they must support `darwin-arm64` or provide a WASM fallback"
93
+ docs:
94
+ - url: "https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources"
95
+ label: "GitHub Docs: Supported runners and hardware resources"
96
+ - url: "https://support.apple.com/en-us/102527"
97
+ label: "Apple: If you need to install Rosetta on your Mac"
@@ -0,0 +1,106 @@
1
+ id: runner-environment-155
2
+ title: "GitHub-hosted runner disk full — 'No space left on device' during large builds"
3
+ category: runner-environment
4
+ severity: error
5
+ tags:
6
+ - disk-space
7
+ - ubuntu
8
+ - docker
9
+ - large-builds
10
+ - runner-storage
11
+ patterns:
12
+ - regex: 'No space left on device'
13
+ flags: i
14
+ - regex: 'write.*error.*28|errno 28'
15
+ flags: i
16
+ - regex: 'disk.*full|filesystem.*full|out of disk'
17
+ flags: i
18
+ - regex: 'no space left'
19
+ flags: i
20
+ error_messages:
21
+ - "No space left on device"
22
+ - "Error: write /var/lib/docker/tmp/...: no space left on device"
23
+ - "tar: ./app: Cannot write: No space left on device"
24
+ - "error: failed to push some refs: No space left on device"
25
+ - "OSError: [Errno 28] No space left on device"
26
+ root_cause: |
27
+ GitHub-hosted Ubuntu runners have approximately 14 GB of usable disk space after the
28
+ pre-installed tool suite occupies the rest. The runner image includes a large set of
29
+ pre-installed software that consumes the majority of available disk:
30
+ - Android SDK: ~8.7 GB
31
+ - .NET SDKs: ~1.5 GB
32
+ - Haskell/GHC: ~5.4 GB
33
+ - CodeQL: ~5.4 GB
34
+ - Docker images cached on the runner: varies
35
+
36
+ Workflows that perform large Docker builds, pull multiple Docker images, build large
37
+ native packages, or accumulate many npm/cargo/maven dependencies can exhaust the
38
+ ~14 GB limit. Common triggers:
39
+ - `docker build` for large multi-stage images
40
+ - `docker pull` of several large base images
41
+ - Building and caching large Rust/C++ projects
42
+ - Downloading large ML models or datasets
43
+ - Generating large test fixtures or build artifacts
44
+
45
+ The error manifests in diverse ways: Docker build errors (errno 28), tar extraction
46
+ failures, git push failures writing pack files, or Python/Node crashes writing temp files.
47
+ fix: |
48
+ Free disk space before your build by removing pre-installed tools you don't need.
49
+ The community-maintained `jlumbroso/free-disk-space` action is the standard solution.
50
+
51
+ Manual removal commands for the largest consumers:
52
+ - Android SDK: `sudo rm -rf /usr/local/lib/android`
53
+ - .NET: `sudo rm -rf /usr/share/dotnet`
54
+ - Haskell/GHC: `sudo rm -rf /usr/local/.ghcup`
55
+ - Large packages: `sudo apt-get clean && sudo apt-get autoremove -y`
56
+ - Docker build cache: `docker builder prune -af`
57
+
58
+ Alternatively, upgrade to GitHub-hosted larger runners (4+ cores) which provide
59
+ more disk space, or use self-hosted runners.
60
+ fix_code:
61
+ - language: yaml
62
+ label: "Free disk space before large Docker build"
63
+ code: |
64
+ jobs:
65
+ build:
66
+ runs-on: ubuntu-latest
67
+ steps:
68
+ - name: Free disk space
69
+ uses: jlumbroso/free-disk-space@main
70
+ with:
71
+ tool-cache: false
72
+ android: true
73
+ dotnet: true
74
+ haskell: true
75
+ large-packages: true
76
+ docker-images: true
77
+ swap-storage: true
78
+ - uses: actions/checkout@v4
79
+ - name: Build Docker image
80
+ run: docker build -t myapp:latest .
81
+ - language: yaml
82
+ label: "Manual removal of largest pre-installed tools"
83
+ code: |
84
+ steps:
85
+ - name: Free disk space (manual)
86
+ run: |
87
+ df -h /
88
+ sudo rm -rf /usr/local/lib/android
89
+ sudo rm -rf /usr/share/dotnet
90
+ sudo rm -rf /usr/local/.ghcup
91
+ sudo apt-get clean
92
+ docker builder prune -af || true
93
+ df -h /
94
+ prevention:
95
+ - "Add a disk-space check step (`df -h /`) early in the workflow to detect capacity issues before they cause cryptic failures"
96
+ - "Run `jlumbroso/free-disk-space` before any Docker build or large compilation job"
97
+ - "Remove only tools you know you don't need — removing the tool-cache may break `actions/setup-*` steps"
98
+ - "Consider using `runs-on: ubuntu-latest-4-cores` (GitHub Teams) which provides more disk space"
99
+ - "Use Docker `--no-cache` flags cautiously — reusing layer cache can actually save disk vs rebuilding every layer"
100
+ docs:
101
+ - url: "https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources"
102
+ label: "GitHub Docs: Runner hardware resources"
103
+ - url: "https://github.com/jlumbroso/free-disk-space"
104
+ label: "jlumbroso/free-disk-space action"
105
+ - url: "https://github.com/actions/runner-images/issues/2840"
106
+ label: "runner-images#2840: Tracking disk space usage"
@@ -0,0 +1,93 @@
1
+ id: runner-environment-151
2
+ title: "Docker container action creates root-owned files causing Permission denied in following steps"
3
+ category: runner-environment
4
+ severity: error
5
+ tags:
6
+ - docker
7
+ - container-action
8
+ - file-permissions
9
+ - root-user
10
+ - workspace
11
+ patterns:
12
+ - regex: 'Permission denied'
13
+ flags: i
14
+ - regex: 'EACCES: permission denied'
15
+ flags: i
16
+ - regex: 'cannot open .+ Permission denied'
17
+ flags: i
18
+ - regex: 'error: open .+ permission denied'
19
+ flags: i
20
+ error_messages:
21
+ - "Permission denied"
22
+ - "cannot create directory '...': Permission denied"
23
+ - "EACCES: permission denied, open '/github/workspace/...'"
24
+ - "error: cannot open '.git/COMMIT_EDITMSG': Permission denied"
25
+ - "chown: changing ownership of '...': Operation not permitted"
26
+ root_cause: |
27
+ Docker-based GitHub Actions — both `uses: docker://image:tag` inline steps and
28
+ actions with `runs.using: docker` in their action.yml — run their container process
29
+ as root (UID 0) by default unless the Dockerfile explicitly sets a USER directive.
30
+
31
+ Any files written to `GITHUB_WORKSPACE` (mounted at `/github/workspace` inside the
32
+ container) are created with root ownership (uid=0, gid=0). Subsequent workflow steps
33
+ run as the `runner` user (UID 1001) and cannot read, modify, or delete root-owned
34
+ files, causing "Permission denied" errors.
35
+
36
+ The failure is especially common when:
37
+ - A Docker action writes build artifacts, generated code, or lock files.
38
+ - A subsequent step tries to commit changes or run tools on the generated output.
39
+ - The workflow uses `actions/checkout` after a Docker action and git operations fail.
40
+ fix: |
41
+ Option 1 — Chown the workspace after the Docker action (easiest for third-party actions):
42
+ Add a step after the Docker action: `sudo chown -R "$USER:$(id -gn)" "$GITHUB_WORKSPACE"`
43
+
44
+ Option 2 — Set USER in the Dockerfile (best for actions you control):
45
+ Add `USER 1001` (runner UID) to the action's Dockerfile so all written files
46
+ are owned by the runner user.
47
+
48
+ Option 3 — Use --user flag in the Docker run args:
49
+ Set `args` in the action's action.yml to include `--user=1001:127`.
50
+ fix_code:
51
+ - language: yaml
52
+ label: "Fix: chown workspace after Docker action (works for any Docker-based action)"
53
+ code: |
54
+ steps:
55
+ - uses: actions/checkout@v4
56
+
57
+ - name: Run Docker-based action
58
+ uses: docker://my-build-image:latest
59
+ with:
60
+ args: '--output /github/workspace/dist'
61
+
62
+ - name: Fix file ownership after Docker action
63
+ run: sudo chown -R "$USER:$(id -gn)" "$GITHUB_WORKSPACE"
64
+
65
+ - name: Use build output (now accessible as runner user)
66
+ run: ls -la dist/ && cat dist/output.txt
67
+ - language: yaml
68
+ label: "Fix: set non-root USER in Dockerfile (preferred when controlling the action)"
69
+ code: |
70
+ # In your Docker action's Dockerfile — run as UID 1001 to match GitHub runner
71
+ # FROM ubuntu:22.04
72
+ # RUN useradd -u 1001 -g 127 runner
73
+ # WORKDIR /github/workspace
74
+ # USER 1001
75
+ # ENTRYPOINT ["/entrypoint.sh"]
76
+
77
+ # With this Dockerfile, no chown step is needed — files are owned by runner user
78
+ steps:
79
+ - uses: actions/checkout@v4
80
+ - uses: ./ # local Docker action with non-root USER
81
+ - run: cat generated-output.txt # accessible without permission errors
82
+ prevention:
83
+ - "Always set a non-root USER directive in Dockerfiles for actions that write to the workspace."
84
+ - "After any third-party Docker action, add a chown step before accessing created files."
85
+ - "Prefer JavaScript/TypeScript or composite actions over Docker actions when workspace I/O is needed — they run as the runner user by default."
86
+ - "Test Docker actions locally with UID 1001 to catch permission issues before CI."
87
+ docs:
88
+ - url: "https://docs.github.com/en/actions/sharing-automations/creating-actions/dockerfile-support-for-github-actions"
89
+ label: "Dockerfile support for GitHub Actions"
90
+ - url: "https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-docker-container-action"
91
+ label: "Creating a Docker container action"
92
+ - url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables"
93
+ label: "GITHUB_WORKSPACE default environment variable"
@@ -0,0 +1,90 @@
1
+ id: runner-environment-152
2
+ title: 'Organization "Require immutable actions" policy blocks tag-pinned action refs — must use full commit SHA'
3
+ category: runner-environment
4
+ severity: error
5
+ tags:
6
+ - immutable-actions
7
+ - sha-pinning
8
+ - security-policy
9
+ - organization-policy
10
+ - enterprise
11
+ - supply-chain
12
+ patterns:
13
+ - regex: 'not pinned to an immutable SHA'
14
+ flags: 'i'
15
+ - regex: 'Action.*is not pinned.*immutable'
16
+ flags: 'i'
17
+ - regex: 'immutable actions.*required'
18
+ flags: 'i'
19
+ - regex: 'Update the workflow to use the immutable reference'
20
+ flags: 'i'
21
+ error_messages:
22
+ - "Error: Action 'actions/checkout@v4' is not pinned to an immutable SHA. Update the workflow to use the immutable reference."
23
+ - "Actions must be pinned to an immutable commit SHA. Organization policy requires immutable actions."
24
+ - "Error: This action reference is not immutable. The organization requires all actions to be pinned by SHA."
25
+ root_cause: |
26
+ GitHub introduced an organization-level security policy called "Require immutable
27
+ actions" (available in GitHub Enterprise and GitHub Teams since late 2025). When
28
+ enabled by an organization admin, every `uses:` reference in every workflow within
29
+ that organization must point to a full commit SHA rather than a mutable tag (such
30
+ as `@v4`) or branch name (such as `@main`).
31
+
32
+ Tags and branches are mutable — they can be updated to point to a different commit
33
+ at any time. An attacker who compromises an upstream action repository can move a
34
+ widely used tag to point to malicious code. The SHA immutability policy protects
35
+ against this supply-chain attack vector by ensuring the exact code being run is
36
+ pinned and verifiable.
37
+
38
+ When the policy is active, the Actions runner validates every `uses:` reference
39
+ before execution and refuses to run any workflow containing mutable refs, even if
40
+ the referenced tag currently points to a trusted commit. This commonly surfaces
41
+ after an organization adopts the policy post-hoc, breaking all existing workflows.
42
+ fix: |
43
+ Replace every `uses:` tag or branch reference with the full 40-character commit SHA.
44
+ Add the human-readable tag as an inline comment for maintainability.
45
+
46
+ To find the SHA for a specific action version:
47
+ - Visit the action's GitHub releases page (e.g., github.com/actions/checkout/releases/tag/v4)
48
+ - Click the commit SHA link next to the tag
49
+
50
+ Tools that automate SHA pinning across all workflow files:
51
+ - `ratchet` (github.com/thepwagner/ratchet): `ratchet pin .github/workflows/`
52
+ - `pin-github-action` (Mend): available as a GitHub Action or CLI
53
+ - Dependabot with `package-ecosystem: github-actions` maintains pinned SHAs automatically
54
+ fix_code:
55
+ - language: yaml
56
+ label: 'Pin action refs to full commit SHA (with readable comment)'
57
+ code: |
58
+ # BEFORE (fails with immutable actions org policy)
59
+ steps:
60
+ - uses: actions/checkout@v4
61
+ - uses: actions/setup-node@v4
62
+ - uses: actions/upload-artifact@v4
63
+
64
+ # AFTER (SHA-pinned with human-readable tag comment)
65
+ steps:
66
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
67
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
68
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
69
+ - language: yaml
70
+ label: 'Dependabot config to keep SHA pins up to date automatically'
71
+ code: |
72
+ # .github/dependabot.yml
73
+ version: 2
74
+ updates:
75
+ - package-ecosystem: github-actions
76
+ directory: /
77
+ schedule:
78
+ interval: weekly
79
+ prevention:
80
+ - 'Audit all workflow files before enabling the org policy — run ratchet or pin-github-action across the entire .github/workflows/ directory first.'
81
+ - 'Enable the policy in a single pilot repository before rolling it out organization-wide to identify and fix all mutable refs.'
82
+ - 'Add actionlint with SHA-pinning rules to CI so new mutable refs are caught in PRs before reaching main.'
83
+ - 'Configure Dependabot for github-actions to automatically open PRs whenever a pinned SHA has a new upstream release.'
84
+ docs:
85
+ - url: 'https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-third-party-actions'
86
+ label: 'Security hardening for GitHub Actions: using third-party actions'
87
+ - url: 'https://docs.github.com/en/organizations/managing-organization-settings/disabling-or-limiting-github-actions-for-your-organization'
88
+ label: 'Requiring immutable action components (org policy settings)'
89
+ - url: 'https://github.com/thepwagner/ratchet'
90
+ label: 'ratchet — CLI tool to pin GitHub Actions refs to SHA'
@@ -0,0 +1,90 @@
1
+ id: runner-environment-156
2
+ title: "ubuntu runner: snap packages unavailable — snapd not running"
3
+ category: runner-environment
4
+ severity: error
5
+ tags:
6
+ - ubuntu
7
+ - snap
8
+ - snapd
9
+ - packages
10
+ - ubuntu-24
11
+ patterns:
12
+ - regex: 'cannot communicate with server.*snapd\.socket'
13
+ flags: i
14
+ - regex: 'snapd is not running'
15
+ flags: i
16
+ - regex: 'snap.*connect.*refused|dial unix.*snapd'
17
+ flags: i
18
+ - regex: 'snap: command not found'
19
+ flags: i
20
+ error_messages:
21
+ - "error: cannot communicate with server: Post \"http://localhost/v2/snaps/kubectl\": dial unix /run/snapd.socket: connect: no such file or directory"
22
+ - "error: cannot communicate with server: Post http://localhost/v2/snaps: dial unix /run/snapd.socket: connect: no such file or directory"
23
+ - "snapd is not running"
24
+ - "snap: command not found"
25
+ root_cause: |
26
+ GitHub-hosted Ubuntu runners do not run the snapd daemon. The snap socket
27
+ `/run/snapd.socket` is never created, so any `snap install` command immediately fails
28
+ when attempting to communicate with the snap store daemon.
29
+
30
+ While the `snap` binary may be present on the runner image, the background snapd service
31
+ is not started because GitHub-hosted runners use a minimal systemd-free environment.
32
+ Starting snapd manually is not practical — it requires systemd or a complex manual
33
+ daemon startup sequence, and snap packages are confined using AppArmor profiles that
34
+ may also be unavailable in the runner environment.
35
+
36
+ This is frequently encountered when workflows try to install tools distributed as snaps:
37
+ kubectl, microk8s, aws-cli, spotify, and many developer utilities.
38
+ fix: |
39
+ Replace `snap install` with an alternative package source for the tool you need:
40
+
41
+ - **kubectl**: Use `curl -LO https://dl.k8s.io/release/...` or the `azure/setup-kubectl` action
42
+ - **aws-cli v2**: Use `pip install awscli` or the official AWS install script via apt
43
+ - **Helm**: Use the `azure/setup-helm` action or the official install script
44
+ - **General tools**: Check for apt, pip, npm, or GitHub releases as alternatives
45
+
46
+ If you must install a snap, consider using a self-hosted runner with snapd enabled,
47
+ or a Docker container runner with snapd configured.
48
+ fix_code:
49
+ - language: yaml
50
+ label: "Install kubectl via apt instead of snap"
51
+ code: |
52
+ steps:
53
+ - name: Install kubectl
54
+ run: |
55
+ curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key \
56
+ | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
57
+ echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' \
58
+ | sudo tee /etc/apt/sources.list.d/kubernetes.list
59
+ sudo apt-get update
60
+ sudo apt-get install -y kubectl
61
+ - language: yaml
62
+ label: "Use dedicated setup actions instead of snap"
63
+ code: |
64
+ steps:
65
+ # Instead of: snap install kubectl --classic
66
+ - uses: azure/setup-kubectl@v4
67
+ with:
68
+ version: 'v1.30.0'
69
+
70
+ # Instead of: snap install helm --classic
71
+ - uses: azure/setup-helm@v4
72
+ with:
73
+ version: '3.14.0'
74
+
75
+ # Instead of: snap install aws-cli --classic
76
+ - name: Install AWS CLI v2
77
+ run: |
78
+ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
79
+ unzip awscliv2.zip
80
+ sudo ./aws/install
81
+ prevention:
82
+ - "Audit your workflow for any `snap install` commands — GitHub-hosted runners cannot run snap packages"
83
+ - "Check if the tool has an official GitHub Action (setup-kubectl, setup-helm, etc.) before writing a manual install step"
84
+ - "For tools not on apt, prefer GitHub Releases binary downloads with `curl` and `chmod +x` over snap"
85
+ - "Self-hosted runners with a full Ubuntu desktop/server installation do support snap if needed"
86
+ docs:
87
+ - url: "https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#software-installed-on-github-hosted-runners"
88
+ label: "GitHub Docs: Software installed on GitHub-hosted runners"
89
+ - url: "https://github.com/actions/runner-images/issues/1769"
90
+ label: "runner-images#1769: snap is not supported"
@@ -0,0 +1,95 @@
1
+ id: runner-environment-153
2
+ title: "ubuntu runner systemctl fails — systemd not running as PID 1"
3
+ category: runner-environment
4
+ severity: error
5
+ tags:
6
+ - ubuntu
7
+ - systemd
8
+ - systemctl
9
+ - services
10
+ - daemon
11
+ patterns:
12
+ - regex: 'System has not been booted with systemd as init system \(PID 1\)\. Can''t operate\.'
13
+ flags: i
14
+ - regex: 'Failed to connect to bus: No such file or directory'
15
+ flags: i
16
+ - regex: 'systemctl: command not found'
17
+ flags: i
18
+ error_messages:
19
+ - "System has not been booted with systemd as init system (PID 1). Can't operate."
20
+ - "Failed to connect to bus: No such file or directory"
21
+ - "Failed to start postgresql.service: Unit not found."
22
+ - "System has not been booted with systemd"
23
+ root_cause: |
24
+ GitHub-hosted Ubuntu runners do not run systemd as the init system (PID 1). The runner
25
+ process starts directly inside a container-like environment where systemd is never initialized.
26
+ Any workflow step that calls `sudo systemctl start <service>`, `systemctl enable <service>`,
27
+ or `systemctl status <service>` will immediately fail because the D-Bus socket and systemd
28
+ unit system are not present.
29
+
30
+ This is a fundamental architectural constraint of GitHub-hosted runners — they are ephemeral
31
+ VMs (not containers) but systemd is disabled to reduce boot time and resource overhead. The
32
+ pattern is extremely common when devs copy-paste service startup commands from their local
33
+ Linux environment or documentation into GitHub Actions run: steps.
34
+
35
+ Common affected services: PostgreSQL, MySQL, Redis, MongoDB, Nginx, Apache, RabbitMQ.
36
+ fix: |
37
+ Replace `systemctl` calls with one of these approaches:
38
+
39
+ 1. **Use GitHub Actions `services:` containers** — the recommended approach for databases and
40
+ message queues. GitHub spins up Docker containers alongside your job.
41
+
42
+ 2. **Use direct daemon start commands** — start the service binary directly:
43
+ - PostgreSQL: `sudo -u postgres pg_ctl -D /etc/postgresql/*/main start`
44
+ - MySQL: `sudo mysqld_safe &`
45
+ - Redis: `redis-server --daemonize yes`
46
+
47
+ 3. **Use `service` SysV command** — on Ubuntu runners, SysV init `service` works even without
48
+ systemd: `sudo service postgresql start`
49
+
50
+ fix_code:
51
+ - language: yaml
52
+ label: "Use services: containers block (recommended for databases)"
53
+ code: |
54
+ jobs:
55
+ test:
56
+ runs-on: ubuntu-latest
57
+ services:
58
+ postgres:
59
+ image: postgres:16
60
+ env:
61
+ POSTGRES_PASSWORD: postgres
62
+ ports:
63
+ - 5432:5432
64
+ options: >-
65
+ --health-cmd pg_isready
66
+ --health-interval 10s
67
+ --health-timeout 5s
68
+ --health-retries 5
69
+ steps:
70
+ - uses: actions/checkout@v4
71
+ - name: Run tests
72
+ run: npm test
73
+ env:
74
+ DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
75
+ - language: yaml
76
+ label: "Use SysV 'service' command as systemctl fallback"
77
+ code: |
78
+ steps:
79
+ - name: Start PostgreSQL
80
+ run: |
81
+ sudo service postgresql start
82
+ sudo service postgresql status
83
+ - name: Create test database
84
+ run: |
85
+ sudo -u postgres psql -c "CREATE DATABASE testdb;"
86
+ prevention:
87
+ - "Use the `services:` block in your workflow for databases and message queues — it's the GitHub Actions-native solution"
88
+ - "Replace `systemctl start X` with `sudo service X start` for SysV-compatible services"
89
+ - "For custom daemons, start them in the background with `&` and poll for readiness"
90
+ - "Never copy-paste `systemctl` commands from local Linux setup scripts into GitHub Actions without adaptation"
91
+ docs:
92
+ - url: "https://docs.github.com/en/actions/use-cases-and-examples/using-containerized-services/about-service-containers"
93
+ label: "GitHub Docs: About service containers"
94
+ - url: "https://docs.github.com/en/actions/use-cases-and-examples/using-containerized-services/creating-postgresql-service-containers"
95
+ label: "GitHub Docs: Creating PostgreSQL service containers"
@@ -0,0 +1,97 @@
1
+ id: silent-failures-080
2
+ title: "github.event.commits always empty array on pull_request events"
3
+ category: silent-failures
4
+ severity: silent-failure
5
+ tags:
6
+ - pull-request
7
+ - commits
8
+ - event-payload
9
+ - skip-ci
10
+ - multi-trigger
11
+ patterns:
12
+ - regex: 'github\.event\.commits\[0\]'
13
+ flags: i
14
+ - regex: 'Cannot read propert\w+ of undefined.*message'
15
+ flags: i
16
+ error_messages:
17
+ - "Error: Cannot read properties of undefined (reading 'message')"
18
+ - "github.event.commits[0] evaluates to null on pull_request events"
19
+ - "contains(github.event.commits[0].message, '[skip ci]') always false on PRs"
20
+ root_cause: |
21
+ The `github.event.commits` array is only populated in `push` event payloads.
22
+ On `pull_request` events, `github.event.commits` is always an empty array `[]`.
23
+ Accessing `github.event.commits[0]` returns `null`, and
24
+ `github.event.commits[0].message` returns an empty string without throwing an
25
+ expression error.
26
+
27
+ This is the most common footgun when implementing "[skip ci]" commit message
28
+ detection: the condition works correctly for `push` events but silently never
29
+ triggers (never skips) on `pull_request` events. The workflow runs for every PR
30
+ push regardless of the commit message.
31
+
32
+ The issue affects any multi-trigger workflow with both `push:` and `pull_request:`
33
+ that reads commit data from the event payload.
34
+ fix: |
35
+ Guard `github.event.commits` access by event type, or use PR-specific context:
36
+ 1. Use `if: github.event_name == 'push'` before accessing commits.
37
+ 2. For PR events, check `github.event.pull_request.title` or `github.event.pull_request.body`
38
+ for skip conditions.
39
+ 3. Use the `actions/github-script` action to query commits via the REST API on PR events.
40
+ 4. Consider separate workflows for push and pull_request to avoid cross-event payload confusion.
41
+ fix_code:
42
+ - language: yaml
43
+ label: "Wrong: commit message check silently fails on pull_request events"
44
+ code: |
45
+ on: [push, pull_request]
46
+
47
+ jobs:
48
+ build:
49
+ runs-on: ubuntu-latest
50
+ # github.event.commits[0].message is null on pull_request — condition always false
51
+ if: "!contains(github.event.commits[0].message, '[skip ci]')"
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+ - run: npm test
55
+ - language: yaml
56
+ label: "Fix: guard by event type and use PR-appropriate skip signal"
57
+ code: |
58
+ on: [push, pull_request]
59
+
60
+ jobs:
61
+ build:
62
+ runs-on: ubuntu-latest
63
+ steps:
64
+ - uses: actions/checkout@v4
65
+
66
+ - name: Check skip condition
67
+ id: skip
68
+ run: |
69
+ SKIP=false
70
+ if [[ "${{ github.event_name }}" == "push" ]]; then
71
+ # Commits array is available on push events
72
+ if echo "${{ github.event.commits[0].message }}" | grep -q '\[skip ci\]'; then
73
+ SKIP=true
74
+ fi
75
+ elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
76
+ # Check PR title or body for skip signal on PR events
77
+ if echo "${{ github.event.pull_request.title }}" | grep -q '\[skip ci\]'; then
78
+ SKIP=true
79
+ fi
80
+ fi
81
+ echo "skip=$SKIP" >> "$GITHUB_OUTPUT"
82
+
83
+ - name: Run tests
84
+ if: steps.skip.outputs.skip != 'true'
85
+ run: npm test
86
+ prevention:
87
+ - "Never access github.event.commits on workflows with pull_request triggers — it will always be empty."
88
+ - "Use separate workflows for push and pull_request if commit-message-based conditionals are required."
89
+ - "For PR skip conditions, use the PR title or body rather than commit messages."
90
+ - "Check which fields are populated for each event type in the GitHub webhook event payload documentation."
91
+ docs:
92
+ - url: "https://docs.github.com/en/webhooks/webhook-events-and-payloads#push"
93
+ label: "Push webhook event payload (commits array)"
94
+ - url: "https://docs.github.com/en/webhooks/webhook-events-and-payloads#pull_request"
95
+ label: "Pull request webhook event payload"
96
+ - url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-conditions-to-control-job-execution"
97
+ label: "Using conditions to control job execution"
@@ -0,0 +1,86 @@
1
+ id: silent-failures-081
2
+ title: '`needs.<job_id>.conclusion` does not exist — correct property is `result`; evaluates to empty string silently'
3
+ category: silent-failures
4
+ severity: silent-failure
5
+ tags:
6
+ - needs-context
7
+ - job-result
8
+ - downstream-jobs
9
+ - if-condition
10
+ - empty-string
11
+ - notification-job
12
+ patterns:
13
+ - regex: 'needs\.\w+\.conclusion'
14
+ flags: 'i'
15
+ error_messages:
16
+ - "needs.<job_id>.conclusion evaluates to empty string — condition using .conclusion never fires"
17
+ root_cause: |
18
+ The `needs` context exposes `.result` for each upstream job — NOT `.conclusion`.
19
+ Valid values for `needs.<job_id>.result` are: `success`, `failure`, `cancelled`,
20
+ and `skipped`.
21
+
22
+ The property `.conclusion` does not exist on the needs context object. Accessing
23
+ `needs.<job_id>.conclusion` silently evaluates to an empty string `""` with no
24
+ warning, error, or annotation in the workflow logs. The downstream job runs but the
25
+ `if:` condition never matches because `"" == 'failure'` is always false.
26
+
27
+ This confusion arises because the `steps` context exposes BOTH `.conclusion` and
28
+ `.outcome` for step results, leading developers to assume the needs context follows
29
+ the same property naming. The mismatch causes notification jobs, cleanup steps, and
30
+ failure-handler jobs to silently skip execution with no observable error.
31
+ fix: |
32
+ Replace `.conclusion` with `.result` in all `needs` context expressions.
33
+
34
+ WRONG: if: needs.build.conclusion == 'failure' # always empty string, never matches
35
+ RIGHT: if: needs.build.result == 'failure' # correct
36
+
37
+ For aggregate checks, prefer the built-in status functions over manual needs.result
38
+ comparisons — `failure()`, `success()`, `cancelled()`, `always()` — which evaluate
39
+ the combined result of all upstream jobs automatically.
40
+ fix_code:
41
+ - language: yaml
42
+ label: 'Correct: use needs.<job>.result not needs.<job>.conclusion'
43
+ code: |
44
+ jobs:
45
+ build:
46
+ runs-on: ubuntu-latest
47
+ steps:
48
+ - uses: actions/checkout@v4
49
+ - run: npm ci && npm test
50
+
51
+ notify-on-failure:
52
+ needs: [build]
53
+ # Use built-in failure() or explicit needs.<job>.result
54
+ if: failure()
55
+ runs-on: ubuntu-latest
56
+ steps:
57
+ - name: Send failure notification
58
+ run: |
59
+ echo "Build result: ${{ needs.build.result }}"
60
+ # .result values: success | failure | cancelled | skipped
61
+ - language: yaml
62
+ label: 'Multi-job downstream check using needs.result'
63
+ code: |
64
+ jobs:
65
+ deploy:
66
+ needs: [lint, test, build]
67
+ # Explicit per-job result check — use .result not .conclusion
68
+ if: >-
69
+ needs.lint.result == 'success' &&
70
+ needs.test.result == 'success' &&
71
+ needs.build.result == 'success'
72
+ runs-on: ubuntu-latest
73
+ steps:
74
+ - run: echo "All upstream jobs succeeded"
75
+ prevention:
76
+ - 'The needs context exposes `.result` only — `.conclusion` does not exist. Use `needs.<job>.result` for all downstream conditional checks.'
77
+ - 'Prefer the status check functions `failure()`, `success()`, `cancelled()`, `always()` in downstream job `if:` conditions — they aggregate all upstream job results automatically.'
78
+ - 'Add actionlint to CI — it reports references to undefined context properties including `needs.<job>.conclusion`, catching this mistake at review time.'
79
+ - 'When a notification or cleanup job silently skips, verify the `if:` condition uses `.result` and check the actual result value in the Actions run summary.'
80
+ docs:
81
+ - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#needs-context'
82
+ label: 'GitHub Actions: needs context — result property'
83
+ - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-conditions-to-control-job-execution'
84
+ label: 'Using conditions to control job execution'
85
+ - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#status-check-functions'
86
+ label: 'Status check functions: failure(), success(), cancelled(), always()'
@@ -0,0 +1,106 @@
1
+ id: triggers-059
2
+ title: "workflow_call required: true inputs silently receive empty string when omitted by caller"
3
+ category: triggers
4
+ severity: silent-failure
5
+ tags:
6
+ - reusable-workflow
7
+ - workflow-call
8
+ - inputs
9
+ - required
10
+ - validation
11
+ patterns:
12
+ - regex: 'required.*input.*not provided'
13
+ flags: i
14
+ - regex: 'Input .+ is required but was not provided'
15
+ flags: i
16
+ error_messages:
17
+ - "Input 'environment' is required but was not provided"
18
+ - "required: true input silently receives empty string when omitted from caller with:"
19
+ root_cause: |
20
+ When a reusable workflow declares `on.workflow_call.inputs` with `required: true`,
21
+ GitHub does NOT enforce this requirement at call time via the REST API or from other
22
+ calling workflows. The `required: true` flag is informational documentation only —
23
+ it is not validated before the workflow runs.
24
+
25
+ When a calling workflow omits a required input from its `with:` block, the reusable
26
+ workflow receives an empty string `""` for the missing input with no error or warning.
27
+ The workflow starts and runs to whatever conclusion the empty input causes.
28
+
29
+ This leads to silent failures such as deploying to an undefined environment, running
30
+ against an empty target URL, or passing `""` to shell commands that produce incorrect
31
+ results. The bug is especially hard to find because the calling workflow shows a green
32
+ "success" until the silent empty input causes a downstream failure.
33
+ fix: |
34
+ Explicitly validate all required inputs at the start of the reusable workflow:
35
+ 1. Add an input-validation step as the FIRST step of the FIRST job that checks each
36
+ required input for emptiness and fails with a descriptive error.
37
+ 2. Use `default:` values in `on.workflow_call.inputs` to handle omitted inputs
38
+ gracefully with a fallback rather than silently using empty string.
39
+ 3. In calling workflows, always pass all inputs explicitly with `with:` — never
40
+ rely on `required: true` to catch missing inputs.
41
+ fix_code:
42
+ - language: yaml
43
+ label: "Reusable workflow: validate required inputs at startup"
44
+ code: |
45
+ # .github/workflows/deploy.yml (reusable workflow)
46
+ on:
47
+ workflow_call:
48
+ inputs:
49
+ environment:
50
+ required: true # documentation only — NOT enforced by GitHub
51
+ type: string
52
+ target-url:
53
+ required: true
54
+ type: string
55
+
56
+ jobs:
57
+ validate-inputs:
58
+ runs-on: ubuntu-latest
59
+ steps:
60
+ - name: Validate required inputs
61
+ run: |
62
+ ERRORS=()
63
+ if [[ -z "${{ inputs.environment }}" ]]; then
64
+ ERRORS+=("Required input 'environment' was not provided")
65
+ fi
66
+ if [[ -z "${{ inputs.target-url }}" ]]; then
67
+ ERRORS+=("Required input 'target-url' was not provided")
68
+ fi
69
+ if [[ ${#ERRORS[@]} -gt 0 ]]; then
70
+ for ERR in "${ERRORS[@]}"; do
71
+ echo "::error::$ERR"
72
+ done
73
+ exit 1
74
+ fi
75
+
76
+ deploy:
77
+ needs: validate-inputs
78
+ runs-on: ubuntu-latest
79
+ steps:
80
+ - run: echo "Deploying to ${{ inputs.environment }}"
81
+ - language: yaml
82
+ label: "Fix: use default: values to avoid silent empty string behavior"
83
+ code: |
84
+ on:
85
+ workflow_call:
86
+ inputs:
87
+ environment:
88
+ required: false
89
+ default: 'staging' # explicit default instead of relying on required: true
90
+ type: string
91
+
92
+ jobs:
93
+ deploy:
94
+ runs-on: ubuntu-latest
95
+ steps:
96
+ - run: echo "Deploying to ${{ inputs.environment }}" # always 'staging' if omitted
97
+ prevention:
98
+ - "Treat required: true on workflow_call inputs as documentation only — always validate inside the reusable workflow."
99
+ - "Add a dedicated input-validation job as the first job in every reusable workflow with required inputs."
100
+ - "Prefer default: values over required: true when a sensible fallback exists."
101
+ - "In calling workflows, always explicitly pass all inputs in the with: block."
102
+ docs:
103
+ - url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onworkflow_callinputs"
104
+ label: "on.workflow_call.inputs syntax reference"
105
+ - url: "https://docs.github.com/en/actions/sharing-automations/reusing-workflows#using-inputs-and-secrets-in-a-called-workflow"
106
+ label: "Using inputs in a called workflow"
@@ -0,0 +1,94 @@
1
+ id: yaml-syntax-054
2
+ title: "env context silently empty in reusable workflow with: inputs"
3
+ category: yaml-syntax
4
+ severity: silent-failure
5
+ tags:
6
+ - reusable-workflow
7
+ - env-context
8
+ - with-inputs
9
+ - workflow-call
10
+ - context-availability
11
+ patterns:
12
+ - regex: '"env" context is not available in this context'
13
+ flags: i
14
+ - regex: 'env context.*not available.*reusable'
15
+ flags: i
16
+ error_messages:
17
+ - '"env" context is not available in this context'
18
+ - "Input evaluates to empty string when env context used in reusable workflow with:"
19
+ root_cause: |
20
+ The `env` context is NOT available inside `jobs.<job_id>.with:` blocks used to
21
+ call reusable workflows. Unlike `jobs.<job_id>.steps.with:` (regular action steps,
22
+ which support `env` context), reusable workflow `with:` inputs are evaluated before
23
+ job-level environment variables are injected.
24
+
25
+ Using `${{ env.MY_VAR }}` in a reusable workflow `with:` block silently evaluates
26
+ to an empty string. No error is thrown and the reusable workflow receives `""` for
27
+ that input. The actionlint static linter reports:
28
+ `"env" context is not available in this context`.
29
+
30
+ This is explicitly documented in GitHub's context availability table — `env` is
31
+ available in `jobs.<job_id>.steps.with` but NOT in `jobs.<job_id>.with`.
32
+ fix: |
33
+ Replace `env` context references in reusable workflow `with:` blocks with one of:
34
+ 1. `vars` context — for repository or organization-level configuration variables
35
+ (Settings → Variables). Available in reusable workflow `with:`.
36
+ 2. Inline literal values directly in `with:`.
37
+ 3. Workflow-level `inputs:` context if the calling workflow is itself a reusable
38
+ workflow (chain input passing).
39
+ 4. A prior job that captures the value and passes it via `needs.<job>.outputs`.
40
+ fix_code:
41
+ - language: yaml
42
+ label: "Wrong: env context in reusable workflow with: (silently evaluates to empty string)"
43
+ code: |
44
+ env:
45
+ BASE_URL: 'https://api.example.com'
46
+ DEPLOY_ENV: 'production'
47
+
48
+ jobs:
49
+ call-deploy:
50
+ uses: ./.github/workflows/deploy.yml
51
+ with:
52
+ api-url: ${{ env.BASE_URL }} # silently empty — env unavailable in reusable with:
53
+ environment: ${{ env.DEPLOY_ENV }} # also silently empty
54
+ - language: yaml
55
+ label: "Fix: use vars context or inline value"
56
+ code: |
57
+ # Set BASE_URL as a repository variable in Settings → Secrets and variables → Variables
58
+ jobs:
59
+ call-deploy:
60
+ uses: ./.github/workflows/deploy.yml
61
+ with:
62
+ api-url: ${{ vars.BASE_URL }} # vars IS available in reusable with:
63
+ environment: 'production' # inline literal also works
64
+ - language: yaml
65
+ label: "Fix: pass via prior job output if value is dynamic"
66
+ code: |
67
+ jobs:
68
+ resolve-config:
69
+ runs-on: ubuntu-latest
70
+ outputs:
71
+ api-url: ${{ steps.config.outputs.url }}
72
+ steps:
73
+ - id: config
74
+ run: echo "url=$BASE_URL" >> "$GITHUB_OUTPUT"
75
+ env:
76
+ BASE_URL: ${{ env.BASE_URL }} # env IS available in step env:
77
+
78
+ call-deploy:
79
+ needs: resolve-config
80
+ uses: ./.github/workflows/deploy.yml
81
+ with:
82
+ api-url: ${{ needs.resolve-config.outputs.api-url }} # needs context available
83
+ prevention:
84
+ - "Consult the GitHub Actions context availability table before using contexts in reusable workflow with: blocks."
85
+ - "Prefer vars context (repository/org variables) over env for values shared across workflows."
86
+ - "Run actionlint in CI — it detects unavailable context usage and catches this before runtime."
87
+ - "If a reusable workflow input is silently empty, check context availability as the first debugging step."
88
+ docs:
89
+ - url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/contexts#context-availability"
90
+ label: "GitHub Actions context availability table"
91
+ - url: "https://docs.github.com/en/actions/sharing-automations/reusing-workflows#passing-inputs-and-secrets-to-a-called-workflow"
92
+ label: "Passing inputs and secrets to a called workflow"
93
+ - url: "https://rhysd.github.io/actionlint/"
94
+ label: "actionlint — static checker for GitHub Actions"
@@ -0,0 +1,88 @@
1
+ id: yaml-syntax-055
2
+ title: '`on.workflow_call.outputs.value` references `steps.<id>.outputs` — steps context unavailable at workflow scope; must use `jobs.<job_id>.outputs`'
3
+ category: yaml-syntax
4
+ severity: error
5
+ tags:
6
+ - reusable-workflow
7
+ - workflow_call
8
+ - outputs
9
+ - steps-context
10
+ - jobs-context
11
+ - empty-string
12
+ - scope
13
+ patterns:
14
+ - regex: 'steps\.\w+\.outputs\.\w+'
15
+ flags: 'i'
16
+ error_messages:
17
+ - "Reusable workflow output is empty — steps context not accessible in on.workflow_call.outputs.value"
18
+ - "on.workflow_call.outputs value references steps context which is not available at workflow scope"
19
+ root_cause: |
20
+ In a reusable workflow, `on.workflow_call.outputs[*].value:` expressions are
21
+ evaluated at the WORKFLOW level, not within a job. The `steps` context is scoped to
22
+ a specific job and is only accessible inside that job's step expressions — it is
23
+ not visible at the top-level workflow scope where `on.workflow_call.outputs` lives.
24
+
25
+ Using `${{ steps.upload.outputs.artifact-id }}` in the `value:` field silently
26
+ evaluates to an empty string. No error, warning, or annotation is raised; the
27
+ caller simply receives an empty output value.
28
+
29
+ The correct pattern requires two explicit hops:
30
+ 1. Capture the step output at the JOB level using the job's `outputs:` block.
31
+ 2. Reference the JOB output at the WORKFLOW level using `${{ jobs.<job_id>.outputs.<key> }}`.
32
+
33
+ Note: this is distinct from yaml-syntax-032 (jobs.<id>.result empty in workflow_call
34
+ outputs) which covers the result property specifically, not step output propagation.
35
+ fix: |
36
+ Bridge step outputs up through the job's `outputs:` block first, then reference the
37
+ job output in `on.workflow_call.outputs.value`.
38
+
39
+ Pattern:
40
+ step writes to GITHUB_OUTPUT (or action output)
41
+ → job outputs: exposes it as jobs.<job_id>.outputs.<key>
42
+ → on.workflow_call.outputs.value references jobs.<job_id>.outputs.<key>
43
+ fix_code:
44
+ - language: yaml
45
+ label: 'Correct two-hop output propagation for reusable workflows'
46
+ code: |
47
+ on:
48
+ workflow_call:
49
+ outputs:
50
+ artifact-id:
51
+ description: 'ID of the uploaded build artifact'
52
+ # CORRECT: reference job-level output
53
+ value: ${{ jobs.build.outputs.artifact-id }}
54
+ # WRONG (silent empty string):
55
+ # value: ${{ steps.upload.outputs.artifact-id }}
56
+
57
+ jobs:
58
+ build:
59
+ runs-on: ubuntu-latest
60
+ outputs:
61
+ # Hop 1: expose step output at job level
62
+ artifact-id: ${{ steps.upload.outputs.artifact-id }}
63
+ steps:
64
+ - uses: actions/checkout@v4
65
+
66
+ - name: Build
67
+ run: npm ci && npm run build
68
+
69
+ - name: Upload artifact
70
+ id: upload
71
+ uses: actions/upload-artifact@v4
72
+ with:
73
+ name: build-output
74
+ path: dist/
75
+ # Hop 2: job outputs block above captures steps.upload.outputs.artifact-id
76
+ # Hop 3: on.workflow_call.outputs.value references jobs.build.outputs.artifact-id
77
+ prevention:
78
+ - 'The `steps` context is job-scoped. Always bridge step outputs through the job `outputs:` block before referencing them at workflow level in `on.workflow_call.outputs.value:`.'
79
+ - 'Use actionlint to statically analyze reusable workflow output expressions — it reports invalid context usage at workflow scope.'
80
+ - 'When a reusable workflow output is unexpectedly empty in the caller, check whether the `value:` expression uses `steps.` (wrong scope) vs `jobs.` (correct scope).'
81
+ - 'Each output needs both a job-level `outputs:` mapping AND a workflow-level `on.workflow_call.outputs[*].value:` reference — missing either hop silently returns empty string.'
82
+ docs:
83
+ - url: 'https://docs.github.com/en/actions/sharing-automations/reusing-workflows#using-outputs-from-a-reusable-workflow'
84
+ label: 'Reusable workflows: using outputs'
85
+ - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs'
86
+ label: 'Passing information between jobs (job outputs)'
87
+ - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context'
88
+ label: 'Steps context — job-scoped, not available at workflow level'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@htekdev/actions-debugger",
3
- "version": "1.0.86",
3
+ "version": "1.0.88",
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",