@htekdev/actions-debugger 1.0.14 → 1.0.15
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/dist/db/search.js +3 -1
- package/dist/db/search.js.map +1 -1
- package/dist/tools/suggest-fix.d.ts.map +1 -1
- package/dist/tools/suggest-fix.js +5 -1
- package/dist/tools/suggest-fix.js.map +1 -1
- package/errors/caching-artifacts/cache-key-too-long.yml +93 -0
- package/errors/caching-artifacts/cache-path-not-exist-skipped.yml +152 -0
- package/errors/caching-artifacts/docker-buildx-gha-cache-capacity.yml +107 -0
- package/errors/caching-artifacts/setup-ruby-bundler-ephemeral-workdir-cache-miss.yml +147 -0
- package/errors/caching-artifacts/upload-artifact-v3-retirement-blocked.yml +123 -0
- package/errors/concurrency-timing/always-cleanup-5min-forced-kill.yml +140 -0
- package/errors/concurrency-timing/concurrency-group-env-context-undefined.yml +99 -0
- package/errors/concurrency-timing/required-check-pending-path-filter-skip.yml +160 -0
- package/errors/concurrency-timing/wait-timer-cancel-in-progress-starvation.yml +125 -0
- package/errors/known-unsolved/composite-action-step-timeout-minutes-ignored.yml +146 -0
- package/errors/known-unsolved/reusable-workflow-no-composite-action-call.yml +116 -0
- package/errors/known-unsolved/schedule-trigger-default-branch-only.yml +113 -0
- package/errors/known-unsolved/secrets-not-allowed-in-if-conditions.yml +149 -0
- package/errors/permissions-auth/dependabot-pr-secrets-unavailable.yml +133 -0
- package/errors/permissions-auth/fine-grained-pat-deployment-write-required.yml +146 -0
- package/errors/permissions-auth/github-app-installation-token-new-format.yml +124 -0
- package/errors/permissions-auth/github-packages-read-requires-packages-permission.yml +128 -0
- package/errors/permissions-auth/oidc-id-token-write-permission-missing.yml +169 -0
- package/errors/permissions-auth/permissions-empty-block-removes-contents-read.yml +97 -0
- package/errors/permissions-auth/reusable-workflow-permissions-not-inherited.yml +114 -0
- package/errors/runner-environment/checkout-windows-ebusy-lock.yml +124 -0
- package/errors/runner-environment/deprecated-action-version-auto-rejected.yml +89 -0
- package/errors/runner-environment/github-hosted-runner-disk-space-full.yml +85 -0
- package/errors/runner-environment/github-path-same-step-not-found.yml +114 -0
- package/errors/runner-environment/github-script-v6-octokit-rest-actions-not-function.yml +87 -0
- package/errors/runner-environment/macos-15-mono-nuget-removed.yml +151 -0
- package/errors/runner-environment/macos-15-xcode-simulator-sdk-policy.yml +141 -0
- package/errors/runner-environment/runner-oom-exit-code-137.yml +117 -0
- package/errors/runner-environment/setup-go-go123-telemetry-cache-failure.yml +92 -0
- package/errors/runner-environment/setup-java-distribution-required.yml +108 -0
- package/errors/runner-environment/windows-latest-d-drive-removed.yml +104 -0
- package/errors/runner-environment/windows-vs2026-cuda-host-compiler-unsupported.yml +145 -0
- package/errors/silent-failures/event-commits-empty-on-workflow-dispatch.yml +110 -0
- package/errors/silent-failures/fetch-tags-depth-one-silent-no-op.yml +77 -0
- package/errors/silent-failures/github-env-multiline-value-truncated.yml +127 -0
- package/errors/silent-failures/github-sha-pr-merge-commit-not-head.yml +150 -0
- package/errors/silent-failures/job-output-masked-as-secret-empty.yml +147 -0
- package/errors/silent-failures/upload-artifact-permissions-stripped.yml +98 -0
- package/errors/triggers/pull-request-branches-filter-matches-base-not-head.yml +140 -0
- package/errors/triggers/push-event-fires-on-branch-delete.yml +129 -0
- package/errors/triggers/push-first-commit-before-sha-zeros.yml +160 -0
- package/errors/yaml-syntax/fromjson-empty-string-crash.yml +99 -0
- package/errors/yaml-syntax/if-bang-negation-yaml-tag.yml +145 -0
- package/errors/yaml-syntax/local-action-path-always-top-level.yml +142 -0
- package/package.json +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
id: silent-failures-021
|
|
2
|
+
title: "GITHUB_SHA on pull_request Is the Synthetic Merge Commit — Not the PR Head Commit"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- pull_request
|
|
7
|
+
- github-sha
|
|
8
|
+
- GITHUB_SHA
|
|
9
|
+
- merge-commit
|
|
10
|
+
- head-sha
|
|
11
|
+
- docker-tag
|
|
12
|
+
- commit-lookup
|
|
13
|
+
- git-show
|
|
14
|
+
patterns:
|
|
15
|
+
- regex: "github\\.sha"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "GITHUB_SHA"
|
|
18
|
+
flags: ""
|
|
19
|
+
- regex: "git show \\$\\{\\{\\s*github\\.sha\\s*\\}\\}"
|
|
20
|
+
flags: "i"
|
|
21
|
+
- regex: "commit.*not found|unknown revision or path not in the working tree"
|
|
22
|
+
flags: "i"
|
|
23
|
+
error_messages:
|
|
24
|
+
- "fatal: ambiguous argument 'abc1234ef56': unknown revision or path not in the working tree"
|
|
25
|
+
- "Error: An object named 'abc1234' is not in the repository"
|
|
26
|
+
- "error: pathspec 'abc1234ef56' did not match any file(s) known to git"
|
|
27
|
+
- "commit not found: abc1234ef5678"
|
|
28
|
+
root_cause: |
|
|
29
|
+
When a workflow is triggered by the `pull_request` (or `pull_request_target`) event,
|
|
30
|
+
`github.sha` and `GITHUB_SHA` do NOT contain the SHA of the PR author's latest commit.
|
|
31
|
+
Instead, they contain the SHA of a **synthetic merge commit** — a prospective merge of
|
|
32
|
+
the PR head branch into the target branch, created by GitHub on the fly as
|
|
33
|
+
`refs/pull/<number>/merge`.
|
|
34
|
+
|
|
35
|
+
This merge commit:
|
|
36
|
+
- Exists only as a special GitHub ref, not as a real commit in the repo's history
|
|
37
|
+
- Is NOT returned by `git log` on the PR branch
|
|
38
|
+
- Cannot be resolved by `git show <sha>` after a shallow fetch-depth:1 checkout
|
|
39
|
+
- Has a different SHA than the PR author's actual commit
|
|
40
|
+
|
|
41
|
+
This silently breaks common patterns:
|
|
42
|
+
- Docker image tagging: `docker build -t myimage:${{ github.sha }}` then looking up
|
|
43
|
+
that SHA in CI/CD systems finds nothing in the actual branch history
|
|
44
|
+
- Commit status APIs: posting status to `github.sha` may fail or post to an
|
|
45
|
+
unexpected ref
|
|
46
|
+
- Manual `git log --pretty=format:'%H' | grep ${{ github.sha }}` returns nothing
|
|
47
|
+
- `git show ${{ github.sha }}` after shallow clone fails with "unknown revision"
|
|
48
|
+
|
|
49
|
+
The correct variable to get the PR author's head commit SHA is:
|
|
50
|
+
`github.event.pull_request.head.sha`
|
|
51
|
+
|
|
52
|
+
For `workflow_run` events, use:
|
|
53
|
+
`github.event.workflow_run.head_sha`
|
|
54
|
+
|
|
55
|
+
This behavior is documented in the GitHub environment variables reference but not
|
|
56
|
+
prominently called out, leading to widespread confusion.
|
|
57
|
+
Source: github/docs#15302 — "GITHUB_SHA on PRs refers to the merge commit"
|
|
58
|
+
Source: github/docs#17767 — "github.sha description is misleading for pull_request"
|
|
59
|
+
Source: github/docs#30093 — equivalent of github.sha for other events
|
|
60
|
+
fix: |
|
|
61
|
+
Replace `github.sha` with `github.event.pull_request.head.sha` whenever you need
|
|
62
|
+
the actual PR author's commit SHA in a `pull_request`-triggered workflow.
|
|
63
|
+
|
|
64
|
+
For `workflow_run`, use `github.event.workflow_run.head_sha`.
|
|
65
|
+
For `push` events, `github.sha` is correct (it IS the pushed commit's SHA).
|
|
66
|
+
|
|
67
|
+
If your workflow must be compatible with multiple event types, add a step to compute
|
|
68
|
+
the correct SHA once and expose it via `GITHUB_OUTPUT`:
|
|
69
|
+
```
|
|
70
|
+
COMMIT_SHA=${{ github.event.pull_request.head.sha || github.sha }}
|
|
71
|
+
```
|
|
72
|
+
Note: The `||` expression operator works in GitHub Actions expressions —
|
|
73
|
+
`github.event.pull_request.head.sha` is empty for non-PR events, so `github.sha`
|
|
74
|
+
becomes the fallback.
|
|
75
|
+
fix_code:
|
|
76
|
+
- language: yaml
|
|
77
|
+
label: "Fix: use github.event.pull_request.head.sha for PR head commit"
|
|
78
|
+
code: |
|
|
79
|
+
on: pull_request
|
|
80
|
+
|
|
81
|
+
jobs:
|
|
82
|
+
build:
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/checkout@v4
|
|
86
|
+
|
|
87
|
+
# ❌ WRONG: github.sha is the merge commit SHA, not the PR head commit
|
|
88
|
+
- name: Tag Docker image (wrong SHA)
|
|
89
|
+
run: docker build -t myimage:${{ github.sha }} .
|
|
90
|
+
|
|
91
|
+
# ✅ CORRECT: use the PR head commit SHA
|
|
92
|
+
- name: Tag Docker image (correct SHA)
|
|
93
|
+
run: docker build -t myimage:${{ github.event.pull_request.head.sha }} .
|
|
94
|
+
|
|
95
|
+
- language: yaml
|
|
96
|
+
label: "Cross-event SHA: works for both push and pull_request"
|
|
97
|
+
code: |
|
|
98
|
+
on:
|
|
99
|
+
push:
|
|
100
|
+
pull_request:
|
|
101
|
+
|
|
102
|
+
jobs:
|
|
103
|
+
build:
|
|
104
|
+
runs-on: ubuntu-latest
|
|
105
|
+
steps:
|
|
106
|
+
- uses: actions/checkout@v4
|
|
107
|
+
|
|
108
|
+
- name: Resolve correct commit SHA
|
|
109
|
+
id: sha
|
|
110
|
+
run: |
|
|
111
|
+
# For pull_request: use head.sha; for push: fall back to github.sha
|
|
112
|
+
COMMIT_SHA="${{ github.event.pull_request.head.sha || github.sha }}"
|
|
113
|
+
echo "commit_sha=$COMMIT_SHA" >> "$GITHUB_OUTPUT"
|
|
114
|
+
|
|
115
|
+
- name: Use resolved SHA
|
|
116
|
+
run: docker build -t myimage:${{ steps.sha.outputs.commit_sha }} .
|
|
117
|
+
|
|
118
|
+
- language: yaml
|
|
119
|
+
label: "workflow_run: use head_sha from the triggering workflow"
|
|
120
|
+
code: |
|
|
121
|
+
on:
|
|
122
|
+
workflow_run:
|
|
123
|
+
workflows: [CI]
|
|
124
|
+
types: [completed]
|
|
125
|
+
|
|
126
|
+
jobs:
|
|
127
|
+
deploy:
|
|
128
|
+
runs-on: ubuntu-latest
|
|
129
|
+
steps:
|
|
130
|
+
# ❌ WRONG: github.sha on workflow_run is the default branch SHA
|
|
131
|
+
- run: echo "Wrong SHA: ${{ github.sha }}"
|
|
132
|
+
|
|
133
|
+
# ✅ CORRECT: head_sha is the SHA of the commit that triggered the upstream workflow
|
|
134
|
+
- run: echo "Correct SHA: ${{ github.event.workflow_run.head_sha }}"
|
|
135
|
+
|
|
136
|
+
prevention:
|
|
137
|
+
- "Never use `github.sha` directly in `pull_request` workflows — always use `github.event.pull_request.head.sha`."
|
|
138
|
+
- "Add a comment in your workflow YAML calling out which SHA variable to use for which event."
|
|
139
|
+
- "For Docker tagging and commit status APIs in PR workflows, always derive the SHA from the event payload."
|
|
140
|
+
- "Add a debug step printing both `github.sha` and `github.event.pull_request.head.sha` when troubleshooting."
|
|
141
|
+
- "For `workflow_run` events, use `github.event.workflow_run.head_sha` not `github.sha`."
|
|
142
|
+
docs:
|
|
143
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#github-context"
|
|
144
|
+
label: "GitHub Docs: github context — github.sha"
|
|
145
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request"
|
|
146
|
+
label: "GitHub Docs: pull_request event (GITHUB_SHA value)"
|
|
147
|
+
- url: "https://github.com/github/docs/issues/15302"
|
|
148
|
+
label: "github/docs#15302: GITHUB_SHA on PRs refers to the merge commit"
|
|
149
|
+
- url: "https://github.com/github/docs/issues/17767"
|
|
150
|
+
label: "github/docs#17767: github.sha description is misleading for pull_request"
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
id: silent-failures-020
|
|
2
|
+
title: "Job Output Silently Emptied by Secret Masking — Downstream Steps See Empty String"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- job-outputs
|
|
7
|
+
- secrets
|
|
8
|
+
- masking
|
|
9
|
+
- empty-output
|
|
10
|
+
- runner
|
|
11
|
+
- cryptic
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "Value.*masked.*job output"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "\\[MASKED\\].*output"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "Warning.*masked.*not be set as.*output"
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "(no error shown — downstream job receives empty string instead of expected value)"
|
|
21
|
+
- "Warning: Value is masked and will not be set as an output"
|
|
22
|
+
root_cause: |
|
|
23
|
+
The GitHub Actions runner applies secret masking to **job outputs**. If a job output
|
|
24
|
+
value contains a substring that matches a registered secret (or any value added via
|
|
25
|
+
`add-mask`), the runner replaces the output value with `***` (the masked placeholder)
|
|
26
|
+
before passing it to dependent jobs.
|
|
27
|
+
|
|
28
|
+
When this happens, the downstream job reads the output as an **empty string** (or the
|
|
29
|
+
literal `***` which evaluates as empty in most conditional checks), producing a silent
|
|
30
|
+
failure with no error in the logs — the upload/set step exits 0, but the value is gone.
|
|
31
|
+
|
|
32
|
+
**Triggers:**
|
|
33
|
+
- Outputting a value that happens to contain a secret substring (e.g., a build artifact
|
|
34
|
+
path that includes a secret-derived token)
|
|
35
|
+
- Using `echo "::add-mask::$VALUE"` and then emitting `$VALUE` as a job output
|
|
36
|
+
- Dynamic values (UUIDs, tokens, generated keys) that partially overlap masked values
|
|
37
|
+
|
|
38
|
+
**Why it's silent:**
|
|
39
|
+
- The `set-output` / `$GITHUB_OUTPUT` step exits with code 0
|
|
40
|
+
- The parent job shows "success"
|
|
41
|
+
- No "masked" warning appears in the UI by default
|
|
42
|
+
- The dependent job simply receives `""` and often silently skips dependent work
|
|
43
|
+
|
|
44
|
+
Sources: actions/runner#1498, GitHub Community discussions/37942
|
|
45
|
+
fix: |
|
|
46
|
+
**Option 1 (recommended): Encode output before setting, decode before using**
|
|
47
|
+
|
|
48
|
+
Base64-encode the value before writing it to `$GITHUB_OUTPUT`. Decode it in the
|
|
49
|
+
consuming job. Base64 strings do not match secret substrings.
|
|
50
|
+
|
|
51
|
+
**Option 2: Avoid passing secret-derived values as job outputs**
|
|
52
|
+
|
|
53
|
+
Restructure the workflow so secrets are consumed directly in the job that has access
|
|
54
|
+
to them rather than forwarded through outputs.
|
|
55
|
+
|
|
56
|
+
**Option 3: Check for masking with a debug step**
|
|
57
|
+
|
|
58
|
+
Add a step that prints the raw output value to confirm it is not masked before
|
|
59
|
+
downstream jobs consume it.
|
|
60
|
+
|
|
61
|
+
**Option 4: Use artifact upload instead of job outputs for large/sensitive values**
|
|
62
|
+
|
|
63
|
+
Upload the value as a workflow artifact (with restricted retention) and download it
|
|
64
|
+
in dependent jobs — artifacts bypass the masking pipeline.
|
|
65
|
+
fix_code:
|
|
66
|
+
- language: yaml
|
|
67
|
+
label: "Broken — output value matches secret pattern, silently emptied"
|
|
68
|
+
code: |
|
|
69
|
+
# ❌ BROKEN: If 'generated-token' overlaps with a masked secret, output is empty
|
|
70
|
+
jobs:
|
|
71
|
+
generate:
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
outputs:
|
|
74
|
+
token: ${{ steps.gen.outputs.token }}
|
|
75
|
+
steps:
|
|
76
|
+
- id: gen
|
|
77
|
+
run: echo "token=$SOME_DERIVED_VALUE" >> $GITHUB_OUTPUT
|
|
78
|
+
|
|
79
|
+
consume:
|
|
80
|
+
needs: generate
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
steps:
|
|
83
|
+
- run: echo "Token is ${{ needs.generate.outputs.token }}"
|
|
84
|
+
# Prints: "Token is " — silently empty, no error
|
|
85
|
+
- language: yaml
|
|
86
|
+
label: "Fixed — base64-encode output to avoid masking collision"
|
|
87
|
+
code: |
|
|
88
|
+
# ✅ FIXED: Encode before output, decode before use
|
|
89
|
+
jobs:
|
|
90
|
+
generate:
|
|
91
|
+
runs-on: ubuntu-latest
|
|
92
|
+
outputs:
|
|
93
|
+
token_b64: ${{ steps.gen.outputs.token_b64 }}
|
|
94
|
+
steps:
|
|
95
|
+
- id: gen
|
|
96
|
+
run: |
|
|
97
|
+
DERIVED_VALUE="$(generate-my-token)"
|
|
98
|
+
# Encode so masking patterns don't match
|
|
99
|
+
echo "token_b64=$(echo -n "$DERIVED_VALUE" | base64)" >> $GITHUB_OUTPUT
|
|
100
|
+
|
|
101
|
+
consume:
|
|
102
|
+
needs: generate
|
|
103
|
+
runs-on: ubuntu-latest
|
|
104
|
+
steps:
|
|
105
|
+
- run: |
|
|
106
|
+
# Decode in the consumer job
|
|
107
|
+
TOKEN=$(echo "${{ needs.generate.outputs.token_b64 }}" | base64 -d)
|
|
108
|
+
echo "Token: $TOKEN"
|
|
109
|
+
- language: yaml
|
|
110
|
+
label: "Fixed — pass value via artifact instead of job output"
|
|
111
|
+
code: |
|
|
112
|
+
# ✅ ALTERNATIVE: Use artifact upload to avoid masking pipeline entirely
|
|
113
|
+
jobs:
|
|
114
|
+
generate:
|
|
115
|
+
runs-on: ubuntu-latest
|
|
116
|
+
steps:
|
|
117
|
+
- id: gen
|
|
118
|
+
run: generate-my-token > token.txt
|
|
119
|
+
- uses: actions/upload-artifact@v4
|
|
120
|
+
with:
|
|
121
|
+
name: generated-token
|
|
122
|
+
path: token.txt
|
|
123
|
+
retention-days: 1
|
|
124
|
+
|
|
125
|
+
consume:
|
|
126
|
+
needs: generate
|
|
127
|
+
runs-on: ubuntu-latest
|
|
128
|
+
steps:
|
|
129
|
+
- uses: actions/download-artifact@v4
|
|
130
|
+
with:
|
|
131
|
+
name: generated-token
|
|
132
|
+
- run: TOKEN=$(cat token.txt) && echo "Token: $TOKEN"
|
|
133
|
+
prevention:
|
|
134
|
+
- "Never pass values through job outputs that contain or partially overlap with known secret values — use artifacts or re-derive them in consuming jobs."
|
|
135
|
+
- "If a job output starts arriving empty with no error, add a debug step to print `${{ steps.ID.outputs.KEY }}` immediately after the output is set — if it shows `***`, masking is the culprit."
|
|
136
|
+
- "Avoid `add-mask` on values you later need to pass as job outputs — once masked, the value cannot be safely forwarded."
|
|
137
|
+
- "Use base64 encoding as a general defensive practice for any dynamic/computed value passed through `$GITHUB_OUTPUT` across jobs."
|
|
138
|
+
- "Monitor GitHub runner release notes — the masking heuristics have changed across runner versions and may affect previously working workflows."
|
|
139
|
+
docs:
|
|
140
|
+
- url: "https://github.com/actions/runner/issues/1498"
|
|
141
|
+
label: "actions/runner#1498 — Job output emptied by secret masking"
|
|
142
|
+
- url: "https://github.com/orgs/community/discussions/37942"
|
|
143
|
+
label: "GitHub Community #37942 — Combining job outputs with masking leads to empty output"
|
|
144
|
+
- url: "https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#redacting-secrets-in-logs"
|
|
145
|
+
label: "GitHub Docs: Redacting secrets in logs"
|
|
146
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs"
|
|
147
|
+
label: "GitHub Docs: Passing information between jobs"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
id: silent-failures-023
|
|
2
|
+
title: "upload-artifact Silently Strips Unix File Permissions (chmod +x Lost)"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- upload-artifact
|
|
7
|
+
- download-artifact
|
|
8
|
+
- file-permissions
|
|
9
|
+
- chmod
|
|
10
|
+
- executable
|
|
11
|
+
- linux
|
|
12
|
+
- macos
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: "Permission denied"
|
|
15
|
+
flags: "i"
|
|
16
|
+
- regex: "EACCES: permission denied"
|
|
17
|
+
flags: "i"
|
|
18
|
+
- regex: "cannot execute.*Permission denied"
|
|
19
|
+
flags: "i"
|
|
20
|
+
error_messages:
|
|
21
|
+
- "/bin/sh: ./script.sh: Permission denied"
|
|
22
|
+
- "Error: EACCES: permission denied, access '/path/to/binary'"
|
|
23
|
+
- "bash: ./build/my-binary: Permission denied"
|
|
24
|
+
- "Process completed with exit code 126"
|
|
25
|
+
root_cause: |
|
|
26
|
+
The `actions/upload-artifact` action zips files using a Node.js-based zip implementation
|
|
27
|
+
that does not preserve Unix file permission mode bits. When the artifact is downloaded in
|
|
28
|
+
a subsequent job with `actions/download-artifact`, all files are extracted with default
|
|
29
|
+
permissions (typically 0644), silently losing any executable bits or other custom permissions
|
|
30
|
+
that were set in the producing job.
|
|
31
|
+
|
|
32
|
+
This affects any workflow that:
|
|
33
|
+
- Builds a binary in one job and runs it in a downstream job
|
|
34
|
+
- Produces shell scripts with +x and executes them after download
|
|
35
|
+
- Compiles Python wheels with C extensions requiring executable shared libraries
|
|
36
|
+
- Uses tools that detect file mode bits (e.g., `test -x ./binary`)
|
|
37
|
+
|
|
38
|
+
The upload and download both complete with no warnings. The failure only surfaces when the
|
|
39
|
+
downloaded file is executed in the downstream job. This is a known open issue since 2020
|
|
40
|
+
affecting both `upload-artifact@v3` and `v4`.
|
|
41
|
+
fix: |
|
|
42
|
+
Wrap files in a tar archive before uploading. Since the tar utility preserves Unix permission
|
|
43
|
+
mode bits, permissions survive the upload/download cycle. Alternatively, re-apply permissions
|
|
44
|
+
explicitly with `chmod` in the downstream job after downloading.
|
|
45
|
+
fix_code:
|
|
46
|
+
- language: yaml
|
|
47
|
+
label: "Preserve permissions with tar (recommended)"
|
|
48
|
+
code: |
|
|
49
|
+
# --- Job A: Build and upload ---
|
|
50
|
+
jobs:
|
|
51
|
+
build:
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
steps:
|
|
54
|
+
- name: Build binary
|
|
55
|
+
run: |
|
|
56
|
+
make my-binary
|
|
57
|
+
chmod +x my-binary
|
|
58
|
+
|
|
59
|
+
- name: Package with tar to preserve permissions
|
|
60
|
+
run: tar -czf artifacts.tar.gz my-binary
|
|
61
|
+
|
|
62
|
+
- uses: actions/upload-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: my-artifacts
|
|
65
|
+
path: artifacts.tar.gz
|
|
66
|
+
|
|
67
|
+
# --- Job B: Download and run ---
|
|
68
|
+
deploy:
|
|
69
|
+
needs: build
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/download-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
name: my-artifacts
|
|
75
|
+
|
|
76
|
+
- name: Extract and run (permissions restored)
|
|
77
|
+
run: |
|
|
78
|
+
tar -xzf artifacts.tar.gz
|
|
79
|
+
./my-binary
|
|
80
|
+
- language: yaml
|
|
81
|
+
label: "Re-apply chmod after download"
|
|
82
|
+
code: |
|
|
83
|
+
- uses: actions/download-artifact@v4
|
|
84
|
+
with:
|
|
85
|
+
name: my-artifacts
|
|
86
|
+
|
|
87
|
+
- name: Restore executable permissions
|
|
88
|
+
run: chmod +x my-binary ./scripts/*.sh
|
|
89
|
+
prevention:
|
|
90
|
+
- "Never rely on upload-artifact to preserve Unix file permissions — always tar or re-chmod"
|
|
91
|
+
- "If your downstream job runs a downloaded binary, add `chmod +x` before executing it as a safety habit"
|
|
92
|
+
- "Use `ls -la` after download as a debug step to verify permissions are what you expect"
|
|
93
|
+
- "For permission-sensitive artifacts, `actions/cache` can be used as an alternative — it preserves permissions"
|
|
94
|
+
docs:
|
|
95
|
+
- url: "https://github.com/actions/upload-artifact/issues/38"
|
|
96
|
+
label: "actions/upload-artifact#38 — upload-artifact does not retain artifact permissions (169 reactions)"
|
|
97
|
+
- url: "https://github.com/actions/download-artifact/issues/14"
|
|
98
|
+
label: "actions/download-artifact#14 — download-artifact loses permissions on download (54 reactions)"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
id: triggers-013
|
|
2
|
+
title: "pull_request branches Filter Matches Base (Target) Branch, Not Head (Source) Branch"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- pull-request
|
|
7
|
+
- branches-filter
|
|
8
|
+
- base-branch
|
|
9
|
+
- head-branch
|
|
10
|
+
- trigger
|
|
11
|
+
- silent-failure
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "branches.*pull_request.*head_ref"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "on.*pull_request.*branches"
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "Workflow not triggered for pull request from feature branch"
|
|
19
|
+
- "Workflow triggered unexpectedly for pull request to main"
|
|
20
|
+
root_cause: |
|
|
21
|
+
The `branches:` filter under `on.pull_request` matches the **target (base) branch**
|
|
22
|
+
of the pull request — i.e., the branch being merged INTO — NOT the source (head)
|
|
23
|
+
branch the PR was created from.
|
|
24
|
+
|
|
25
|
+
This is the opposite of what many developers expect. Common mistakes:
|
|
26
|
+
|
|
27
|
+
1. **"Run CI only for PRs from feature branches"** — You write:
|
|
28
|
+
```yaml
|
|
29
|
+
on:
|
|
30
|
+
pull_request:
|
|
31
|
+
branches: ['feature/**']
|
|
32
|
+
```
|
|
33
|
+
This does NOT trigger for PRs *from* feature branches. It triggers for PRs
|
|
34
|
+
*targeting* branches matching `feature/**`. Most feature branches are opened
|
|
35
|
+
targeting `main` or `develop`, so this filter silently prevents CI from running.
|
|
36
|
+
|
|
37
|
+
2. **"Run CI only when targeting main"** — You write the correct filter but test
|
|
38
|
+
with a PR from `main` to a release branch and are surprised it fires.
|
|
39
|
+
|
|
40
|
+
3. **branches-ignore on pull_request** — Same inversion: `branches-ignore` ignores
|
|
41
|
+
based on the TARGET branch, not the source branch.
|
|
42
|
+
|
|
43
|
+
There is no built-in filter for the source (head) branch of a pull request at
|
|
44
|
+
the trigger level. The `on.pull_request.branches` filter only controls the
|
|
45
|
+
target/base branch.
|
|
46
|
+
|
|
47
|
+
Sources: GitHub Community #26795, Stack Overflow #70101203
|
|
48
|
+
fix: |
|
|
49
|
+
**To filter by the TARGET (base) branch** (where the PR merges to):
|
|
50
|
+
Use `on.pull_request.branches:` — this is the intended behavior.
|
|
51
|
+
|
|
52
|
+
**To filter by the SOURCE (head) branch** (where the PR was created from):
|
|
53
|
+
You cannot do this at the `on:` trigger level. Instead, add an `if:` condition on
|
|
54
|
+
the job or step using `github.head_ref`:
|
|
55
|
+
|
|
56
|
+
`if: startsWith(github.head_ref, 'feature/')`
|
|
57
|
+
|
|
58
|
+
**To trigger only on PRs targeting main:**
|
|
59
|
+
```yaml
|
|
60
|
+
on:
|
|
61
|
+
pull_request:
|
|
62
|
+
branches: [main]
|
|
63
|
+
```
|
|
64
|
+
This is correct — branches: [main] means "PRs whose base is main".
|
|
65
|
+
fix_code:
|
|
66
|
+
- language: yaml
|
|
67
|
+
label: "Broken — developer intends to filter by head/source branch but branches: filters base"
|
|
68
|
+
code: |
|
|
69
|
+
# ❌ BROKEN: developer wants CI only for PRs from 'feature/**' branches
|
|
70
|
+
# but branches: matches TARGET branch, not SOURCE branch
|
|
71
|
+
# This workflow will NOT trigger for "feature/my-feat → main" PRs
|
|
72
|
+
# (because 'main' doesn't match 'feature/**')
|
|
73
|
+
on:
|
|
74
|
+
pull_request:
|
|
75
|
+
branches:
|
|
76
|
+
- 'feature/**' # This matches the BASE branch, not the head branch!
|
|
77
|
+
- language: yaml
|
|
78
|
+
label: "Fixed — use if: condition on github.head_ref to filter by source branch"
|
|
79
|
+
code: |
|
|
80
|
+
# ✅ FIXED: trigger on all PRs, then conditionally run based on head_ref
|
|
81
|
+
on:
|
|
82
|
+
pull_request:
|
|
83
|
+
|
|
84
|
+
jobs:
|
|
85
|
+
ci:
|
|
86
|
+
# Only run for PRs from feature branches (filtering by HEAD/source branch)
|
|
87
|
+
if: startsWith(github.head_ref, 'feature/')
|
|
88
|
+
runs-on: ubuntu-latest
|
|
89
|
+
steps:
|
|
90
|
+
- uses: actions/checkout@v4
|
|
91
|
+
- run: npm test
|
|
92
|
+
- language: yaml
|
|
93
|
+
label: "Correct — branches: to filter by TARGET branch (the intended use case)"
|
|
94
|
+
code: |
|
|
95
|
+
# ✅ CORRECT: run CI only for PRs targeting main or release branches
|
|
96
|
+
# (branches: matches the BASE/target branch — the branch being merged INTO)
|
|
97
|
+
on:
|
|
98
|
+
pull_request:
|
|
99
|
+
branches:
|
|
100
|
+
- main
|
|
101
|
+
- 'release/**'
|
|
102
|
+
|
|
103
|
+
jobs:
|
|
104
|
+
ci:
|
|
105
|
+
runs-on: ubuntu-latest
|
|
106
|
+
steps:
|
|
107
|
+
- uses: actions/checkout@v4
|
|
108
|
+
- run: npm test
|
|
109
|
+
- language: yaml
|
|
110
|
+
label: "Both filters — target main AND from a feature branch"
|
|
111
|
+
code: |
|
|
112
|
+
# ✅ Combine trigger-level base filter with job-level head_ref condition
|
|
113
|
+
on:
|
|
114
|
+
pull_request:
|
|
115
|
+
branches:
|
|
116
|
+
- main # Only for PRs targeting main
|
|
117
|
+
|
|
118
|
+
jobs:
|
|
119
|
+
ci:
|
|
120
|
+
# Further restrict to feature/* source branches only
|
|
121
|
+
if: startsWith(github.head_ref, 'feature/')
|
|
122
|
+
runs-on: ubuntu-latest
|
|
123
|
+
steps:
|
|
124
|
+
- uses: actions/checkout@v4
|
|
125
|
+
- run: npm test
|
|
126
|
+
prevention:
|
|
127
|
+
- "Remember: `on.pull_request.branches` matches the BASE (target) branch — the branch being merged into."
|
|
128
|
+
- "To filter by the source branch, use `if: startsWith(github.head_ref, 'your-prefix/')` at the job level."
|
|
129
|
+
- "Test your trigger config by opening a test PR and checking the Actions tab — verify the workflow does/doesn't trigger as expected."
|
|
130
|
+
- "`github.base_ref` = target branch name. `github.head_ref` = source branch name. Know the difference."
|
|
131
|
+
- "Use `branches-ignore:` to exclude PRs targeting specific branches (e.g., skip CI for PRs targeting a `docs` branch)."
|
|
132
|
+
docs:
|
|
133
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request"
|
|
134
|
+
label: "pull_request event — branches filter behavior"
|
|
135
|
+
- url: "https://github.com/orgs/community/discussions/26795"
|
|
136
|
+
label: "GitHub Community #26795 — branches filter matches base not head"
|
|
137
|
+
- url: "https://stackoverflow.com/questions/70101203/github-actions-workflow-syntax-not-working-as-expected"
|
|
138
|
+
label: "Stack Overflow #70101203 — pull_request branches filter confusion"
|
|
139
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/using-filters#using-filters-to-target-specific-branches-for-pull-request-events"
|
|
140
|
+
label: "Using filters to target specific branches for pull request events"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
id: triggers-015
|
|
2
|
+
title: "on: push Fires When a Branch Is Deleted — Unguarded Workflows Fail"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- push
|
|
7
|
+
- branch-delete
|
|
8
|
+
- github.event.deleted
|
|
9
|
+
- ref-not-found
|
|
10
|
+
- workflow-trigger
|
|
11
|
+
- branch-cleanup
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "on:\\s*push"
|
|
14
|
+
flags: "im"
|
|
15
|
+
- regex: "github\\.event\\.deleted"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "fatal.*not.*a.*git repository|fatal.*couldn't find remote ref"
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "fatal: couldn't find remote ref refs/heads/my-feature-branch"
|
|
21
|
+
- "Error: The process '/usr/bin/git' failed with exit code 128"
|
|
22
|
+
- "fatal: not a git repository (or any of the parent directories): .git"
|
|
23
|
+
- "remote: Repository not found"
|
|
24
|
+
root_cause: |
|
|
25
|
+
GitHub's `push` webhook fires on ALL ref write operations, including branch and tag
|
|
26
|
+
deletions. When a branch is deleted, a push event is emitted with:
|
|
27
|
+
- `github.event.deleted == true`
|
|
28
|
+
- `github.event.created == false`
|
|
29
|
+
- `github.ref` set to the deleted branch ref (e.g., `refs/heads/feature-xyz`)
|
|
30
|
+
- `github.event.after` set to `0000000000000000000000000000000000000000` (40 zeros)
|
|
31
|
+
- `github.sha` reverts to the default branch SHA
|
|
32
|
+
|
|
33
|
+
Workflows triggered by `on: push` that do not guard against deletion events can:
|
|
34
|
+
- Fail when `actions/checkout` tries to check out a branch that no longer exists
|
|
35
|
+
- Trigger deployment pipelines, test suites, or notification workflows unexpectedly
|
|
36
|
+
- Produce misleading failures that look like unrelated CI breakage
|
|
37
|
+
|
|
38
|
+
This is especially problematic in workflows that:
|
|
39
|
+
- Checkout a specific branch ref from `github.ref`
|
|
40
|
+
- Run clean-up scripts expecting the branch to exist
|
|
41
|
+
- Use `git describe` or `git log` on the tip of the deleted branch
|
|
42
|
+
|
|
43
|
+
GitHub documentation notes: "When you delete a branch, the SHA in the workflow run
|
|
44
|
+
(and its associated refs) reverts to the default branch of the repository."
|
|
45
|
+
Source: GitHub Docs — Events that trigger workflows (push event payload)
|
|
46
|
+
Source: GitHub Docs — Webhook events and payloads (`deleted` field on push payload)
|
|
47
|
+
fix: |
|
|
48
|
+
Add an `if:` guard to any job or workflow step that should not run on branch deletions.
|
|
49
|
+
The `github.event.deleted` context field is `true` when the push event is a deletion.
|
|
50
|
+
|
|
51
|
+
For most push-triggered workflows, the intent is to react to new commits — guard with:
|
|
52
|
+
`if: github.event.deleted != true`
|
|
53
|
+
|
|
54
|
+
For tag deletion events, also check `github.event.ref_type` if you use the `delete`
|
|
55
|
+
event, or use `if: github.event.deleted != true && !startsWith(github.ref, 'refs/tags/')`.
|
|
56
|
+
|
|
57
|
+
Alternatively, use the `on: delete` event for branch/tag cleanup workflows instead of
|
|
58
|
+
listening for deletions via the push event.
|
|
59
|
+
fix_code:
|
|
60
|
+
- language: yaml
|
|
61
|
+
label: "Guard push jobs from running on branch deletion"
|
|
62
|
+
code: |
|
|
63
|
+
on:
|
|
64
|
+
push:
|
|
65
|
+
branches:
|
|
66
|
+
- 'feature/**'
|
|
67
|
+
- 'fix/**'
|
|
68
|
+
|
|
69
|
+
jobs:
|
|
70
|
+
build:
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
# ✅ Skip the job entirely when the push is a branch deletion
|
|
73
|
+
if: github.event.deleted != true
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/checkout@v4
|
|
76
|
+
- run: npm ci && npm test
|
|
77
|
+
|
|
78
|
+
- language: yaml
|
|
79
|
+
label: "Per-step guard if you need some steps to run on deletion"
|
|
80
|
+
code: |
|
|
81
|
+
jobs:
|
|
82
|
+
process:
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
steps:
|
|
85
|
+
- name: Checkout (skip on delete)
|
|
86
|
+
if: github.event.deleted != true
|
|
87
|
+
uses: actions/checkout@v4
|
|
88
|
+
|
|
89
|
+
- name: Run tests (skip on delete)
|
|
90
|
+
if: github.event.deleted != true
|
|
91
|
+
run: npm test
|
|
92
|
+
|
|
93
|
+
- name: Log branch deleted
|
|
94
|
+
if: github.event.deleted == true
|
|
95
|
+
run: echo "Branch ${{ github.ref_name }} was deleted"
|
|
96
|
+
|
|
97
|
+
- language: yaml
|
|
98
|
+
label: "Use on: delete event for cleanup workflows instead"
|
|
99
|
+
code: |
|
|
100
|
+
# Prefer the dedicated 'delete' event for branch/tag cleanup
|
|
101
|
+
on:
|
|
102
|
+
delete:
|
|
103
|
+
# Optionally filter to branches only (not tags)
|
|
104
|
+
|
|
105
|
+
jobs:
|
|
106
|
+
cleanup:
|
|
107
|
+
runs-on: ubuntu-latest
|
|
108
|
+
# github.event.ref is the deleted branch/tag name
|
|
109
|
+
# github.event.ref_type is 'branch' or 'tag'
|
|
110
|
+
if: github.event.ref_type == 'branch'
|
|
111
|
+
steps:
|
|
112
|
+
- name: Clean up preview environment
|
|
113
|
+
run: |
|
|
114
|
+
echo "Cleaning up for deleted branch: ${{ github.event.ref }}"
|
|
115
|
+
./scripts/teardown-preview.sh "${{ github.event.ref }}"
|
|
116
|
+
|
|
117
|
+
prevention:
|
|
118
|
+
- "Add `if: github.event.deleted != true` to all jobs in `on: push` workflows."
|
|
119
|
+
- "Use the `on: delete` event for branch/tag teardown workflows instead of catching deletions via push."
|
|
120
|
+
- "Check `github.event.after == '0000000000000000000000000000000000000000'` as an alternative deletion guard."
|
|
121
|
+
- "Avoid checking out `${{ github.ref }}` directly; use `actions/checkout` defaults which handle this gracefully."
|
|
122
|
+
- "Add branch filters under `on: push: branches:` to limit which branches trigger the workflow."
|
|
123
|
+
docs:
|
|
124
|
+
- url: "https://docs.github.com/en/webhooks/webhook-events-and-payloads#push"
|
|
125
|
+
label: "GitHub Docs: push webhook event payload (deleted field)"
|
|
126
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#push"
|
|
127
|
+
label: "GitHub Docs: push event trigger"
|
|
128
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#delete"
|
|
129
|
+
label: "GitHub Docs: delete event trigger"
|