@htekdev/actions-debugger 1.0.59 → 1.0.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/errors/caching-artifacts/fork-pr-cache-isolation.yml +114 -0
- package/errors/concurrency-timing/cancel-in-progress-required-check-cancelled-conclusion.yml +84 -0
- package/errors/known-unsolved/skipped-job-outputs-empty-string.yml +109 -0
- package/errors/permissions-auth/github-token-cross-repo-dispatch-limitation.yml +115 -0
- package/errors/silent-failures/reusable-workflow-outputs-undeclared-workflow-level.yml +95 -0
- package/errors/silent-failures/workflow-call-boolean-input-string-coercion.yml +98 -0
- package/errors/triggers/pull-request-target-checkout-base-branch.yml +101 -0
- package/errors/yaml-syntax/composite-action-run-step-missing-shell.yml +93 -0
- package/package.json +1 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
id: caching-artifacts-041
|
|
2
|
+
title: "Fork PR workflows cannot read caches from the parent repository — always cache miss"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- fork
|
|
8
|
+
- pull-request
|
|
9
|
+
- security
|
|
10
|
+
- cache-miss
|
|
11
|
+
- isolation
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'Cache not found for input keys'
|
|
14
|
+
flags: i
|
|
15
|
+
- regex: 'No cache found'
|
|
16
|
+
flags: i
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Cache not found for input keys:"
|
|
19
|
+
- "No cache found"
|
|
20
|
+
root_cause: |
|
|
21
|
+
GitHub Actions intentionally prevents fork PR workflows from reading caches created by
|
|
22
|
+
the parent repository. This is a security measure: a malicious fork could craft a cache
|
|
23
|
+
key matching a parent repo cache, read its contents (which may contain build artifacts,
|
|
24
|
+
installed dependencies, or sensitive intermediate files), and exfiltrate them via the
|
|
25
|
+
fork workflow's logs or artifact uploads.
|
|
26
|
+
|
|
27
|
+
The restriction applies to `pull_request` events triggered by fork contributors:
|
|
28
|
+
- The fork workflow runs in a restricted sandbox with a read-only GITHUB_TOKEN
|
|
29
|
+
- `restore-keys` fallback does NOT traverse to the parent repository's cache namespace
|
|
30
|
+
- Even a perfect cache key match on the parent repo returns a miss in the fork
|
|
31
|
+
- The `actions/cache` step reports "Cache not found" with no indication that security
|
|
32
|
+
isolation is the cause — developers commonly assume misconfigured cache keys
|
|
33
|
+
|
|
34
|
+
Affected scenarios:
|
|
35
|
+
- Open source projects where external contributors submit PRs
|
|
36
|
+
- npm/pip/gem dependency caches that would accelerate fork CI significantly
|
|
37
|
+
- Docker buildx layer caches configured via `actions/cache`
|
|
38
|
+
- Turborepo and Nx remote caches backed by GitHub Actions cache
|
|
39
|
+
fix: |
|
|
40
|
+
Fork PR cache isolation is intentional and cannot be disabled. Mitigations:
|
|
41
|
+
|
|
42
|
+
1. Accept cold builds for fork PRs and optimize speed without cache:
|
|
43
|
+
Use package manager offline flags (npm ci --prefer-offline, pip --no-index),
|
|
44
|
+
pre-built base Docker images, and build sharding to minimize cold-build time.
|
|
45
|
+
|
|
46
|
+
2. Use `pull_request_target` instead of `pull_request` — this event runs in the parent
|
|
47
|
+
repo context with full cache access. SECURITY WARNING: this grants the fork workflow
|
|
48
|
+
write-level GITHUB_TOKEN access. Only safe for workflows that do NOT check out and
|
|
49
|
+
execute code from the PR head branch.
|
|
50
|
+
|
|
51
|
+
3. Pre-warm fork caches via a separate trusted workflow that runs on push to the base
|
|
52
|
+
branch and stores a cache accessible by the fork's restore-keys fallback scope. This
|
|
53
|
+
works because forks CAN read the parent's default branch caches via restore-keys.
|
|
54
|
+
The fork cannot read arbitrary keys, but restore-keys prefix matching against the
|
|
55
|
+
default branch cache does work.
|
|
56
|
+
fix_code:
|
|
57
|
+
- language: yaml
|
|
58
|
+
label: "Accept cold builds — optimize fork PR without relying on cache"
|
|
59
|
+
code: |
|
|
60
|
+
on:
|
|
61
|
+
pull_request:
|
|
62
|
+
|
|
63
|
+
jobs:
|
|
64
|
+
build:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v4
|
|
68
|
+
- uses: actions/setup-node@v4
|
|
69
|
+
with:
|
|
70
|
+
node-version: '20'
|
|
71
|
+
# Cache works for non-fork PRs; fork PRs will be cold builds
|
|
72
|
+
cache: 'npm'
|
|
73
|
+
- name: Install dependencies
|
|
74
|
+
# --prefer-offline minimizes network round-trips on cold builds
|
|
75
|
+
run: npm ci --prefer-offline --no-audit
|
|
76
|
+
|
|
77
|
+
- language: yaml
|
|
78
|
+
label: "Restore-keys fallback from default branch — forks CAN match default-branch caches"
|
|
79
|
+
code: |
|
|
80
|
+
# Caches saved on the default branch (main/master) ARE accessible to fork PRs
|
|
81
|
+
# via restore-keys prefix matching. Pre-warm by saving on each push to main.
|
|
82
|
+
on:
|
|
83
|
+
push:
|
|
84
|
+
branches: [main]
|
|
85
|
+
pull_request:
|
|
86
|
+
|
|
87
|
+
jobs:
|
|
88
|
+
build:
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
steps:
|
|
91
|
+
- uses: actions/checkout@v4
|
|
92
|
+
- uses: actions/cache@v4
|
|
93
|
+
with:
|
|
94
|
+
path: ~/.npm
|
|
95
|
+
# Exact key includes lockfile hash — will miss on forks
|
|
96
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
97
|
+
# Restore-key matches default-branch cache — forks get a partial hit
|
|
98
|
+
restore-keys: |
|
|
99
|
+
${{ runner.os }}-node-
|
|
100
|
+
prevention:
|
|
101
|
+
- "Document that fork contributor CI will be slower due to cache isolation — set expectations"
|
|
102
|
+
- "Use restore-keys with a broad prefix to allow forks to partially match default-branch caches"
|
|
103
|
+
- "Avoid `pull_request_target` with PR head checkout and code execution — this is a high-severity security vulnerability"
|
|
104
|
+
- "Optimize cold-build speed with offline package manager flags and pre-built base images"
|
|
105
|
+
- "Consider caching at the Docker image layer level via a registry rather than GitHub Actions cache"
|
|
106
|
+
docs:
|
|
107
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request"
|
|
108
|
+
label: "pull_request event — fork restrictions — GitHub Docs"
|
|
109
|
+
- url: "https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-pull_request_target"
|
|
110
|
+
label: "Security hardening — pull_request_target risks — GitHub Docs"
|
|
111
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache"
|
|
112
|
+
label: "Cache access restrictions — GitHub Docs"
|
|
113
|
+
- url: "https://github.com/orgs/community/discussions/44783"
|
|
114
|
+
label: "Fork PR workflows cannot access parent repo cache — GitHub Community #44783"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
id: concurrency-timing-035
|
|
2
|
+
title: "cancel-in-progress posts 'cancelled' conclusion — required status check blocks PR merge"
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- cancel-in-progress
|
|
8
|
+
- required-checks
|
|
9
|
+
- branch-protection
|
|
10
|
+
- pull-request
|
|
11
|
+
- status-checks
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'Required status checks have not passed'
|
|
14
|
+
flags: i
|
|
15
|
+
- regex: 'cancelled.*required.*check|required.*check.*cancelled'
|
|
16
|
+
flags: i
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Required status checks have not passed"
|
|
19
|
+
- "Branch protection rule requires status check to pass before merging"
|
|
20
|
+
root_cause: |
|
|
21
|
+
When `cancel-in-progress: true` is configured in a concurrency group and that workflow
|
|
22
|
+
is a required status check on a branch protection rule, a cancelled run posts a
|
|
23
|
+
`cancelled` conclusion to the Checks API. GitHub treats `cancelled` as a non-passing
|
|
24
|
+
conclusion for required status checks — identical to `failure`.
|
|
25
|
+
|
|
26
|
+
The failure cycle:
|
|
27
|
+
1. Push A triggers run A (check posts `in_progress`)
|
|
28
|
+
2. Push B triggers run B; concurrency group cancels run A
|
|
29
|
+
3. Run A posts `cancelled` conclusion — required check is now "failed"
|
|
30
|
+
4. Run B may itself get cancelled by Push C before it completes
|
|
31
|
+
5. PR is permanently stuck: merge button blocked until a run completes with `success`
|
|
32
|
+
|
|
33
|
+
This is especially painful in fast-moving PRs where rapid pushes prevent any run from
|
|
34
|
+
completing. The `cancelled` status on the required check is indistinguishable from a
|
|
35
|
+
genuine test failure in the PR view.
|
|
36
|
+
fix: |
|
|
37
|
+
Option 1 — Use a separate reporter job with `if: always()` that is NOT subject to the
|
|
38
|
+
concurrency group. The main build job can be cancelled; the reporter always runs and
|
|
39
|
+
posts the actual status. Configure branch protection to require the reporter job, not
|
|
40
|
+
the build job.
|
|
41
|
+
|
|
42
|
+
Option 2 — Set concurrency at job level instead of workflow level for the job that
|
|
43
|
+
posts the required status check. The job itself will not be cancelled mid-run; only
|
|
44
|
+
newly queued workflow runs are affected.
|
|
45
|
+
|
|
46
|
+
Option 3 — Accept the pattern and use auto-merge on PRs. Once any non-cancelled run
|
|
47
|
+
succeeds, the merge proceeds automatically without manual unblocking.
|
|
48
|
+
fix_code:
|
|
49
|
+
- language: yaml
|
|
50
|
+
label: "Reporter job pattern — required check never posts 'cancelled'"
|
|
51
|
+
code: |
|
|
52
|
+
concurrency:
|
|
53
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
54
|
+
cancel-in-progress: true
|
|
55
|
+
|
|
56
|
+
jobs:
|
|
57
|
+
build:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v4
|
|
61
|
+
- run: npm ci && npm test
|
|
62
|
+
|
|
63
|
+
# This job is NOT inside the concurrency group — it always runs.
|
|
64
|
+
# Configure branch protection to require 'report-status', not 'build'.
|
|
65
|
+
report-status:
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
needs: [build]
|
|
68
|
+
if: always()
|
|
69
|
+
steps:
|
|
70
|
+
- name: Fail if build did not succeed
|
|
71
|
+
if: needs.build.result != 'success'
|
|
72
|
+
run: exit 1
|
|
73
|
+
prevention:
|
|
74
|
+
- "Test required-check + cancel-in-progress interaction on a draft PR before enabling branch protection"
|
|
75
|
+
- "Configure branch protection to require a dedicated reporter job, not the cancellable build job"
|
|
76
|
+
- "Set concurrency at job level for jobs that post required checks"
|
|
77
|
+
- "Enable auto-merge to reduce manual intervention when cancelled checks momentarily block the PR"
|
|
78
|
+
docs:
|
|
79
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-concurrency"
|
|
80
|
+
label: "Using concurrency — GitHub Docs"
|
|
81
|
+
- url: "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches"
|
|
82
|
+
label: "About protected branches — GitHub Docs"
|
|
83
|
+
- url: "https://github.com/orgs/community/discussions/13015"
|
|
84
|
+
label: "cancel-in-progress fails required status check — GitHub Community #13015"
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
id: known-unsolved-040
|
|
2
|
+
title: "Skipped job outputs always resolve to empty string — downstream consumers receive '' silently"
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- outputs
|
|
7
|
+
- skipped
|
|
8
|
+
- needs
|
|
9
|
+
- conditional-jobs
|
|
10
|
+
- silent-failure
|
|
11
|
+
- limitation
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'needs\.[a-z_-]+\.outputs\.[a-z_-]+'
|
|
14
|
+
flags: i
|
|
15
|
+
error_messages: []
|
|
16
|
+
root_cause: |
|
|
17
|
+
When a job is skipped — because its `if:` condition evaluated to false, or because all
|
|
18
|
+
jobs in its `needs` array were skipped or failed — that job's `outputs` block never
|
|
19
|
+
executes. Any downstream job that reads `needs.<skipped-job>.outputs.<key>` receives an
|
|
20
|
+
empty string `''`.
|
|
21
|
+
|
|
22
|
+
This is completely silent: no workflow annotation, no job failure, no warning in the logs.
|
|
23
|
+
The downstream job receives `''` and continues executing as though the output was
|
|
24
|
+
legitimately set to an empty string.
|
|
25
|
+
|
|
26
|
+
Common failure scenarios:
|
|
27
|
+
- A build job is gated with `if: github.ref == 'refs/heads/main'`; a deploy job reads
|
|
28
|
+
its artifact path output and deploys with an empty path string
|
|
29
|
+
- A matrix job is conditionally skipped on a specific OS; a dependent job reads the
|
|
30
|
+
OS-specific output and silently operates on an empty value
|
|
31
|
+
- A fan-out aggregator reads outputs from multiple conditional jobs; skipped job outputs
|
|
32
|
+
silently produce empty aggregation results
|
|
33
|
+
|
|
34
|
+
There is no way to distinguish "output was deliberately set to empty string" from "job
|
|
35
|
+
was skipped and output was never set". The `needs.<job>.result` value is `'skipped'`
|
|
36
|
+
but `needs.<job>.outputs.*` is always `''` regardless of cause.
|
|
37
|
+
fix: |
|
|
38
|
+
There is no built-in mechanism to throw an error when reading outputs from a skipped job.
|
|
39
|
+
This is a known GitHub Actions limitation — skipped job outputs permanently resolve to ''.
|
|
40
|
+
|
|
41
|
+
Workarounds:
|
|
42
|
+
|
|
43
|
+
1. Guard downstream jobs with a result check:
|
|
44
|
+
Add `if: needs.<job>.result == 'success'` to every job that consumes outputs from a
|
|
45
|
+
conditional job. This prevents the downstream job from running on empty outputs.
|
|
46
|
+
|
|
47
|
+
2. Provide explicit sentinel defaults in the output-setting step:
|
|
48
|
+
Use a shell default expansion in the step that sets the output:
|
|
49
|
+
`echo "path=${ARTIFACT_PATH:-UNSET}" >> $GITHUB_OUTPUT`
|
|
50
|
+
Downstream jobs check for `'UNSET'` to detect the unset case.
|
|
51
|
+
|
|
52
|
+
3. Inline null-coalescing in expressions:
|
|
53
|
+
`${{ needs.build.outputs.path != '' && needs.build.outputs.path || 'default-value' }}`
|
|
54
|
+
fix_code:
|
|
55
|
+
- language: yaml
|
|
56
|
+
label: "Guard downstream job with result check — prevents execution on empty outputs"
|
|
57
|
+
code: |
|
|
58
|
+
jobs:
|
|
59
|
+
build:
|
|
60
|
+
if: github.ref == 'refs/heads/main'
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
outputs:
|
|
63
|
+
artifact-path: ${{ steps.build.outputs.path }}
|
|
64
|
+
steps:
|
|
65
|
+
- id: build
|
|
66
|
+
run: echo "path=dist/" >> $GITHUB_OUTPUT
|
|
67
|
+
|
|
68
|
+
deploy:
|
|
69
|
+
needs: [build]
|
|
70
|
+
# Only run if build actually succeeded — never run if skipped or failed
|
|
71
|
+
if: needs.build.result == 'success'
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
steps:
|
|
74
|
+
- run: echo "Deploying ${{ needs.build.outputs.artifact-path }}"
|
|
75
|
+
|
|
76
|
+
- language: yaml
|
|
77
|
+
label: "Sentinel default — detect skipped output in downstream job"
|
|
78
|
+
code: |
|
|
79
|
+
jobs:
|
|
80
|
+
build:
|
|
81
|
+
if: github.event_name == 'push'
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
outputs:
|
|
84
|
+
version: ${{ steps.ver.outputs.version }}
|
|
85
|
+
steps:
|
|
86
|
+
- id: ver
|
|
87
|
+
run: echo "version=${VERSION:-UNSET}" >> $GITHUB_OUTPUT
|
|
88
|
+
|
|
89
|
+
publish:
|
|
90
|
+
needs: [build]
|
|
91
|
+
runs-on: ubuntu-latest
|
|
92
|
+
steps:
|
|
93
|
+
- name: Abort if build was skipped
|
|
94
|
+
if: needs.build.outputs.version == 'UNSET' || needs.build.outputs.version == ''
|
|
95
|
+
run: |
|
|
96
|
+
echo "Build was skipped or version not set — aborting publish"
|
|
97
|
+
exit 1
|
|
98
|
+
prevention:
|
|
99
|
+
- "Always check `needs.<job>.result == 'success'` before using that job's outputs"
|
|
100
|
+
- "Provide explicit sentinel defaults in output-setting steps for critical values"
|
|
101
|
+
- "Treat empty string outputs from conditional jobs as indicative of skip or failure"
|
|
102
|
+
- "Document which jobs are conditional and which downstream jobs depend on their outputs"
|
|
103
|
+
docs:
|
|
104
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs"
|
|
105
|
+
label: "Passing information between jobs — GitHub Docs"
|
|
106
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/using-conditions-to-control-job-execution"
|
|
107
|
+
label: "Using conditions to control job execution — GitHub Docs"
|
|
108
|
+
- url: "https://github.com/orgs/community/discussions/26704"
|
|
109
|
+
label: "Job outputs from skipped jobs are empty string — GitHub Community #26704"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
id: permissions-auth-043
|
|
2
|
+
title: 'GITHUB_TOKEN cannot dispatch workflows or events in other repositories'
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- GITHUB_TOKEN
|
|
7
|
+
- repository_dispatch
|
|
8
|
+
- workflow_dispatch
|
|
9
|
+
- cross-repo
|
|
10
|
+
- 404
|
|
11
|
+
- permissions
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'Resource not accessible by integration'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: '404.*dispatches|dispatches.*404'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'HttpError: Not Found'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- 'Resource not accessible by integration'
|
|
21
|
+
- 'HttpError: Not Found'
|
|
22
|
+
- '{"message":"Not Found","documentation_url":"https://docs.github.com/rest/repos/repos#create-a-repository-dispatch-event"}'
|
|
23
|
+
- 'RequestError [HttpError]: Not Found'
|
|
24
|
+
root_cause: |
|
|
25
|
+
GITHUB_TOKEN is automatically scoped to the repository where the workflow
|
|
26
|
+
runs. It cannot authenticate to any other repository, regardless of what is
|
|
27
|
+
declared in the workflow's permissions block. The permissions block controls
|
|
28
|
+
only the scopes of the automatically-generated token for the CURRENT repo —
|
|
29
|
+
it has no effect on access to other repositories.
|
|
30
|
+
|
|
31
|
+
Attempts to call the GitHub REST API against a different repository —
|
|
32
|
+
including POST /repos/{owner}/{other-repo}/dispatches (repository_dispatch)
|
|
33
|
+
or POST /repos/{owner}/{other-repo}/actions/workflows/{id}/dispatches
|
|
34
|
+
(workflow_dispatch) — return 404 Not Found. GitHub returns 404 rather than
|
|
35
|
+
403 to avoid leaking information about whether the target repository exists
|
|
36
|
+
or is private.
|
|
37
|
+
|
|
38
|
+
This is a hard authentication boundary enforced by GitHub's token system,
|
|
39
|
+
not a configuration issue. No amount of permissions: adjustments in the
|
|
40
|
+
workflow file can grant the automatic GITHUB_TOKEN cross-repository access.
|
|
41
|
+
|
|
42
|
+
Note: this is distinct from the GITHUB_TOKEN loopback limitation (where
|
|
43
|
+
GITHUB_TOKEN cannot trigger new workflow runs in the SAME repository). This
|
|
44
|
+
error applies to any cross-repository API call.
|
|
45
|
+
fix: |
|
|
46
|
+
Replace GITHUB_TOKEN with a credential that has access to the target
|
|
47
|
+
repository: a Personal Access Token (PAT) with repo scope, or a GitHub App
|
|
48
|
+
installation token scoped to both repositories. Store the credential as an
|
|
49
|
+
encrypted repository or organization secret.
|
|
50
|
+
|
|
51
|
+
GitHub Apps with installation tokens are the recommended approach for
|
|
52
|
+
production cross-repo automation — they provide fine-grained permissions,
|
|
53
|
+
do not depend on a specific user account, and tokens are automatically
|
|
54
|
+
rotated.
|
|
55
|
+
fix_code:
|
|
56
|
+
- language: yaml
|
|
57
|
+
label: 'Use a PAT secret for cross-repo repository_dispatch'
|
|
58
|
+
code: |
|
|
59
|
+
jobs:
|
|
60
|
+
trigger-downstream:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
steps:
|
|
63
|
+
- name: Dispatch event to other repository
|
|
64
|
+
uses: actions/github-script@v7
|
|
65
|
+
with:
|
|
66
|
+
github-token: ${{ secrets.CROSS_REPO_PAT }}
|
|
67
|
+
script: |
|
|
68
|
+
await github.rest.repos.createDispatchEvent({
|
|
69
|
+
owner: 'my-org',
|
|
70
|
+
repo: 'other-repo',
|
|
71
|
+
event_type: 'build-triggered',
|
|
72
|
+
client_payload: {
|
|
73
|
+
ref: context.ref,
|
|
74
|
+
sha: context.sha
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
- language: yaml
|
|
78
|
+
label: 'Use a GitHub App installation token for cross-repo workflow dispatch (recommended)'
|
|
79
|
+
code: |
|
|
80
|
+
jobs:
|
|
81
|
+
trigger-downstream:
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- name: Generate GitHub App installation token
|
|
85
|
+
id: app-token
|
|
86
|
+
uses: actions/create-github-app-token@v1
|
|
87
|
+
with:
|
|
88
|
+
app-id: ${{ vars.APP_ID }}
|
|
89
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
90
|
+
owner: my-org
|
|
91
|
+
repositories: other-repo
|
|
92
|
+
|
|
93
|
+
- name: Dispatch workflow in other repository
|
|
94
|
+
uses: actions/github-script@v7
|
|
95
|
+
with:
|
|
96
|
+
github-token: ${{ steps.app-token.outputs.token }}
|
|
97
|
+
script: |
|
|
98
|
+
await github.rest.actions.createWorkflowDispatch({
|
|
99
|
+
owner: 'my-org',
|
|
100
|
+
repo: 'other-repo',
|
|
101
|
+
workflow_id: 'deploy.yml',
|
|
102
|
+
ref: 'main'
|
|
103
|
+
});
|
|
104
|
+
prevention:
|
|
105
|
+
- 'Never use ${{ secrets.GITHUB_TOKEN }} in API calls targeting other repositories — it will always 404'
|
|
106
|
+
- 'Prefer GitHub Apps with installation tokens over PATs for cross-repo automation — scoped, auto-rotating, and not tied to a user account'
|
|
107
|
+
- 'Store cross-repo PATs as organization secrets so they are reusable across multiple source repositories without per-repo duplication'
|
|
108
|
+
- 'When debugging 404 on dispatch endpoints, first check whether the token is GITHUB_TOKEN vs a PAT — this is the most common cause'
|
|
109
|
+
docs:
|
|
110
|
+
- url: 'https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token'
|
|
111
|
+
label: 'GitHub Docs: Permissions for the GITHUB_TOKEN'
|
|
112
|
+
- url: 'https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow'
|
|
113
|
+
label: 'GitHub Docs: Authenticating with a GitHub App in GitHub Actions'
|
|
114
|
+
- url: 'https://github.com/orgs/community/discussions/26724'
|
|
115
|
+
label: 'GitHub Community: GITHUB_TOKEN cannot dispatch to another repository'
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
id: silent-failures-060
|
|
2
|
+
title: 'Reusable workflow outputs not declared at on.workflow_call.outputs are silently empty in callers'
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- reusable-workflow
|
|
7
|
+
- workflow_call
|
|
8
|
+
- outputs
|
|
9
|
+
- empty-string
|
|
10
|
+
- silent-failure
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'needs\.[a-z][a-z0-9_-]*\.outputs\.[a-z][a-z0-9_-]*'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
error_messages:
|
|
15
|
+
- '(no error emitted — callers receive empty strings silently)'
|
|
16
|
+
- 'Error: Input required and not supplied'
|
|
17
|
+
root_cause: |
|
|
18
|
+
Reusable workflows require a two-level output declaration. Developers
|
|
19
|
+
correctly declare job-level outputs under jobs.<job-id>.outputs, but omit
|
|
20
|
+
the second required declaration: the workflow-level outputs block under
|
|
21
|
+
on.workflow_call.outputs.
|
|
22
|
+
|
|
23
|
+
Without the workflow-level outputs declaration, any caller that references
|
|
24
|
+
${{ needs.<call-job>.outputs.<name> }} receives an empty string. GitHub
|
|
25
|
+
Actions does not emit any error, warning, or annotation — downstream steps
|
|
26
|
+
silently receive empty values, producing wrong behavior or spurious failures
|
|
27
|
+
(e.g., artifact path is empty, deployment target is blank).
|
|
28
|
+
|
|
29
|
+
The root cause is that GitHub's reusable workflow output model requires two
|
|
30
|
+
separate declarations:
|
|
31
|
+
1. job.outputs: maps a step output to a job-scoped output
|
|
32
|
+
2. on.workflow_call.outputs: hoists a job output up to the workflow level
|
|
33
|
+
so external callers can reference it
|
|
34
|
+
|
|
35
|
+
Omitting level 2 is invisible — the job output exists internally but is
|
|
36
|
+
never exposed to the calling workflow.
|
|
37
|
+
fix: |
|
|
38
|
+
Add an outputs block directly under on.workflow_call that maps each desired
|
|
39
|
+
workflow output name to ${{ jobs.<job-id>.outputs.<output-name> }}. This is
|
|
40
|
+
in addition to (not a replacement for) the job-level outputs block.
|
|
41
|
+
fix_code:
|
|
42
|
+
- language: yaml
|
|
43
|
+
label: 'Reusable workflow: declare outputs at both job level and on.workflow_call level'
|
|
44
|
+
code: |
|
|
45
|
+
on:
|
|
46
|
+
workflow_call:
|
|
47
|
+
outputs:
|
|
48
|
+
build-version:
|
|
49
|
+
description: 'The semantic version produced by this workflow'
|
|
50
|
+
value: ${{ jobs.build.outputs.build-version }}
|
|
51
|
+
artifact-path:
|
|
52
|
+
description: 'Path to the uploaded artifact'
|
|
53
|
+
value: ${{ jobs.build.outputs.artifact-path }}
|
|
54
|
+
|
|
55
|
+
jobs:
|
|
56
|
+
build:
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
outputs:
|
|
59
|
+
build-version: ${{ steps.version.outputs.version }}
|
|
60
|
+
artifact-path: ${{ steps.upload.outputs.artifact-path }}
|
|
61
|
+
steps:
|
|
62
|
+
- id: version
|
|
63
|
+
run: echo "version=1.2.3" >> "$GITHUB_OUTPUT"
|
|
64
|
+
shell: bash
|
|
65
|
+
- id: upload
|
|
66
|
+
uses: actions/upload-artifact@v4
|
|
67
|
+
with:
|
|
68
|
+
name: my-artifact
|
|
69
|
+
path: dist/
|
|
70
|
+
- language: yaml
|
|
71
|
+
label: 'Calling workflow: access reusable workflow outputs via needs context'
|
|
72
|
+
code: |
|
|
73
|
+
jobs:
|
|
74
|
+
call-build:
|
|
75
|
+
uses: ./.github/workflows/build.yml
|
|
76
|
+
|
|
77
|
+
deploy:
|
|
78
|
+
needs: call-build
|
|
79
|
+
runs-on: ubuntu-latest
|
|
80
|
+
steps:
|
|
81
|
+
- name: Use build outputs
|
|
82
|
+
run: |
|
|
83
|
+
echo "Version: ${{ needs.call-build.outputs.build-version }}"
|
|
84
|
+
echo "Artifact: ${{ needs.call-build.outputs.artifact-path }}"
|
|
85
|
+
shell: bash
|
|
86
|
+
prevention:
|
|
87
|
+
- 'Always declare outputs under on.workflow_call.outputs in addition to any job-level outputs blocks — they are two distinct YAML nodes'
|
|
88
|
+
- 'Immediately echo needs.<call-job>.outputs.* in the caller after wiring up to verify outputs are populated during development'
|
|
89
|
+
- 'Use actionlint locally or in CI to catch missing workflow-level output declarations before they ship'
|
|
90
|
+
- 'Treat the two-level declaration as a checklist: job outputs block + workflow_call outputs block'
|
|
91
|
+
docs:
|
|
92
|
+
- url: 'https://docs.github.com/en/actions/sharing-automations/reusing-workflows#using-outputs-from-a-reusable-workflow'
|
|
93
|
+
label: 'GitHub Docs: Using outputs from a reusable workflow'
|
|
94
|
+
- url: 'https://github.com/orgs/community/discussions/17245'
|
|
95
|
+
label: 'GitHub Community: Outputs from reusable workflow missing in caller'
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
id: silent-failures-061
|
|
2
|
+
title: "`workflow_call` boolean inputs evaluate as strings — `if: inputs.flag` is truthy even when caller passes `false`"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- workflow-call
|
|
7
|
+
- reusable-workflow
|
|
8
|
+
- boolean
|
|
9
|
+
- inputs
|
|
10
|
+
- if-condition
|
|
11
|
+
- type-coercion
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'inputs\.[a-z_-]+\s*(?:==\s*true|==\s*false)?'
|
|
14
|
+
flags: i
|
|
15
|
+
error_messages: []
|
|
16
|
+
root_cause: |
|
|
17
|
+
In `on.workflow_call` inputs, declaring `type: boolean` describes the intended type of
|
|
18
|
+
the input — it does NOT cause the value to be passed as a JSON boolean. At runtime,
|
|
19
|
+
inside the called workflow, `${{ inputs.my_flag }}` always evaluates to the string
|
|
20
|
+
`'true'` or `'false'`.
|
|
21
|
+
|
|
22
|
+
Because non-empty strings are truthy in GitHub Actions expression evaluation:
|
|
23
|
+
- `if: inputs.my_flag` evaluates the string `'false'` as TRUTHY (non-empty string = true)
|
|
24
|
+
- Steps and jobs conditioned on `if: inputs.my_flag` run even when the caller passes false
|
|
25
|
+
|
|
26
|
+
This is distinct from the `workflow_dispatch` boolean input coercion issue (silent-failures-053)
|
|
27
|
+
in that it affects all callers of a reusable workflow simultaneously. A single bug in a
|
|
28
|
+
widely-used reusable workflow can cause unintended deploys, notifications, or expensive
|
|
29
|
+
steps to run across every calling workflow.
|
|
30
|
+
|
|
31
|
+
Example of the failure:
|
|
32
|
+
Caller passes `deploy: false` intending to skip deployment.
|
|
33
|
+
Reusable workflow has `if: inputs.deploy` on its deploy job.
|
|
34
|
+
The deploy job runs because the string 'false' is non-empty and therefore truthy.
|
|
35
|
+
fix: |
|
|
36
|
+
Always compare boolean inputs against the boolean literal `true` using `==`, or use
|
|
37
|
+
`fromJSON()` to parse the string to an actual boolean value:
|
|
38
|
+
|
|
39
|
+
CORRECT patterns:
|
|
40
|
+
- `if: inputs.my_flag == true` — Actions coerces the string 'true' to boolean true for == comparison
|
|
41
|
+
- `if: fromJSON(inputs.my_flag)` — parses string 'true'/'false' to actual boolean
|
|
42
|
+
|
|
43
|
+
WRONG patterns:
|
|
44
|
+
- `if: inputs.my_flag` — always true for non-empty strings ('false' is truthy)
|
|
45
|
+
- `if: inputs.my_flag == 'true'` — works but fragile; fails if value is boolean true not string
|
|
46
|
+
fix_code:
|
|
47
|
+
- language: yaml
|
|
48
|
+
label: "Correct boolean input handling in reusable workflow"
|
|
49
|
+
code: |
|
|
50
|
+
# reusable.yml
|
|
51
|
+
on:
|
|
52
|
+
workflow_call:
|
|
53
|
+
inputs:
|
|
54
|
+
deploy:
|
|
55
|
+
type: boolean
|
|
56
|
+
default: false
|
|
57
|
+
notify:
|
|
58
|
+
type: boolean
|
|
59
|
+
default: true
|
|
60
|
+
|
|
61
|
+
jobs:
|
|
62
|
+
deploy:
|
|
63
|
+
# Correct: == true comparison handles string-to-boolean coercion
|
|
64
|
+
if: inputs.deploy == true
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- run: echo "Deploying..."
|
|
68
|
+
|
|
69
|
+
notify:
|
|
70
|
+
# Alternative: fromJSON() parses string to actual boolean
|
|
71
|
+
if: fromJSON(inputs.notify)
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
steps:
|
|
74
|
+
- run: echo "Notifying..."
|
|
75
|
+
|
|
76
|
+
- language: yaml
|
|
77
|
+
label: "Caller side — no change needed; fix is in the reusable workflow"
|
|
78
|
+
code: |
|
|
79
|
+
# caller.yml — calling the reusable workflow
|
|
80
|
+
jobs:
|
|
81
|
+
call-reusable:
|
|
82
|
+
uses: ./.github/workflows/reusable.yml
|
|
83
|
+
with:
|
|
84
|
+
deploy: false # This passes as string 'false' — fix is in reusable.yml
|
|
85
|
+
notify: true
|
|
86
|
+
prevention:
|
|
87
|
+
- "Audit all reusable workflows for bare `if: inputs.<name>` patterns where the input is type boolean"
|
|
88
|
+
- "Use `if: inputs.<flag> == true` consistently — never `if: inputs.<flag>` for boolean inputs"
|
|
89
|
+
- "Add a comment in reusable workflows noting that boolean inputs are strings at runtime"
|
|
90
|
+
- "Test callers with both `true` and `false` values and verify both branches execute as expected"
|
|
91
|
+
- "Consider wrapping boolean inputs in `fromJSON()` at the point of use for clarity"
|
|
92
|
+
docs:
|
|
93
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#literals"
|
|
94
|
+
label: "Expressions — literals and type coercion — GitHub Docs"
|
|
95
|
+
- url: "https://docs.github.com/en/actions/sharing-automations/reusing-workflows#using-inputs-and-secrets-in-a-reusable-workflow"
|
|
96
|
+
label: "Reusable workflows — using inputs — GitHub Docs"
|
|
97
|
+
- url: "https://github.com/orgs/community/discussions/45843"
|
|
98
|
+
label: "workflow_call boolean inputs evaluated as truthy strings — GitHub Community #45843"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
id: triggers-044
|
|
2
|
+
title: 'pull_request_target checks out base branch by default — PR head requires explicit ref'
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- pull_request_target
|
|
7
|
+
- checkout
|
|
8
|
+
- ref
|
|
9
|
+
- base-branch
|
|
10
|
+
- security
|
|
11
|
+
- fork
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'on:\s*\n\s*pull_request_target'
|
|
14
|
+
flags: 'im'
|
|
15
|
+
- regex: 'uses:\s*actions/checkout.*\n(?!.*ref:)'
|
|
16
|
+
flags: 'im'
|
|
17
|
+
error_messages:
|
|
18
|
+
- '(no error — workflow runs against base branch code, not PR changes)'
|
|
19
|
+
root_cause: |
|
|
20
|
+
pull_request_target runs in the context of the BASE repository rather than
|
|
21
|
+
the contributor's fork. This is an intentional security boundary: because
|
|
22
|
+
the workflow has access to repository secrets and typically runs with write
|
|
23
|
+
permissions, GitHub executes it against trusted code on the base branch
|
|
24
|
+
rather than potentially untrusted PR code.
|
|
25
|
+
|
|
26
|
+
As a direct consequence, actions/checkout without an explicit ref: parameter
|
|
27
|
+
checks out the TARGET BRANCH (e.g., main), not the contributor's changes.
|
|
28
|
+
The workflow appears to run successfully — tests pass, linters pass — but
|
|
29
|
+
only because they are testing the existing base branch code. The PR's actual
|
|
30
|
+
changes are completely ignored. CI gives a false green for every PR
|
|
31
|
+
regardless of what was submitted.
|
|
32
|
+
|
|
33
|
+
This is among the most common misuses of pull_request_target, appearing
|
|
34
|
+
frequently in workflows that were migrated from pull_request to gain write
|
|
35
|
+
access (e.g., to post PR comments or add labels) without understanding the
|
|
36
|
+
checkout difference.
|
|
37
|
+
fix: |
|
|
38
|
+
If the goal is to build and test PR code, use on: pull_request instead of
|
|
39
|
+
pull_request_target. pull_request runs in the fork context with limited
|
|
40
|
+
permissions, which is the correct and safe choice for CI testing.
|
|
41
|
+
|
|
42
|
+
Reserve pull_request_target only for operations that specifically need write
|
|
43
|
+
permissions or secrets AND that do NOT execute untrusted PR code (e.g.,
|
|
44
|
+
adding labels based on event metadata, posting a comment using a stored
|
|
45
|
+
token). If pull_request_target must check out PR code, consult the GitHub
|
|
46
|
+
Security Lab advisory on pwn requests first — the attack surface is
|
|
47
|
+
significant.
|
|
48
|
+
fix_code:
|
|
49
|
+
- language: yaml
|
|
50
|
+
label: 'Preferred: use pull_request for code testing workflows (runs against PR head safely)'
|
|
51
|
+
code: |
|
|
52
|
+
on:
|
|
53
|
+
pull_request:
|
|
54
|
+
branches: [main]
|
|
55
|
+
|
|
56
|
+
jobs:
|
|
57
|
+
test:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
permissions:
|
|
60
|
+
contents: read
|
|
61
|
+
steps:
|
|
62
|
+
- uses: actions/checkout@v4
|
|
63
|
+
# No ref needed — pull_request automatically checks out the merge commit
|
|
64
|
+
- run: npm ci && npm test
|
|
65
|
+
shell: bash
|
|
66
|
+
- language: yaml
|
|
67
|
+
label: 'Safe pull_request_target: metadata-only operations — no checkout of PR code'
|
|
68
|
+
code: |
|
|
69
|
+
on:
|
|
70
|
+
pull_request_target:
|
|
71
|
+
types: [opened, synchronize, labeled]
|
|
72
|
+
|
|
73
|
+
jobs:
|
|
74
|
+
label:
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
permissions:
|
|
77
|
+
pull-requests: write
|
|
78
|
+
steps:
|
|
79
|
+
# Safe: uses only github.event data, does NOT check out PR code
|
|
80
|
+
- name: Add triage label
|
|
81
|
+
uses: actions/github-script@v7
|
|
82
|
+
with:
|
|
83
|
+
script: |
|
|
84
|
+
await github.rest.issues.addLabels({
|
|
85
|
+
owner: context.repo.owner,
|
|
86
|
+
repo: context.repo.repo,
|
|
87
|
+
issue_number: context.payload.pull_request.number,
|
|
88
|
+
labels: ['needs-review']
|
|
89
|
+
});
|
|
90
|
+
prevention:
|
|
91
|
+
- 'Default to on: pull_request for all CI testing workflows — it checks out PR code correctly and runs with limited permissions'
|
|
92
|
+
- 'Only use pull_request_target when write permissions or secrets are required AND untrusted PR code is never executed'
|
|
93
|
+
- 'If pull_request_target is in a workflow, verify there is NO actions/checkout step (or that it explicitly uses a trusted ref)'
|
|
94
|
+
- 'Review the GitHub Security Lab pwn requests advisory before combining pull_request_target with any form of code execution from the PR'
|
|
95
|
+
docs:
|
|
96
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request_target'
|
|
97
|
+
label: 'GitHub Docs: pull_request_target event'
|
|
98
|
+
- url: 'https://securitylab.github.com/research/github-actions-preventing-pwn-requests/'
|
|
99
|
+
label: 'GitHub Security Lab: Preventing pwn requests in GitHub Actions'
|
|
100
|
+
- url: 'https://github.com/orgs/community/discussions/15669'
|
|
101
|
+
label: 'GitHub Community: pull_request_target and checkout behavior'
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
id: yaml-syntax-042
|
|
2
|
+
title: 'Composite action run steps require explicit shell property — no default shell is applied'
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- composite-action
|
|
7
|
+
- shell
|
|
8
|
+
- run-step
|
|
9
|
+
- action-yaml
|
|
10
|
+
- validation-error
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Required property is missing: shell'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'The `shell` property must be set'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'shell is required for `run` step in composite action'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
error_messages:
|
|
19
|
+
- 'Required property is missing: shell'
|
|
20
|
+
- 'The `shell` property must be set for step `run` in composite actions'
|
|
21
|
+
- "Error: Unexpected value ''"
|
|
22
|
+
root_cause: |
|
|
23
|
+
Regular workflow jobs default to bash on Linux and macOS, and pwsh on
|
|
24
|
+
Windows, when the shell property is omitted from a run step. Composite
|
|
25
|
+
actions (using: composite) do not inherit any default shell — every run
|
|
26
|
+
step must explicitly declare a shell value such as bash, pwsh, sh, or cmd.
|
|
27
|
+
|
|
28
|
+
This inconsistency frequently catches developers who copy run steps from a
|
|
29
|
+
workflow job into a composite action's action.yml without adding shell: bash.
|
|
30
|
+
The error surfaces immediately on the first run and is caught by the runner's
|
|
31
|
+
action schema validation, but the message can be confusing because shell is
|
|
32
|
+
a property developers rarely need to specify in regular workflows.
|
|
33
|
+
|
|
34
|
+
The GitHub Actions runner enforces this requirement because composite actions
|
|
35
|
+
must be portable across multiple callers (Linux, macOS, Windows) and the
|
|
36
|
+
runner has no basis to assume which shell is appropriate without the caller
|
|
37
|
+
specifying one.
|
|
38
|
+
fix: |
|
|
39
|
+
Add shell: bash (or shell: pwsh on Windows-targeted steps, shell: sh for
|
|
40
|
+
minimal Alpine images) to every run step in the composite action's
|
|
41
|
+
action.yml. If the action must support multiple operating systems, use a
|
|
42
|
+
conditional expression to select the shell based on runner.os.
|
|
43
|
+
fix_code:
|
|
44
|
+
- language: yaml
|
|
45
|
+
label: 'action.yml: add explicit shell to every run step in a composite action'
|
|
46
|
+
code: |
|
|
47
|
+
name: My Composite Action
|
|
48
|
+
description: 'Example composite action with correct shell declarations'
|
|
49
|
+
runs:
|
|
50
|
+
using: composite
|
|
51
|
+
steps:
|
|
52
|
+
- name: Install dependencies
|
|
53
|
+
run: npm ci
|
|
54
|
+
shell: bash
|
|
55
|
+
|
|
56
|
+
- name: Build
|
|
57
|
+
run: npm run build
|
|
58
|
+
shell: bash
|
|
59
|
+
|
|
60
|
+
- name: Run tests
|
|
61
|
+
run: npm test
|
|
62
|
+
shell: bash
|
|
63
|
+
- language: yaml
|
|
64
|
+
label: 'Cross-platform composite action: select shell via runner.os expression'
|
|
65
|
+
code: |
|
|
66
|
+
runs:
|
|
67
|
+
using: composite
|
|
68
|
+
steps:
|
|
69
|
+
- name: Platform-aware step
|
|
70
|
+
run: echo "Running on ${{ runner.os }}"
|
|
71
|
+
shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
|
|
72
|
+
|
|
73
|
+
- name: Windows-only step
|
|
74
|
+
if: runner.os == 'Windows'
|
|
75
|
+
run: Write-Host "Windows step"
|
|
76
|
+
shell: pwsh
|
|
77
|
+
|
|
78
|
+
- name: Unix step
|
|
79
|
+
if: runner.os != 'Windows'
|
|
80
|
+
run: echo "Unix step"
|
|
81
|
+
shell: bash
|
|
82
|
+
prevention:
|
|
83
|
+
- 'Add shell: bash to every run step when authoring or editing a composite action — treat it as required boilerplate'
|
|
84
|
+
- 'Use actionlint or the VS Code GitHub Actions extension to surface missing shell properties before committing'
|
|
85
|
+
- 'Keep a composite action template or snippet that includes shell: bash by default in every run step'
|
|
86
|
+
- 'When converting a workflow job to a composite action, do a search for all run: blocks and add shell: to each'
|
|
87
|
+
docs:
|
|
88
|
+
- url: 'https://docs.github.com/en/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#runsusing-for-composite-actions'
|
|
89
|
+
label: 'GitHub Docs: Metadata syntax for composite actions'
|
|
90
|
+
- url: 'https://github.com/actions/runner/issues/835'
|
|
91
|
+
label: 'actions/runner #835: Composite actions should have a default shell'
|
|
92
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell'
|
|
93
|
+
label: 'GitHub Docs: Workflow syntax — steps.shell'
|
package/package.json
CHANGED