From b1e8b4154d366d464a4b85a56a349394e83a6880 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Thu, 30 Jul 2026 16:00:20 +0530 Subject: [PATCH] ci: skip the expensive jobs on documentation-only changes `ci.yml` and `ui-tests.yml` carried `paths-ignore` for `**.md` and `docs/**`. A workflow skipped by path filtering never reports its checks at all: they sit in Pending forever. `Server` and `Playwright E2E Tests` are required checks on `develop`, so a documentation-only PR was not merely cheap to run, it was unmergeable. A job skipped by a job-level `if:` reports Success instead, and satisfies a required check. So the filter moves from the workflow trigger to the jobs: a `changes` job on `ubuntu-slim` computes one boolean, and every expensive job takes `needs: changes` plus an `if:` on it. The detection lives in a composite action so the four workflows share it. It lists the PR's files via `gh api` rather than `git diff`, which keeps the shallow clone shallow. Only `pull_request` has a base to compare against, so every other event -- push, merge_group, workflow_dispatch, schedule -- falls through to running everything; those guard a branch directly and must never skip. The API call fails open for the same reason: a skipped required check reads as success, so a transient failure must not look like "docs only". Co-Authored-By: Claude Opus 5 --- .../actions/detect-code-changes/action.yml | 56 +++++++++++++++++++ .github/workflows/ci.yml | 20 +++++-- .github/workflows/typecheck.yml | 14 +++++ .github/workflows/ui-tests.yml | 20 +++++-- .github/workflows/unit-tests.yml | 14 +++++ 5 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 .github/actions/detect-code-changes/action.yml diff --git a/.github/actions/detect-code-changes/action.yml b/.github/actions/detect-code-changes/action.yml new file mode 100644 index 00000000..2bdf1a97 --- /dev/null +++ b/.github/actions/detect-code-changes/action.yml @@ -0,0 +1,56 @@ +name: "Detect Code Changes" +description: "Reports whether the event touched anything a build or test could care about" + +outputs: + code: + description: "'true' unless this is a pull request that only touched documentation" + value: ${{ steps.detect.outputs.code }} + +runs: + using: "composite" + steps: + - name: Detect + id: detect + shell: bash -e {0} + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + # Only a pull request has a base to diff against. push, merge_group, + # workflow_dispatch and schedule all guard a branch directly, so they run + # everything. + if [ "$GITHUB_EVENT_NAME" != "pull_request" ]; then + echo "Event is $GITHUB_EVENT_NAME, nothing to diff." >> "$GITHUB_STEP_SUMMARY" + echo "code=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Fails open: downstream jobs are skipped when this one fails, and a + # skipped required check counts as successful, so an API hiccup must + # never look like "documentation only". + files=$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/files" \ + --jq '.[].filename') || files="" + + if [ -z "$files" ]; then + echo "No file list available, running everything." >> "$GITHUB_STEP_SUMMARY" + echo "code=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + code=false + while IFS= read -r file; do + case "$file" in + *.md | docs/* | .claude/* | specs/*) ;; + *) + echo "Code change: $file" >> "$GITHUB_STEP_SUMMARY" + code=true + break + ;; + esac + done <<< "$files" + + if [ "$code" = false ]; then + echo "Documentation only." >> "$GITHUB_STEP_SUMMARY" + fi + echo "code=$code" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4bfd1527..98e84859 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,13 +5,7 @@ on: branches: - develop - main - paths-ignore: - - "**.md" - - "docs/**" pull_request: - paths-ignore: - - "**.md" - - "docs/**" merge_group: concurrency: @@ -19,7 +13,21 @@ concurrency: cancel-in-progress: true jobs: + changes: + runs-on: ubuntu-slim + permissions: + contents: read + pull-requests: read + outputs: + code: ${{ steps.detect.outputs.code }} + steps: + - uses: actions/checkout@v4 + - id: detect + uses: ./.github/actions/detect-code-changes + tests: + needs: changes + if: needs.changes.outputs.code == 'true' runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index f7bd93e7..0ea48f95 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -13,8 +13,22 @@ concurrency: cancel-in-progress: true jobs: + changes: + runs-on: ubuntu-slim + permissions: + contents: read + pull-requests: read + outputs: + code: ${{ steps.detect.outputs.code }} + steps: + - uses: actions/checkout@v4 + - id: detect + uses: ./.github/actions/detect-code-changes + typecheck: name: TypeScript + needs: changes + if: needs.changes.outputs.code == 'true' runs-on: ubuntu-latest steps: diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 67eae662..5b64f6a0 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -5,13 +5,7 @@ on: branches: - develop - main - paths-ignore: - - "**.md" - - "docs/**" pull_request: - paths-ignore: - - "**.md" - - "docs/**" merge_group: workflow_dispatch: @@ -20,7 +14,21 @@ concurrency: cancel-in-progress: true jobs: + changes: + runs-on: ubuntu-slim + permissions: + contents: read + pull-requests: read + outputs: + code: ${{ steps.detect.outputs.code }} + steps: + - uses: actions/checkout@v4 + - id: detect + uses: ./.github/actions/detect-code-changes + ui-tests: + needs: changes + if: needs.changes.outputs.code == 'true' runs-on: ubuntu-latest timeout-minutes: 20 name: Playwright E2E Tests diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 3c41af0e..bdb172d0 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -13,8 +13,22 @@ concurrency: cancel-in-progress: true jobs: + changes: + runs-on: ubuntu-slim + permissions: + contents: read + pull-requests: read + outputs: + code: ${{ steps.detect.outputs.code }} + steps: + - uses: actions/checkout@v4 + - id: detect + uses: ./.github/actions/detect-code-changes + unit-tests: name: Unit Tests + needs: changes + if: needs.changes.outputs.code == 'true' runs-on: ubuntu-latest steps: