@htekdev/actions-debugger 1.0.0 → 1.0.1

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.
Files changed (53) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +108 -108
  3. package/errors/_schema.json +89 -89
  4. package/errors/caching-artifacts/artifact-storage-quota-exceeded.yml +118 -0
  5. package/errors/caching-artifacts/cache-miss.yml +56 -56
  6. package/errors/caching-artifacts/cache-save-cancelled-job.yml +82 -0
  7. package/errors/caching-artifacts/cache-v3-to-v4-breaking-changes.yml +95 -0
  8. package/errors/caching-artifacts/cross-repo-artifacts-not-supported.yml +102 -0
  9. package/errors/caching-artifacts/upload-artifact-no-files-found.yml +92 -0
  10. package/errors/caching-artifacts/upload-artifact-v4-breaking.yml +67 -67
  11. package/errors/concurrency-timing/cancel-in-progress-deploy-drops.yml +97 -0
  12. package/errors/concurrency-timing/jobs-cancelled-unexpectedly.yml +60 -60
  13. package/errors/concurrency-timing/skipped-needs-cascade.yml +103 -0
  14. package/errors/concurrency-timing/workflow-run-conclusion-unchecked.yml +100 -0
  15. package/errors/known-unsolved/composite-input-env-vars-missing.yml +91 -0
  16. package/errors/known-unsolved/composite-nested-outputs-null.yml +101 -0
  17. package/errors/known-unsolved/no-dynamic-secret-access.yml +111 -0
  18. package/errors/known-unsolved/no-step-level-rerun.yml +94 -0
  19. package/errors/known-unsolved/no-step-retry.yml +53 -53
  20. package/errors/permissions-auth/checkout-submodule-private-auth.yml +91 -0
  21. package/errors/permissions-auth/fork-pr-secrets-unavailable.yml +97 -0
  22. package/errors/permissions-auth/github-token-403.yml +64 -64
  23. package/errors/permissions-auth/github-token-protected-branch-push.yml +109 -0
  24. package/errors/permissions-auth/oidc-aws-failure.yml +85 -85
  25. package/errors/permissions-auth/oidc-azure-subject-mismatch.yml +91 -0
  26. package/errors/runner-environment/disk-space.yml +57 -57
  27. package/errors/runner-environment/docker-buildx-not-setup.yml +106 -0
  28. package/errors/runner-environment/macos-homebrew-path.yml +90 -0
  29. package/errors/runner-environment/node-runtime-deprecation.yml +56 -56
  30. package/errors/runner-environment/npm-ci-lockfile-mismatch.yml +112 -0
  31. package/errors/runner-environment/self-hosted-stale-toolcache.yml +73 -0
  32. package/errors/runner-environment/setup-node-version-file-missing.yml +105 -0
  33. package/errors/runner-environment/windows-execution-policy.yml +83 -0
  34. package/errors/silent-failures/add-mask-no-retroactive-masking.yml +75 -0
  35. package/errors/silent-failures/composite-boolean-inputs-as-strings.yml +110 -0
  36. package/errors/silent-failures/conditional-output-null-downstream.yml +82 -0
  37. package/errors/silent-failures/continue-on-error-masks-failure.yml +86 -0
  38. package/errors/silent-failures/github-token-no-trigger.yml +57 -57
  39. package/errors/silent-failures/reusable-workflow-env-secrets-empty.yml +90 -0
  40. package/errors/silent-failures/scheduled-workflow-disabled.yml +59 -59
  41. package/errors/triggers/cron-schedule-late.yml +59 -59
  42. package/errors/triggers/pull-request-target-rce-risk.yml +117 -0
  43. package/errors/triggers/workflow-not-triggering.yml +60 -60
  44. package/errors/triggers/workflow-run-default-branch-requirement.yml +78 -0
  45. package/errors/yaml-syntax/anchors-not-supported.yml +95 -0
  46. package/errors/yaml-syntax/dynamic-matrix-fromjson-failure.yml +99 -0
  47. package/errors/yaml-syntax/if-always-true.yml +52 -52
  48. package/errors/yaml-syntax/missing-expression-wrapper.yml +67 -0
  49. package/errors/yaml-syntax/needs-indirect-outputs.yml +91 -0
  50. package/errors/yaml-syntax/secrets-in-if.yml +55 -55
  51. package/errors/yaml-syntax/unexpected-yaml-key.yml +69 -69
  52. package/errors/yaml-syntax/working-directory-ignored-on-uses.yml +66 -0
  53. package/package.json +70 -67
@@ -0,0 +1,97 @@
1
+ id: concurrency-timing-002
2
+ title: "cancel-in-progress Silently Drops In-Flight Deploy Runs"
3
+ category: concurrency-timing
4
+ severity: silent-failure
5
+ tags:
6
+ - concurrency
7
+ - cancel-in-progress
8
+ - deployment
9
+ - silent-failure
10
+ - queue
11
+ patterns:
12
+ - regex: "Run was cancelled"
13
+ flags: "i"
14
+ - regex: "Canceling since a higher priority waiting run was found"
15
+ flags: "i"
16
+ - regex: "This run was cancelled by a more recent run"
17
+ flags: "i"
18
+ error_messages:
19
+ - "Run was cancelled"
20
+ - "Canceling since a higher priority waiting run was found"
21
+ - "This run was cancelled by a more recent run"
22
+ root_cause: |
23
+ When a workflow sets `concurrency.cancel-in-progress: true`, GitHub Actions cancels
24
+ any currently running workflow in the same concurrency group the moment a new run
25
+ starts. This is intentional for CI (cancel stale PR checks), but is dangerous for
26
+ **deploy** workflows where every commit must be deployed in order.
27
+
28
+ The silent failure happens because:
29
+ - The cancelled run is marked "CANCELLED" in the UI — it does not appear as a
30
+ failure, so PR status checks may still pass.
31
+ - If the cancelled run was mid-deploy (e.g., partway through Terraform apply or a
32
+ Kubernetes rollout), the environment is left in a partially-updated state.
33
+ - Developers see the new run succeed and assume all commits were deployed, when in
34
+ reality one or more intermediate commits were silently skipped.
35
+
36
+ A second failure mode occurs with the concurrency queue: GitHub keeps at most one
37
+ *pending* run per group. If a third run starts while one is running and one is pending,
38
+ the older pending run is cancelled to queue the latest one — again silently.
39
+
40
+ Both behaviors are documented but frequently misunderstood when the same workflow
41
+ serves both CI (fast feedback) and CD (reliable delivery) purposes.
42
+ fix: |
43
+ For deploy workflows, set `cancel-in-progress: false` and use a per-branch concurrency
44
+ group so only one deploy runs per branch at a time but no run is silently dropped.
45
+
46
+ If you need cancel-in-progress for PR CI, split your workflow into separate files:
47
+ one for CI (cancel-in-progress: true) and one for deploys (cancel-in-progress: false).
48
+ fix_code:
49
+ - language: yaml
50
+ label: "Safe deploy workflow — never silently cancel"
51
+ code: |
52
+ name: Deploy
53
+ on:
54
+ push:
55
+ branches: [main]
56
+
57
+ concurrency:
58
+ # One deploy at a time per branch, but queue — never cancel
59
+ group: deploy-${{ github.ref }}
60
+ cancel-in-progress: false
61
+
62
+ jobs:
63
+ deploy:
64
+ runs-on: ubuntu-latest
65
+ steps:
66
+ - uses: actions/checkout@v4
67
+ - run: ./scripts/deploy.sh
68
+ - language: yaml
69
+ label: "Split CI (cancel ok) vs Deploy (queue, no cancel) in one repo"
70
+ code: |
71
+ # .github/workflows/ci.yml — cancel stale PR checks is fine
72
+ name: CI
73
+ on: pull_request
74
+ concurrency:
75
+ group: ci-${{ github.ref }}
76
+ cancel-in-progress: true # OK: PR checks, not deploys
77
+
78
+ # .github/workflows/deploy.yml — never drop a deploy
79
+ name: Deploy
80
+ on:
81
+ push:
82
+ branches: [main, 'release/**']
83
+ concurrency:
84
+ group: deploy-${{ github.ref }}
85
+ cancel-in-progress: false # Queue; never skip a commit
86
+ prevention:
87
+ - "Never use `cancel-in-progress: true` on workflows that deploy, apply infrastructure changes, or produce side effects that must run for every commit."
88
+ - "Monitor the Actions tab for unexpected CANCELLED runs — a CANCELLED deploy job is NOT the same as a skipped/passing check."
89
+ - "Use separate workflow files for CI and CD to apply different concurrency strategies."
90
+ - "Set the concurrency group key to include `github.ref` so branches don't cancel each other."
91
+ docs:
92
+ - url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/using-concurrency"
93
+ label: "Using concurrency"
94
+ - url: "https://github.com/actions/runner/issues/3722"
95
+ label: "actions/runner#3722 — Concurrency cancellation behaviors"
96
+ - url: "https://github.com/orgs/community/discussions/5435"
97
+ label: "Community discussion: cancel-in-progress semantics"
@@ -1,60 +1,60 @@
1
- id: concurrency-timing-001
2
- title: "Jobs Cancelled Unexpectedly"
3
- category: concurrency-timing
4
- severity: warning
5
- tags:
6
- - concurrency
7
- - cancellation
8
- - matrix
9
- - timing
10
- - branch-isolation
11
- patterns:
12
- - regex: "Canceling since a higher priority waiting request for '.+' exists"
13
- flags: "i"
14
- - regex: "The workflow run was canceled because another workflow run with the same concurrency group was queued"
15
- flags: "i"
16
- - regex: "Operation was canceled\\."
17
- flags: "i"
18
- error_messages:
19
- - "Canceling since a higher priority waiting request for 'deploy' exists"
20
- - "Operation was canceled."
21
- root_cause: |
22
- Concurrency groups are global to the repository unless you scope them yourself. If every
23
- branch uses the same group name, a push on one branch can cancel a run on a completely
24
- different branch. Matrix jobs can make this harder to spot because only some legs appear to
25
- vanish, even though the concurrency rule is what canceled them.
26
-
27
- The workflow is behaving exactly as configured, but the group name is too broad.
28
- fix: |
29
- Include branch or ref information in the concurrency group so only related runs cancel one
30
- another. Keep `cancel-in-progress: true` only when you truly want a new run to replace the
31
- older run for the same ref.
32
- fix_code:
33
- - language: yaml
34
- label: "Scope concurrency groups by workflow and ref"
35
- code: |
36
- concurrency:
37
- group: ${{ github.workflow }}-${{ github.ref }}
38
- cancel-in-progress: true
39
-
40
- jobs:
41
- test:
42
- runs-on: ubuntu-latest
43
- strategy:
44
- matrix:
45
- node: [18, 20]
46
- steps:
47
- - uses: actions/checkout@v4
48
- - run: npm test
49
- prevention:
50
- - "Never use a bare group like `deploy` or `ci` unless cross-branch cancellation is intentional."
51
- - "Include `${{ github.ref }}` or `${{ github.head_ref || github.ref }}` in concurrency groups."
52
- - "Review concurrency settings any time runs are mysteriously disappearing."
53
- docs:
54
- - url: "https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency"
55
- label: "Control the concurrency of workflows and jobs"
56
- - url: "https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions"
57
- label: "Workflow syntax for GitHub Actions"
58
- source:
59
- article: "https://htek.dev/articles/github-actions-debugging-guide"
60
- section: "Unexpected cancellations from concurrency"
1
+ id: concurrency-timing-001
2
+ title: "Jobs Cancelled Unexpectedly"
3
+ category: concurrency-timing
4
+ severity: warning
5
+ tags:
6
+ - concurrency
7
+ - cancellation
8
+ - matrix
9
+ - timing
10
+ - branch-isolation
11
+ patterns:
12
+ - regex: "Canceling since a higher priority waiting request for '.+' exists"
13
+ flags: "i"
14
+ - regex: "The workflow run was canceled because another workflow run with the same concurrency group was queued"
15
+ flags: "i"
16
+ - regex: "Operation was canceled\\."
17
+ flags: "i"
18
+ error_messages:
19
+ - "Canceling since a higher priority waiting request for 'deploy' exists"
20
+ - "Operation was canceled."
21
+ root_cause: |
22
+ Concurrency groups are global to the repository unless you scope them yourself. If every
23
+ branch uses the same group name, a push on one branch can cancel a run on a completely
24
+ different branch. Matrix jobs can make this harder to spot because only some legs appear to
25
+ vanish, even though the concurrency rule is what canceled them.
26
+
27
+ The workflow is behaving exactly as configured, but the group name is too broad.
28
+ fix: |
29
+ Include branch or ref information in the concurrency group so only related runs cancel one
30
+ another. Keep `cancel-in-progress: true` only when you truly want a new run to replace the
31
+ older run for the same ref.
32
+ fix_code:
33
+ - language: yaml
34
+ label: "Scope concurrency groups by workflow and ref"
35
+ code: |
36
+ concurrency:
37
+ group: ${{ github.workflow }}-${{ github.ref }}
38
+ cancel-in-progress: true
39
+
40
+ jobs:
41
+ test:
42
+ runs-on: ubuntu-latest
43
+ strategy:
44
+ matrix:
45
+ node: [18, 20]
46
+ steps:
47
+ - uses: actions/checkout@v4
48
+ - run: npm test
49
+ prevention:
50
+ - "Never use a bare group like `deploy` or `ci` unless cross-branch cancellation is intentional."
51
+ - "Include `${{ github.ref }}` or `${{ github.head_ref || github.ref }}` in concurrency groups."
52
+ - "Review concurrency settings any time runs are mysteriously disappearing."
53
+ docs:
54
+ - url: "https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency"
55
+ label: "Control the concurrency of workflows and jobs"
56
+ - url: "https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions"
57
+ label: "Workflow syntax for GitHub Actions"
58
+ source:
59
+ article: "https://htek.dev/articles/github-actions-debugging-guide"
60
+ section: "Unexpected cancellations from concurrency"
@@ -0,0 +1,103 @@
1
+ id: concurrency-timing-004
2
+ title: "Skipped needs Job Cascades Skip to All Dependent Jobs"
3
+ category: concurrency-timing
4
+ severity: silent-failure
5
+ tags:
6
+ - needs
7
+ - skipped
8
+ - cascade
9
+ - conditionals
10
+ - job-status
11
+ - always
12
+ - dependency
13
+ patterns:
14
+ - regex: "skipped.*needs.*skipped"
15
+ flags: "i"
16
+ - regex: "This job was skipped"
17
+ flags: "i"
18
+ - regex: "Result: skipped"
19
+ flags: "i"
20
+ error_messages:
21
+ - "This job was skipped."
22
+ - "Skipping this job because a previous job in the chain was skipped."
23
+ root_cause: |
24
+ When a job is **skipped** (because its `if:` condition evaluated to false), GitHub
25
+ Actions propagates the skip status to all jobs that `needs` it. Dependent jobs are
26
+ skipped automatically unless they explicitly handle this with `if: always()` or by
27
+ checking `needs.<id>.result == 'skipped'`.
28
+
29
+ This cascades silently: if `build` is skipped on non-main branches, then `test` (which
30
+ needs `build`) is also skipped, and `deploy` (which needs `test`) is also skipped —
31
+ even if `deploy` has no condition of its own and the developer expected it to run.
32
+
33
+ The cascade happens regardless of whether the dependency was skipped by an `if:` guard
34
+ or by the job being cancelled. The default behavior treats a skipped upstream job the
35
+ same as a failed one from the perspective of downstream jobs.
36
+ fix: |
37
+ Use `if: always()` or explicit result checks (`needs.<id>.result == 'success' || needs.<id>.result == 'skipped'`)
38
+ on downstream jobs that should run regardless of whether an upstream job was skipped.
39
+ fix_code:
40
+ - language: yaml
41
+ label: "WRONG — notification job silently skipped when build is skipped"
42
+ code: |
43
+ jobs:
44
+ build:
45
+ runs-on: ubuntu-latest
46
+ if: github.ref == 'refs/heads/main' # only runs on main
47
+ steps:
48
+ - run: npm run build
49
+
50
+ notify:
51
+ needs: build # skipped when build is skipped (non-main branches)
52
+ runs-on: ubuntu-latest
53
+ steps:
54
+ - run: ./notify-slack.sh "Build complete"
55
+ - language: yaml
56
+ label: "CORRECT — notify even when build was skipped"
57
+ code: |
58
+ jobs:
59
+ build:
60
+ runs-on: ubuntu-latest
61
+ if: github.ref == 'refs/heads/main'
62
+ steps:
63
+ - run: npm run build
64
+
65
+ notify:
66
+ needs: build
67
+ if: always() # runs regardless of build's outcome or skip
68
+ runs-on: ubuntu-latest
69
+ steps:
70
+ - run: |
71
+ RESULT="${{ needs.build.result }}"
72
+ if [ "$RESULT" = "success" ]; then
73
+ ./notify-slack.sh "Build succeeded"
74
+ elif [ "$RESULT" = "skipped" ]; then
75
+ echo "Build was skipped (non-main branch) — no notification needed"
76
+ else
77
+ ./notify-slack.sh "Build $RESULT"
78
+ fi
79
+ - language: yaml
80
+ label: "CORRECT — final job that always runs regardless of upstream"
81
+ code: |
82
+ jobs:
83
+ deploy:
84
+ needs: [build, test]
85
+ # Run if upstream succeeded OR if they were skipped (allows partial pipelines)
86
+ if: |
87
+ always() &&
88
+ (needs.build.result == 'success' || needs.build.result == 'skipped') &&
89
+ (needs.test.result == 'success' || needs.test.result == 'skipped')
90
+ runs-on: ubuntu-latest
91
+ steps:
92
+ - run: ./deploy.sh
93
+ prevention:
94
+ - "Use `if: always()` on jobs that must run regardless of upstream outcomes (cleanup, notifications, summaries)."
95
+ - "Check `needs.<id>.result` explicitly when a job should behave differently based on upstream skip vs success vs failure."
96
+ - "Document which jobs in your pipeline are optional (skippable) vs required to avoid surprises."
97
+ docs:
98
+ - url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idif"
99
+ label: "jobs.<job_id>.if"
100
+ - url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds"
101
+ label: "jobs.<job_id>.needs"
102
+ - url: "https://docs.github.com/en/actions/learn-github-actions/expressions#status-check-functions"
103
+ label: "Status check functions (always, success, failure, cancelled)"
@@ -0,0 +1,100 @@
1
+ id: concurrency-timing-003
2
+ title: "workflow_run Downstream Job Ignores Source Workflow Conclusion"
3
+ category: concurrency-timing
4
+ severity: silent-failure
5
+ tags:
6
+ - workflow_run
7
+ - conclusion
8
+ - downstream
9
+ - deploy
10
+ - conditional
11
+ - completed
12
+ patterns:
13
+ - regex: "github\\.event\\.workflow_run\\.conclusion"
14
+ flags: "i"
15
+ - regex: "workflow_run.*completed.*always"
16
+ flags: "i"
17
+ error_messages:
18
+ - "Warning: workflow_run downstream job ran despite source workflow failing."
19
+ root_cause: |
20
+ The `workflow_run` trigger fires when a specified workflow completes, regardless of
21
+ whether that workflow **succeeded or failed**. The trigger type is `completed` — there
22
+ is no built-in `on_success` equivalent. If you do not check
23
+ `github.event.workflow_run.conclusion`, your downstream workflow runs even when the
24
+ upstream workflow failed, was cancelled, or was skipped.
25
+
26
+ This is a common mistake in CD pipelines where a deploy workflow is triggered by a CI
27
+ workflow run. Without a conclusion check, a failed test run triggers the deploy — which
28
+ may deploy broken code, send a notification for a non-event, or waste runner minutes.
29
+ fix: |
30
+ Always add a job-level `if:` condition checking `github.event.workflow_run.conclusion == 'success'`
31
+ in any `workflow_run`-triggered workflow that should only run on success.
32
+ fix_code:
33
+ - language: yaml
34
+ label: "WRONG — deploy runs regardless of CI conclusion"
35
+ code: |
36
+ on:
37
+ workflow_run:
38
+ workflows: ["CI"]
39
+ types: [completed]
40
+
41
+ jobs:
42
+ deploy:
43
+ runs-on: ubuntu-latest
44
+ # Missing conclusion check — deploys even when CI fails!
45
+ steps:
46
+ - run: ./deploy.sh
47
+ - language: yaml
48
+ label: "CORRECT — gate on success conclusion"
49
+ code: |
50
+ on:
51
+ workflow_run:
52
+ workflows: ["CI"]
53
+ types: [completed]
54
+
55
+ jobs:
56
+ deploy:
57
+ runs-on: ubuntu-latest
58
+ if: ${{ github.event.workflow_run.conclusion == 'success' }}
59
+ steps:
60
+ - run: ./deploy.sh
61
+
62
+ notify-failure:
63
+ runs-on: ubuntu-latest
64
+ if: ${{ github.event.workflow_run.conclusion == 'failure' }}
65
+ steps:
66
+ - run: ./notify-slack.sh "CI failed — no deploy"
67
+ - language: yaml
68
+ label: "CORRECT — handle all outcomes explicitly"
69
+ code: |
70
+ on:
71
+ workflow_run:
72
+ workflows: ["CI"]
73
+ types: [completed]
74
+
75
+ jobs:
76
+ gate:
77
+ runs-on: ubuntu-latest
78
+ steps:
79
+ - name: Check conclusion
80
+ run: |
81
+ CONCLUSION="${{ github.event.workflow_run.conclusion }}"
82
+ echo "Source workflow concluded: $CONCLUSION"
83
+ if [ "$CONCLUSION" != "success" ]; then
84
+ echo "::error::Upstream CI did not succeed (conclusion: $CONCLUSION) — aborting deploy"
85
+ exit 1
86
+ fi
87
+
88
+ deploy:
89
+ needs: gate
90
+ runs-on: ubuntu-latest
91
+ steps:
92
+ - run: ./deploy.sh
93
+ prevention:
94
+ - "Always add `if: github.event.workflow_run.conclusion == 'success'` to any job triggered by `workflow_run: completed`."
95
+ - "Consider adding a separate job for failure notifications to avoid silently swallowing upstream failures."
96
+ docs:
97
+ - url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_run"
98
+ label: "workflow_run event — conclusion values"
99
+ - url: "https://docs.github.com/en/actions/writing-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow"
100
+ label: "Triggering a workflow from a workflow"
@@ -0,0 +1,91 @@
1
+ id: known-unsolved-003
2
+ title: "Composite Actions Do Not Set INPUT_* Environment Variables"
3
+ category: known-unsolved
4
+ severity: limitation
5
+ tags:
6
+ - composite-actions
7
+ - INPUT_
8
+ - env-vars
9
+ - shell-scripts
10
+ - action-migration
11
+ patterns:
12
+ - regex: "\\$INPUT_[A-Z_]+"
13
+ flags: ""
14
+ - regex: "INPUT_[A-Z_]+:\\s*(unbound variable|empty|command not found)"
15
+ flags: "i"
16
+ error_messages:
17
+ - "INPUT_MY_PARAM: unbound variable"
18
+ - "$INPUT_MY_PARAM: empty"
19
+ root_cause: |
20
+ JavaScript actions and Docker container actions receive their inputs as `INPUT_*` environment
21
+ variables at runtime (e.g., the input `my-param` becomes `INPUT_MY-PARAM` or `INPUT_MY_PARAM`).
22
+ Composite actions do NOT follow this convention — they do not set any `INPUT_*` environment
23
+ variables on the runner.
24
+
25
+ Composite actions make inputs available ONLY through the `${{ inputs.name }}` expression context,
26
+ which is evaluated by the Actions runner before the shell command is invoked. Shell scripts
27
+ running inside composite `run:` steps cannot access inputs as `$INPUT_*` environment variables.
28
+
29
+ This limitation has been reported and discussed since 2020 (actions/runner#665, 66+ reactions).
30
+ GitHub has not implemented `INPUT_*` for composite actions. The likely reason is that composite
31
+ actions support multiple shells (bash, powershell, python) and the runner processes inputs as
32
+ templated expressions rather than environment variables for composites.
33
+
34
+ Developers who migrate a shell-script-based action from a JavaScript wrapper (where INPUT_*
35
+ was available via `core.getInput()` delegating to `process.env.INPUT_*`) to a composite action,
36
+ or who copy examples from Docker action documentation, will silently get empty values.
37
+ fix: |
38
+ There is no way to make composite actions set INPUT_* env vars automatically. The workarounds are:
39
+
40
+ Option 1 — inline expression (simple, but creates expression injection risk with untrusted input):
41
+ `run: echo "${{ inputs.my-param }}"`
42
+
43
+ Option 2 — explicit env: mapping on the step (recommended — safe for untrusted input):
44
+ ```yaml
45
+ - name: Use input
46
+ env:
47
+ MY_PARAM: ${{ inputs.my-param }}
48
+ run: echo "$MY_PARAM"
49
+ ```
50
+
51
+ Option 3 — convert to a JavaScript action if INPUT_* pattern is deeply embedded in scripts.
52
+ fix_code:
53
+ - language: yaml
54
+ label: "Map composite action inputs to env vars explicitly on each step"
55
+ code: |
56
+ # action.yml (composite action)
57
+ inputs:
58
+ deploy-env:
59
+ description: "Target environment name"
60
+ required: true
61
+ dry-run:
62
+ description: "Whether to perform a dry run"
63
+ required: false
64
+ default: "false"
65
+
66
+ runs:
67
+ using: composite
68
+ steps:
69
+ # ❌ WRONG — INPUT_DEPLOY_ENV and INPUT_DRY_RUN are not set in composite actions
70
+ # - run: ./scripts/deploy.sh "$INPUT_DEPLOY_ENV" "$INPUT_DRY_RUN"
71
+ # shell: bash
72
+
73
+ # ✅ CORRECT — map to env vars in the step's env: block
74
+ - name: Deploy
75
+ env:
76
+ DEPLOY_ENV: ${{ inputs.deploy-env }}
77
+ DRY_RUN: ${{ inputs.dry-run }}
78
+ shell: bash
79
+ run: ./scripts/deploy.sh "$DEPLOY_ENV" "$DRY_RUN"
80
+ prevention:
81
+ - "Never reference `INPUT_*` env vars in composite action shell scripts — they are not set."
82
+ - "When converting a JavaScript or Docker action to composite, audit all `INPUT_*` and `process.env.INPUT_*` references."
83
+ - "Use the `env:` block on each `run:` step to explicitly map inputs to environment variables."
84
+ - "For untrusted user inputs, always use the `env:` mapping pattern rather than inline `${{ inputs.name }}` in shell commands to prevent injection."
85
+ docs:
86
+ - url: "https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs-for-composite-actions"
87
+ label: "Metadata syntax — runs for composite actions"
88
+ - url: "https://docs.github.com/en/actions/creating-actions/creating-a-composite-action"
89
+ label: "Creating a composite action"
90
+ - url: "https://github.com/actions/runner/issues/665"
91
+ label: "actions/runner#665 — INPUT_* env vars missing in composite actions (open since 2020, 66+ reactions)"
@@ -0,0 +1,101 @@
1
+ id: known-unsolved-004
2
+ title: "Composite Action Nested Step Outputs Evaluate to null in Calling Workflow"
3
+ category: known-unsolved
4
+ severity: limitation
5
+ tags:
6
+ - composite-actions
7
+ - nested-actions
8
+ - step-outputs
9
+ - actions-runner
10
+ - known-bug
11
+ patterns:
12
+ - regex: "##\\[debug\\]Evaluating: steps\\.[^.]+\\.outputs\\.[^\\s]+"
13
+ flags: "i"
14
+ - regex: "##\\[debug\\]=> null"
15
+ flags: "i"
16
+ - regex: "value: '' is not"
17
+ flags: "i"
18
+ error_messages:
19
+ - "##[debug]Evaluating: steps.setter.outputs.some-val"
20
+ - "##[debug]=> null"
21
+ - "Error: value: '' is not 'expected-value'"
22
+ root_cause: |
23
+ When a composite action is nested inside another composite action (A calls B, and B
24
+ exposes step outputs), the runner loses the `steps` context for the inner composite.
25
+ References like `steps.<id>.outputs.<name>` inside the nested composite evaluate to
26
+ `null`, and the outer composite (or the calling workflow) receives an empty string
27
+ instead of the intended output value.
28
+
29
+ The root cause is a context-wiring bug in the Actions runner (documented in
30
+ actions/runner#2009): the runner does not correctly register and propagate the
31
+ `steps` context for nested composite actions, particularly in post-steps. Each
32
+ composite "layer" should maintain its own steps context, but in practice the context
33
+ from the outer layer bleeds through or is lost entirely.
34
+
35
+ A related issue (actions/runner#2030) describes nested composite post-steps inheriting
36
+ the wrong context from the parent action.
37
+
38
+ **This is a known runner limitation.** There is no clean fix — only workarounds.
39
+ The runner team is aware; check the issue threads for status on any official fix.
40
+ fix: |
41
+ There is no direct fix. Use one of these workarounds depending on your situation:
42
+
43
+ **Workaround 1 — Flatten nested composites**: Avoid nesting composite actions when
44
+ step outputs must propagate. Move the logic into a single top-level composite that
45
+ exposes outputs directly to the calling workflow.
46
+
47
+ **Workaround 2 — Use a published remote action**: Reference the inner composite as a
48
+ published action (`uses: owner/repo@ref`) rather than a local nested reference. Some
49
+ reporters find the context bug is avoided when the inner action runs in its own
50
+ repository context.
51
+
52
+ **Workaround 3 — Pass values via environment files**: Instead of `steps.*.outputs.*`,
53
+ write values to `$GITHUB_ENV` or `$GITHUB_OUTPUT` at the top-level composite step and
54
+ read them from there. This sidesteps the `steps` context lookup entirely.
55
+
56
+ **Workaround 4 — Use a JavaScript/Docker action**: If output propagation is critical,
57
+ rewrite the inner composite as a JavaScript or container action, which has correct
58
+ lifecycle and output semantics.
59
+ fix_code:
60
+ - language: yaml
61
+ label: "Flatten: expose output directly from top-level composite"
62
+ code: |
63
+ # action.yml (top-level composite — do NOT nest another composite inside)
64
+ name: My Action
65
+ outputs:
66
+ result:
67
+ description: "The computed result"
68
+ value: ${{ steps.compute.outputs.result }}
69
+ runs:
70
+ using: composite
71
+ steps:
72
+ - id: compute
73
+ shell: bash
74
+ run: echo "result=hello-world" >> $GITHUB_OUTPUT
75
+ - language: yaml
76
+ label: "Workaround: write to GITHUB_OUTPUT from inner step directly"
77
+ code: |
78
+ # Instead of relying on steps.*.outputs.* in a nested composite,
79
+ # write directly to the top-level $GITHUB_OUTPUT from within the step:
80
+ runs:
81
+ using: composite
82
+ steps:
83
+ - shell: bash
84
+ run: |
85
+ COMPUTED=$(./scripts/compute.sh)
86
+ # Write directly to top-level output file
87
+ echo "result=${COMPUTED}" >> $GITHUB_OUTPUT
88
+ prevention:
89
+ - "Avoid nesting composite actions more than one level deep when step outputs must propagate."
90
+ - "Test output propagation explicitly (assert the output equals the expected value) before relying on it in production workflows."
91
+ - "Prefer JavaScript actions over composite actions when reliable output propagation is critical."
92
+ - "Track actions/runner#2009 for an official fix — the limitation may be resolved in a future runner version."
93
+ docs:
94
+ - url: "https://github.com/actions/runner/issues/2009"
95
+ label: "actions/runner#2009 — Composite: Step Outputs not available in nested composite actions"
96
+ - url: "https://github.com/actions/runner/issues/2030"
97
+ label: "actions/runner#2030 — Composite: Nested actions post steps have wrong context"
98
+ - url: "https://github.com/actions/runner/issues/646"
99
+ label: "actions/runner#646 — Composite action feature limitations"
100
+ - url: "https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-composite-action"
101
+ label: "Creating a composite action"