@@ -167,3 +167,90 @@ jobs:
167167 rm -f "$eslint_output"
168168 done
169169 exit $lint_failed
170+
171+ # Guardrail: fail PRs when files touched by the PR are not Prettier-formatted.
172+ format-changed-files :
173+ if : github.event_name == 'pull_request'
174+ runs-on : ubuntu-latest
175+ steps :
176+ - name : Checkout Code
177+ uses : actions/checkout@v4
178+ with :
179+ fetch-depth : 0
180+
181+ - uses : pnpm/action-setup@v4
182+ name : Install pnpm
183+ with :
184+ version : 10.15.1
185+ run_install : false
186+
187+ - name : Setup Node.js environment
188+ uses : actions/setup-node@v4
189+ with :
190+ node-version : 20
191+ cache : " pnpm"
192+
193+ - name : Install Dependencies
194+ run : pnpm install --frozen-lockfile
195+
196+ - name : Check formatting on changed files
197+ shell : bash
198+ env :
199+ BASE_REF : ${{ github.base_ref }}
200+ PR_HEAD_SHA : ${{ github.event.pull_request.head.sha }}
201+ run : |
202+ set -euo pipefail
203+
204+ # Compute a stable merge-base for the PR diff while keeping fetch depth bounded.
205+ BASE_SHA=""
206+ for fetch_depth in 1 10 50 200; do
207+ git fetch origin "$BASE_REF" --depth="$fetch_depth"
208+ BASE_SHA="$(git merge-base "origin/$BASE_REF" "$PR_HEAD_SHA" || true)"
209+ if [ -n "$BASE_SHA" ]; then
210+ break
211+ fi
212+ done
213+
214+ if [ -z "$BASE_SHA" ]; then
215+ echo "Unable to determine merge-base within depth limit (max depth: 200) for base ref '$BASE_REF'."
216+ echo "Increase the depth sequence in CI for unusually long-lived branches."
217+ exit 1
218+ fi
219+
220+ HEAD_SHA="$PR_HEAD_SHA"
221+ # Scope checks to files introduced or modified by the PR.
222+ mapfile -t changed_files < <(git diff --name-only --diff-filter=ACMR "$BASE_SHA" "$HEAD_SHA")
223+
224+ if [ ${#changed_files[@]} -eq 0 ]; then
225+ echo "No files changed in this pull request."
226+ exit 0
227+ fi
228+
229+ # Skip paths removed in the PR (deleted files cannot be formatted).
230+ mapfile -t existing_changed_files < <(
231+ for file in "${changed_files[@]}"; do
232+ if [ -f "$file" ]; then
233+ printf '%s\n' "$file"
234+ fi
235+ done
236+ )
237+
238+ if [ ${#existing_changed_files[@]} -eq 0 ]; then
239+ echo "No changed files exist in the checkout."
240+ exit 0
241+ fi
242+
243+ format_failed=0
244+ # Chunk file lists to avoid command-line length limits on very large PRs.
245+ chunk_size=100
246+
247+ for ((i=0; i<${#existing_changed_files[@]}; i+=chunk_size)); do
248+ chunk=( "${existing_changed_files[@]:i:chunk_size}" )
249+ # --ignore-unknown lets us pass all changed files safely; Prettier only checks supported types.
250+ if ! pnpm exec prettier --check --ignore-unknown "${chunk[@]}"; then
251+ format_failed=1
252+ fi
253+ done
254+
255+ # Exit non-zero if any chunk had formatting violations.
256+ exit $format_failed
0 commit comments