fix(ci): unblock docs-only PRs from required check deadlock#2641
fix(ci): unblock docs-only PRs from required check deadlock#2641
Conversation
Move paths-ignore from workflow triggers to job-level `if:` conditions. When a workflow uses paths-ignore at the trigger level, GitHub never creates the check run, so required checks stay "Waiting" forever. Job-level skips via `if:` report as passed, satisfying branch protection. Also update deploy-gate to treat "skipped" conclusions as passing.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Superseded — CI fix cherry-picked into #2639 so the workflow changes trigger real GitHub Actions check runs (manual status posts don't satisfy branch protection's app requirement). |
Greptile SummaryThis PR fixes a well-known GitHub Actions deadlock: when Key changes:
Issues found:
Confidence Score: 4/5Safe to merge with awareness of the The core logic is correct and well-reasoned: job-level
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
PR[Pull Request opened / updated] --> TW[Test workflow triggered]
PR --> TCW[Typecheck workflow triggered]
PR --> LCW[Lint Code workflow triggered]
TW --> C1[changes job\ngh api …/pulls/N/files]
TCW --> C2[changes job\ngh api …/pulls/N/files]
LCW --> C3[changes job\ngh api …/pulls/N/files]
C1 -->|code=true| U[unit job\nruns normally]
C1 -->|code=false| US[unit job\nskipped → conclusion: skipped]
C2 -->|code=true| TC[typecheck job\nruns normally]
C2 -->|code=false| TCS[typecheck job\nskipped → conclusion: skipped]
C3 -->|code=true| B[biome job\nruns normally]
C3 -->|code=false| BS[biome job\nskipped → conclusion: skipped]
U --> DG[deploy-gate\nworkflow_run completed]
US --> DG
TC --> DG
TCS --> DG
DG --> CHECK{unit == success or skipped?\nAND typecheck == success or skipped?}
CHECK -->|yes| GS[gate: success ✅\nbranch protection passes]
CHECK -->|no| GF[gate: failure ❌\nbranch protection blocks]
CHECK -->|either pending| GP[gate: pending ⏳\nwait for next trigger]
|
| CODE=$(echo "$FILES" | grep -vcE '\.md$|^docs/|^src-tauri/|^CHANGELOG\.md$|^LICENSE$|\.github/workflows/(build-desktop|docker-publish)\.yml$' || echo 0) | ||
| echo "code=$( [ "$CODE" -gt 0 ] && echo true || echo false )" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
changes failure silently skips required checks
If the changes job fails (e.g. transient gh api rate-limit, network error, or a jq parse error), GitHub marks the downstream unit / typecheck / biome jobs as skipped because their needs: dependency did not succeed. The deploy-gate then sees unit=skipped and typecheck=skipped, treats both as passing, and posts a success gate status — effectively letting a broken code PR through without any test or type coverage.
Consider adding an explicit fallback that sets code=true on any API failure so the downstream jobs run rather than skip:
- id: diff
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ github.event_name }}" = "push" ]; then
echo "code=true" >> "$GITHUB_OUTPUT"
exit 0
fi
FILES=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" \
--paginate --jq '.[].filename') || { echo "code=true" >> "$GITHUB_OUTPUT"; exit 0; }
CODE=$(echo "$FILES" | grep -vcE '\.md$|^docs/|^src-tauri/|^CHANGELOG\.md$|^LICENSE$|\.github/workflows/(build-desktop|docker-publish)\.yml$' || echo 0)
echo "code=$( [ "$CODE" -gt 0 ] && echo true || echo false )" >> "$GITHUB_OUTPUT"The same applies to the identical changes jobs in lint-code.yml and typecheck.yml.
| if [ "${{ github.event_name }}" = "push" ]; then | ||
| echo "code=true" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
| FILES=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" \ | ||
| --paginate --jq '.[].filename') | ||
| CODE=$(echo "$FILES" | grep -vcE '\.md$|^docs/|^src-tauri/|^CHANGELOG\.md$|^LICENSE$|\.github/workflows/(build-desktop|docker-publish)\.yml$' || echo 0) | ||
| echo "code=$( [ "$CODE" -gt 0 ] && echo true || echo false )" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
Expression interpolation inside
run: shell scripts
${{ github.event_name }}, ${{ github.repository }}, and ${{ github.event.number }} are interpolated directly into the shell script before execution. While all three are GitHub-controlled values (not user-supplied), this pattern is flagged by actionlint and GitHub's own security guidance because it bypasses shell quoting entirely. The preferred approach is to surface them as env: variables (as done for GH_TOKEN) and reference them as $VAR in the shell.
- id: diff
env:
GH_TOKEN: ${{ github.token }}
EVENT_NAME: ${{ github.event_name }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.number }}
run: |
if [ "$EVENT_NAME" = "push" ]; then
echo "code=true" >> "$GITHUB_OUTPUT"
exit 0
fi
FILES=$(gh api "repos/$GH_REPO/pulls/$PR_NUMBER/files" \
--paginate --jq '.[].filename')
CODE=$(echo "$FILES" | grep -vcE '\.md$|^docs/|^src-tauri/|^CHANGELOG\.md$|^LICENSE$|\.github/workflows/(build-desktop|docker-publish)\.yml$' || echo 0)
echo "code=$( [ "$CODE" -gt 0 ] && echo true || echo false )" >> "$GITHUB_OUTPUT"The same pattern appears in lint-code.yml (lines 23-30) and typecheck.yml (lines 23-30).
Summary
paths-ignore: '**/*.md'at the workflow trigger level means the workflow never fires, so GitHub never creates the check runbiome,typecheck,unit,gateto pass, creating a deadlockFix: Move path filtering from workflow-level
paths-ignoreto job-levelif:conditions via a lightweightchangesjob that queries the GitHub API for changed files. When a job is skipped viaif:, GitHub reports it as passed for required checks.Also updates
deploy-gateto treatskippedconclusions as passing.Test plan