From fba7ef2769fe2b5986130ace0a6dfa08c720142b Mon Sep 17 00:00:00 2001 From: Ahsan Ali Date: Tue, 27 Oct 2026 20:23:25 +0000 Subject: [PATCH 1/3] ci: skip heavy CI work on docs-only pull requests Add a changes job that marks a pull request docs-only when every changed path is docs (**/*.md, docs/**, LICENSE*, .github/ISSUE_TEMPLATE/**, .github/pull_request_template.md). Required jobs still run and report, with their heavy steps gated on it; push, schedule and dispatch runs, and any error, run in full. --- .github/workflows/ci.yml | 68 ++++++++++++++++++++++++++++++++++++ .github/workflows/ios.yml | 42 ++++++++++++++++++++++ scripts/docs-only-changes.sh | 45 ++++++++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100755 scripts/docs-only-changes.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 063bffb..bc84c46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,68 +6,136 @@ on: pull_request: jobs: + # Docs-only pull requests skip the heavy work. The workflow still triggers on every PR, because + # its jobs are required status checks and a paths-ignore would leave them pending forever. + # Instead this job decides, and the jobs below still run (and report) but skip their heavy + # steps, passing in seconds. Only a pull_request run can skip: push, merge_group, schedule, + # workflow_dispatch and workflow_call always run in full. So does anything unexpected (git + # error, empty diff, this job failing). The patterns live in scripts/docs-only-changes.sh. + changes: + runs-on: ubuntu-latest + outputs: + docs_only: ${{ steps.decide.outputs.docs_only }} + steps: + - name: Checkout + if: github.event_name == 'pull_request' + uses: actions/checkout@v4 + with: + fetch-depth: 0 # the diff needs the base branch and the merge base + + - name: Decide whether this is a docs-only change + id: decide + env: + EVENT: ${{ github.event_name }} + BASE_REF: ${{ github.base_ref }} + run: | + set -uo pipefail + if [ "$EVENT" != "pull_request" ]; then + echo "Event '$EVENT': full run." + echo "docs_only=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + # --no-renames: a file moved into docs/ shows as delete + add, so it still counts as code. + if ! files="$(git -c core.quotePath=false diff --name-only --no-renames "origin/$BASE_REF...HEAD")"; then + echo "::warning::Could not list changed files against origin/$BASE_REF; running everything." + echo "docs_only=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "Changed files:"; printf '%s\n' "$files" | sed 's/^/ /' + verdict="$(printf '%s\n' "$files" | bash scripts/docs-only-changes.sh)" || verdict="" + [ "$verdict" = "docs_only=true" ] || verdict="docs_only=false" + echo "$verdict" >> "$GITHUB_OUTPUT" + library: name: Lint · Typecheck · Build · Codegen + needs: changes + if: ${{ !cancelled() }} runs-on: ubuntu-latest steps: + - name: Docs-only change — skipped + if: needs.changes.outputs.docs_only == 'true' + run: echo "::notice title=library::docs-only change — skipped" + - uses: actions/checkout@v4 + if: needs.changes.outputs.docs_only != 'true' - uses: actions/setup-node@v4 + if: needs.changes.outputs.docs_only != 'true' with: node-version: 20 cache: npm - name: Install dependencies + if: needs.changes.outputs.docs_only != 'true' run: npm ci - name: Lint + if: needs.changes.outputs.docs_only != 'true' run: npm run lint - name: Typecheck + if: needs.changes.outputs.docs_only != 'true' run: npm run typecheck - name: Build package (react-native-builder-bob) + if: needs.changes.outputs.docs_only != 'true' run: npm run prepare - name: Validate native codegen spec + if: needs.changes.outputs.docs_only != 'true' run: node node_modules/react-native/scripts/generate-codegen-artifacts.js -p . -o "${RUNNER_TEMP}/codegen" -t all example-android: name: Example · JS + Android build + needs: changes + if: ${{ !cancelled() }} runs-on: ubuntu-latest steps: + - name: Docs-only change — skipped + if: needs.changes.outputs.docs_only == 'true' + run: echo "::notice title=example-android::docs-only change — skipped" + - uses: actions/checkout@v4 + if: needs.changes.outputs.docs_only != 'true' - uses: actions/setup-node@v4 + if: needs.changes.outputs.docs_only != 'true' with: node-version: 20 cache: npm - uses: actions/setup-java@v4 + if: needs.changes.outputs.docs_only != 'true' with: distribution: temurin java-version: 17 - name: Set up Gradle cache + if: needs.changes.outputs.docs_only != 'true' uses: gradle/actions/setup-gradle@v4 - name: Install library dependencies + if: needs.changes.outputs.docs_only != 'true' run: npm ci - name: Install example dependencies + if: needs.changes.outputs.docs_only != 'true' working-directory: example run: npm install --no-audit --no-fund - name: Typecheck example + if: needs.changes.outputs.docs_only != 'true' working-directory: example run: npx tsc --noEmit - name: Bundle JS (Android + iOS) + if: needs.changes.outputs.docs_only != 'true' working-directory: example run: | npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output "${RUNNER_TEMP}/android.bundle" --assets-dest "${RUNNER_TEMP}/android-assets" npx react-native bundle --platform ios --dev false --entry-file index.js --bundle-output "${RUNNER_TEMP}/ios.bundle" --assets-dest "${RUNNER_TEMP}/ios-assets" - name: Build debug APK + if: needs.changes.outputs.docs_only != 'true' working-directory: example/android run: ./gradlew :app:assembleDebug --no-daemon diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 719b861..2893244 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -6,8 +6,50 @@ on: pull_request: jobs: + # Docs-only pull requests skip the heavy work. The workflow still triggers on every PR, because + # its jobs are required status checks and a paths-ignore would leave them pending forever. + # Instead this job decides, and the jobs below still run (and report) but skip their heavy + # steps, passing in seconds. Only a pull_request run can skip: push, merge_group, schedule, + # workflow_dispatch and workflow_call always run in full. So does anything unexpected (git + # error, empty diff, this job failing). The patterns live in scripts/docs-only-changes.sh. + changes: + runs-on: ubuntu-latest + outputs: + docs_only: ${{ steps.decide.outputs.docs_only }} + steps: + - name: Checkout + if: github.event_name == 'pull_request' + uses: actions/checkout@v4 + with: + fetch-depth: 0 # the diff needs the base branch and the merge base + + - name: Decide whether this is a docs-only change + id: decide + env: + EVENT: ${{ github.event_name }} + BASE_REF: ${{ github.base_ref }} + run: | + set -uo pipefail + if [ "$EVENT" != "pull_request" ]; then + echo "Event '$EVENT': full run." + echo "docs_only=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + # --no-renames: a file moved into docs/ shows as delete + add, so it still counts as code. + if ! files="$(git -c core.quotePath=false diff --name-only --no-renames "origin/$BASE_REF...HEAD")"; then + echo "::warning::Could not list changed files against origin/$BASE_REF; running everything." + echo "docs_only=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "Changed files:"; printf '%s\n' "$files" | sed 's/^/ /' + verdict="$(printf '%s\n' "$files" | bash scripts/docs-only-changes.sh)" || verdict="" + [ "$verdict" = "docs_only=true" ] || verdict="docs_only=false" + echo "$verdict" >> "$GITHUB_OUTPUT" + ios: name: Build example (iOS simulator) + needs: changes + if: ${{ !cancelled() && needs.changes.outputs.docs_only != 'true' }} runs-on: macos-15 steps: - uses: actions/checkout@v4 diff --git a/scripts/docs-only-changes.sh b/scripts/docs-only-changes.sh new file mode 100755 index 0000000..d374898 --- /dev/null +++ b/scripts/docs-only-changes.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# +# Decide whether a change set is docs-only, so CI can skip its heavy steps on a pull request that +# cannot affect them. Reads changed paths (one per line, repo-relative, as printed by +# `git diff --name-only --no-renames`) on stdin and prints exactly one line: +# docs_only=true every path is documentation +# docs_only=false at least one path is not, or the list is empty (fail safe: run everything) +# +# Documentation means: **/*.md docs/** LICENSE* .github/ISSUE_TEMPLATE/** +# .github/pull_request_template.md +# Matching is case-sensitive on purpose: anything unexpected runs the full suite. +# +# git diff --name-only --no-renames origin/main...HEAD | bash scripts/docs-only-changes.sh +# +# Exit codes: 0 always when stdin was read (the answer is on stdout), 2 usage error. +set -uo pipefail + +if [ "$#" -ne 0 ]; then + echo "usage: | bash scripts/docs-only-changes.sh" >&2 + exit 2 +fi + +seen=0 +while IFS= read -r path || [ -n "$path" ]; do + path="${path%$'\r'}" + [ -z "$path" ] && continue + seen=$((seen + 1)) + case "$path" in + *.md | docs/* | LICENSE* | .github/ISSUE_TEMPLATE/* | .github/pull_request_template.md) ;; + *) + echo "not docs: $path" >&2 + echo "docs_only=false" + exit 0 + ;; + esac +done + +if [ "$seen" -eq 0 ]; then + echo "no changed files listed; treating as a code change" >&2 + echo "docs_only=false" + exit 0 +fi + +echo "all $seen changed file(s) are docs" >&2 +echo "docs_only=true" From 82e0a19b9fab823cadc4bc1a0ac2a33f5f8f825a Mon Sep 17 00:00:00 2001 From: Ahsan Ali Date: Tue, 27 Oct 2026 22:34:58 +0000 Subject: [PATCH 2/3] ci: run the changes job on the self-hosted Mac runners No GitHub-hosted minutes: the new docs-only changes job runs on [self-hosted, macOS] like the rest of the org's CI. --- .github/workflows/ci.yml | 5 ++++- .github/workflows/ios.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc84c46..0ff7843 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,10 @@ jobs: # workflow_dispatch and workflow_call always run in full. So does anything unexpected (git # error, empty diff, this job failing). The patterns live in scripts/docs-only-changes.sh. changes: - runs-on: ubuntu-latest + # Public repo: fork pull_request runs never reach the self-hosted Macs. + if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request' + # Self-hosted Mac minis, like every job in the org (no GitHub-hosted minutes). + runs-on: [self-hosted, macOS] outputs: docs_only: ${{ steps.decide.outputs.docs_only }} steps: diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 2893244..7503341 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -13,7 +13,10 @@ jobs: # workflow_dispatch and workflow_call always run in full. So does anything unexpected (git # error, empty diff, this job failing). The patterns live in scripts/docs-only-changes.sh. changes: - runs-on: ubuntu-latest + # Public repo: fork pull_request runs never reach the self-hosted Macs. + if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request' + # Self-hosted Mac minis, like every job in the org (no GitHub-hosted minutes). + runs-on: [self-hosted, macOS] outputs: docs_only: ${{ steps.decide.outputs.docs_only }} steps: From 791a4d7b49254c8540caeb49919d66f1d1824b9f Mon Sep 17 00:00:00 2001 From: Ahsan Ali Date: Tue, 27 Oct 2026 22:53:30 +0000 Subject: [PATCH 3/3] Revert: keep the changes job on GitHub-hosted runners The runner plan changed: public repos stay on GitHub-hosted runners (free for public repos), so the changes job goes back to ubuntu-latest and needs no fork guard. --- .github/workflows/ci.yml | 5 +---- .github/workflows/ios.yml | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ff7843..bc84c46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,7 @@ jobs: # workflow_dispatch and workflow_call always run in full. So does anything unexpected (git # error, empty diff, this job failing). The patterns live in scripts/docs-only-changes.sh. changes: - # Public repo: fork pull_request runs never reach the self-hosted Macs. - if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request' - # Self-hosted Mac minis, like every job in the org (no GitHub-hosted minutes). - runs-on: [self-hosted, macOS] + runs-on: ubuntu-latest outputs: docs_only: ${{ steps.decide.outputs.docs_only }} steps: diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 7503341..2893244 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -13,10 +13,7 @@ jobs: # workflow_dispatch and workflow_call always run in full. So does anything unexpected (git # error, empty diff, this job failing). The patterns live in scripts/docs-only-changes.sh. changes: - # Public repo: fork pull_request runs never reach the self-hosted Macs. - if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request' - # Self-hosted Mac minis, like every job in the org (no GitHub-hosted minutes). - runs-on: [self-hosted, macOS] + runs-on: ubuntu-latest outputs: docs_only: ${{ steps.decide.outputs.docs_only }} steps: