@htekdev/actions-debugger 1.0.13 → 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/az-powershell-14-to-15-breaking.yml +108 -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/ubuntu-2204-precached-docker-removed.yml +110 -0
- package/errors/runner-environment/windows-latest-d-drive-removed.yml +104 -0
- package/errors/runner-environment/windows-msvc-ltcg-mixed-image-versions.yml +112 -0
- package/errors/runner-environment/windows-vs2026-cuda-host-compiler-unsupported.yml +145 -0
- package/errors/silent-failures/app-store-ios26-sdk-required.yml +113 -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,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"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
id: triggers-014
|
|
2
|
+
title: "github.event.before Is All Zeros on First Push to a New Branch — git diff Fails"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- push
|
|
7
|
+
- first-push
|
|
8
|
+
- git-diff
|
|
9
|
+
- before-sha
|
|
10
|
+
- new-branch
|
|
11
|
+
- branch-creation
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "fatal.*bad revision.*0000000000000000000000000000000000000000"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "fatal.*ambiguous argument '0{40}'"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "unknown revision or path not in the working tree.*0{10}"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "0{40}\\.\\.\\.\\$\\{\\{.*github\\.event\\.before"
|
|
20
|
+
flags: "i"
|
|
21
|
+
error_messages:
|
|
22
|
+
- "fatal: bad revision '0000000000000000000000000000000000000000...HEAD'"
|
|
23
|
+
- "fatal: ambiguous argument '0000000000000000000000000000000000000000': unknown revision or path not in the working tree"
|
|
24
|
+
- "error: object file .git/objects/00/00000000000000000000000000000000000000 is empty"
|
|
25
|
+
root_cause: |
|
|
26
|
+
When a **new branch is pushed for the first time**, `github.event.before` is set to
|
|
27
|
+
`0000000000000000000000000000000000000000` (40 zeros), representing the null/empty SHA.
|
|
28
|
+
This indicates there was no previous commit on this branch — it is a branch creation event.
|
|
29
|
+
|
|
30
|
+
Workflows that use `github.event.before` to compute a diff range (e.g., to find changed
|
|
31
|
+
files) fail when run on a first push because `git diff` or `git log` cannot resolve the
|
|
32
|
+
null SHA:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
run: git diff ${{ github.event.before }}...HEAD
|
|
36
|
+
# On first push: git diff 0000000000000000000000000000000000000000...HEAD → FATAL ERROR
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**When this occurs:**
|
|
40
|
+
- First push to a new branch (branch creation)
|
|
41
|
+
- `push` events that create a new branch (`github.event.created == true`)
|
|
42
|
+
- Forced pushes that reference a non-existent before-SHA (rare)
|
|
43
|
+
|
|
44
|
+
**Why it's painful:**
|
|
45
|
+
- Workflows work fine for subsequent pushes (where `before` is a real commit)
|
|
46
|
+
- The error only appears on the first push, often during branch creation in a PR workflow
|
|
47
|
+
- The error message references the git object store internals, not the workflow context
|
|
48
|
+
- Developers check out the diff command syntax without realizing the SHA is invalid
|
|
49
|
+
fix: |
|
|
50
|
+
**Option 1 (recommended): Guard against the null SHA before running git diff**
|
|
51
|
+
|
|
52
|
+
Check if `github.event.before` is the null SHA and fall back to diffing against the
|
|
53
|
+
merge base or the initial commit.
|
|
54
|
+
|
|
55
|
+
**Option 2: Use the `dorny/paths-filter` action**
|
|
56
|
+
|
|
57
|
+
The `dorny/paths-filter` action handles first-push, force-push, and all edge cases
|
|
58
|
+
in branch diff computation automatically.
|
|
59
|
+
|
|
60
|
+
**Option 3: Use `github.event.created` to skip diff-based logic on branch creation**
|
|
61
|
+
|
|
62
|
+
Add an `if: github.event.created != true` guard to diff-dependent steps so they only
|
|
63
|
+
run on non-creation push events.
|
|
64
|
+
fix_code:
|
|
65
|
+
- language: yaml
|
|
66
|
+
label: "Broken — git diff with github.event.before fails on first push"
|
|
67
|
+
code: |
|
|
68
|
+
# ❌ BROKEN: Fails when branch is pushed for the first time
|
|
69
|
+
on: push
|
|
70
|
+
|
|
71
|
+
jobs:
|
|
72
|
+
lint-changed:
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/checkout@v4
|
|
76
|
+
with:
|
|
77
|
+
fetch-depth: 0
|
|
78
|
+
- name: Lint only changed files
|
|
79
|
+
run: |
|
|
80
|
+
CHANGED=$(git diff --name-only ${{ github.event.before }}...HEAD)
|
|
81
|
+
# FATAL on first push: 0000000000000000000000000000000000000000 is not a commit
|
|
82
|
+
- language: yaml
|
|
83
|
+
label: "Fixed — guard against null SHA before git diff"
|
|
84
|
+
code: |
|
|
85
|
+
# ✅ FIXED: Check for null SHA and fall back to first commit
|
|
86
|
+
on: push
|
|
87
|
+
|
|
88
|
+
jobs:
|
|
89
|
+
lint-changed:
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
steps:
|
|
92
|
+
- uses: actions/checkout@v4
|
|
93
|
+
with:
|
|
94
|
+
fetch-depth: 0
|
|
95
|
+
- name: Lint only changed files
|
|
96
|
+
run: |
|
|
97
|
+
BEFORE="${{ github.event.before }}"
|
|
98
|
+
NULL_SHA="0000000000000000000000000000000000000000"
|
|
99
|
+
|
|
100
|
+
if [ "$BEFORE" = "$NULL_SHA" ]; then
|
|
101
|
+
# First push to new branch — diff against the initial commit or all files
|
|
102
|
+
echo "First push to branch, diffing all files"
|
|
103
|
+
CHANGED=$(git diff --name-only $(git rev-list --max-parents=0 HEAD)...HEAD)
|
|
104
|
+
else
|
|
105
|
+
CHANGED=$(git diff --name-only "$BEFORE"...HEAD)
|
|
106
|
+
fi
|
|
107
|
+
echo "$CHANGED"
|
|
108
|
+
- language: yaml
|
|
109
|
+
label: "Fixed — skip diff steps on branch creation using github.event.created"
|
|
110
|
+
code: |
|
|
111
|
+
# ✅ FIXED: Skip diff-based steps entirely on first push (branch creation)
|
|
112
|
+
on: push
|
|
113
|
+
|
|
114
|
+
jobs:
|
|
115
|
+
lint-changed:
|
|
116
|
+
runs-on: ubuntu-latest
|
|
117
|
+
steps:
|
|
118
|
+
- uses: actions/checkout@v4
|
|
119
|
+
with:
|
|
120
|
+
fetch-depth: 0
|
|
121
|
+
- name: Lint only changed files
|
|
122
|
+
# Skip on branch creation (before SHA is null)
|
|
123
|
+
if: github.event.created != true
|
|
124
|
+
run: |
|
|
125
|
+
git diff --name-only ${{ github.event.before }}...HEAD | xargs eslint
|
|
126
|
+
- language: yaml
|
|
127
|
+
label: "Fixed — use dorny/paths-filter which handles null SHA automatically"
|
|
128
|
+
code: |
|
|
129
|
+
# ✅ RECOMMENDED: dorny/paths-filter handles first-push and force-push edge cases
|
|
130
|
+
on: push
|
|
131
|
+
|
|
132
|
+
jobs:
|
|
133
|
+
lint-changed:
|
|
134
|
+
runs-on: ubuntu-latest
|
|
135
|
+
steps:
|
|
136
|
+
- uses: actions/checkout@v4
|
|
137
|
+
- uses: dorny/paths-filter@v3
|
|
138
|
+
id: changes
|
|
139
|
+
with:
|
|
140
|
+
filters: |
|
|
141
|
+
src:
|
|
142
|
+
- 'src/**'
|
|
143
|
+
tests:
|
|
144
|
+
- 'tests/**'
|
|
145
|
+
- name: Lint src changes
|
|
146
|
+
if: steps.changes.outputs.src == 'true'
|
|
147
|
+
run: npm run lint
|
|
148
|
+
prevention:
|
|
149
|
+
- "Never use `git diff ${{ github.event.before }}` without guarding against the null SHA (`0000000000000000000000000000000000000000`) — this is guaranteed to fail on first push."
|
|
150
|
+
- "Prefer `dorny/paths-filter` for changed-file detection — it handles all SHA edge cases (first push, force push, merge commits) internally."
|
|
151
|
+
- "If you must use raw git diff, always add: `if [ \"$BEFORE\" = \"$NULL_SHA\" ]; then echo 'first push'; exit 0; fi`."
|
|
152
|
+
- "Test your diff workflows by creating a fresh test branch — don't only test on subsequent pushes where `before` is always valid."
|
|
153
|
+
- "Use `github.event.created == true` in `if:` conditions to identify branch creation events and handle them separately."
|
|
154
|
+
docs:
|
|
155
|
+
- url: "https://docs.github.com/en/webhooks/webhook-events-and-payloads#push"
|
|
156
|
+
label: "GitHub Docs: push webhook event payload — before field"
|
|
157
|
+
- url: "https://github.com/dorny/paths-filter"
|
|
158
|
+
label: "dorny/paths-filter — changed file detection with edge case handling"
|
|
159
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#push"
|
|
160
|
+
label: "GitHub Docs: Events that trigger workflows — push"
|