fix(ci): path-aware find-projects#161
Conversation
…ocs-only PRs Required status checks gate on each named check actually reporting a pass. A workflow that's been paths-filtered out doesn't trigger, so its aggregator check never appears and the PR stays in "Expected — waiting for status to be reported" forever. Drop the `paths:` filter from `pull_request:` on every test workflow plus extract.yml so they always trigger. To keep CI cost flat (no full matrix on irrelevant PRs), make each find-projects step compute the changed paths between PR base and head via `git diff` and emit only the projects whose subtree was touched. Docs-only and other-framework PRs produce an empty matrix → test job skipped → aggregator passes via `result == 'skipped'` → required check satisfied. Workflow-file changes still trigger a full matrix for the affected framework, so a PR editing test-js.yml exercises every JS sample. push/schedule/workflow_dispatch events are unchanged — they run the full matrix. Also harden test-js's find with `-not -path "*/.angular/*"` to defend against Angular's Vite dep cache leaking into the matrix if workflow ordering ever changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Switches the JS / Java / iOS / .NET / Android test workflows and extract.yml from paths:-filtered triggers to always-trigger triggers, then narrows each workflow's matrix at runtime by diffing the PR's changed files against the discovered project list. The motivation is that GitHub's required-status-checks rule waits forever on a check name produced by a workflow that paths:-filtered itself out; making every required workflow always trigger (and producing an empty matrix → skipped job → green aggregator) avoids that stuck state.
Changes:
- Drop
paths:filters frompull_request(and the matchingpushfilters) on five test-* workflows andextract.yml. - In each test-* workflow's
find-projectsstep, checkout withfetch-depth: 0, compute changed paths viagit diff BASE..HEAD, and emit a project only if some changed file is under it; the workflow file itself acts as an "escape hatch" that re-enables the full matrix. - Harden
test-js'sfindwith-not -path "*/.angular/*"to keep Angular's Vite dep cache out of the matrix.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/test-js.yml |
Removes paths filter, adds path-aware find-projects with .angular exclude. |
.github/workflows/test-java.yml |
Removes paths filter, adds path-aware find-projects. |
.github/workflows/test-ios.yml |
Removes paths filter, adds path-aware find-projects. |
.github/workflows/test-dotnet.yml |
Removes paths filter, adds path-aware find-projects. |
.github/workflows/test-android.yml |
Removes paths filter, adds path-aware find-projects. |
.github/workflows/extract.yml |
Removes pull_request paths filter so validate always reports. |
Comments suppressed due to low confidence (4)
.github/workflows/test-js.yml:55
- The matching uses
grep -q "^${proj}/"where$projis interpolated as a regex pattern. Project paths can legitimately contain regex metacharacters (e.g..,+), and any future sample directory named likesamples/node/express.jswould have.match any character — causing both false positives (matching unrelated paths) and unanchored sub-string matches. Use a fixed-string match anchored with a leading separator, e.g. by pipingCHANGEDand usingawkwith a literal prefix check, or by escaping regex metacharacters in$projbefore passing togrep. Same issue exists in test-java.yml, test-ios.yml, test-dotnet.yml, test-android.yml.
FILTERED=$(while IFS= read -r proj; do
[ -z "$proj" ] && continue
if printf '%s\n' "$CHANGED" | grep -q "^${proj}/"; then
echo "$proj"
fi
done <<< "$ALL")
.github/workflows/test-js.yml:44
git diff --name-only "$BASE_SHA" "$HEAD_SHA" 2>/dev/null || echo ""silently swallows any failure (e.g. one of the SHAs not fetched, transient git error). The result isCHANGED="", which yields an empty matrix and the test job is skipped — the aggregator then reports green even though no test ran and no error surfaced. Failing closed (running the full matrix, or failing the find step) would be safer than silently passing. Same concern applies to test-java.yml, test-ios.yml, test-dotnet.yml, and test-android.yml.
if [ "$EVENT_NAME" = "pull_request" ]; then
CHANGED=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" 2>/dev/null || echo "")
.github/workflows/test-js.yml:48
- The
WORKFLOW_FILE"escape hatch" only triggers a full matrix when the workflow file itself changes. PRs that modify shared dependencies the workflow relies on — for example a composite action under.github/actions/**, a reusablescripts/**helper invoked by the test job, root-level config likepackage.json/ lockfiles that affect every sample, or shared workflow includes — will not retrigger any project's matrix and the test job will be skipped. Consider broadening this trigger (e.g. include.github/actions/**, top-level config files, or any non-samples/**path that the test job consumes) or documenting the assumption that no such shared inputs exist. Same applies to all five test-*.yml files.
# If the workflow file itself changed, run the full matrix so we
# exercise the workflow against every sample.
if printf '%s\n' "$CHANGED" | grep -qFx "$WORKFLOW_FILE"; then
FILTERED="$ALL"
.github/workflows/test-js.yml:62
- The same ~20-line find-projects logic (checkout with fetch-depth: 0, env block, set -euo pipefail, find + diff + filter + jq matrix emit) is duplicated across all five test-*.yml workflows. Any fix to the regex-escaping issue, the silent-failure behavior, or the workflow-file escape hatch must be applied in five places and will drift over time. Consider extracting this into a composite action under
.github/actions/find-changed-projectsthat takes the find pattern (and any extra excludes) as inputs and emits the matrix output. Each workflow then becomes ~5 lines.
- id: find
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
EVENT_NAME: ${{ github.event_name }}
WORKFLOW_FILE: .github/workflows/test-js.yml
run: |
set -euo pipefail
# Exclude `.angular/cache/**/package.json` — Angular's Vite dep cache
# writes nested package.json files inside .angular/cache after a build.
# On a fresh CI checkout the cache doesn't exist yet, but the exclude
# hardens us against any future ordering change that runs a build first.
ALL=$(find samples -name "package.json" -not -path "*/node_modules/*" -not -path "*/.angular/*" -exec dirname {} \; 2>/dev/null | sort)
if [ "$EVENT_NAME" = "pull_request" ]; then
CHANGED=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" 2>/dev/null || echo "")
# If the workflow file itself changed, run the full matrix so we
# exercise the workflow against every sample.
if printf '%s\n' "$CHANGED" | grep -qFx "$WORKFLOW_FILE"; then
FILTERED="$ALL"
else
FILTERED=$(while IFS= read -r proj; do
[ -z "$proj" ] && continue
if printf '%s\n' "$CHANGED" | grep -q "^${proj}/"; then
echo "$proj"
fi
done <<< "$ALL")
fi
else
FILTERED="$ALL"
fi
MATRIX=$(printf '%s\n' "$FILTERED" | jq -R -s -c 'split("\n") | map(select(. != ""))')
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
`fetch-depth: 0` clones the entire repo history on every workflow run (5 test workflows × every PR + every push + every schedule). For samples-heavy mono-repos this is real I/O wasted on metadata we don't need — we only care about the PR's changed file list, not history. Use `gh api /repos/.../pulls/N/files` instead. Single HTTP call against the compare API, default shallow checkout suffices, no git history fetched at all. Also add `pull-requests: read` to the permissions block in each test workflow so the auto-issued GITHUB_TOKEN has the scope `gh api` needs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the path-aware filter in 58638e9, an RN dependabot bump touching `samples/react-native/<flow>/package.json` no longer triggered the RN Android `assembleDebug` build — the filter checked `^${proj}/` where $proj is the gradlew dir (`samples/react-native/<flow>/android`), and the package.json bump lives one directory up. Compute a watch_root per project by walking up from gradlew until we find a package.json: - For RN samples, watch_root becomes the RN sample root → JS-side changes that affect Android autolinking now correctly trigger the Android build. - For native Android samples, no package.json ancestor exists, so watch_root stays as the gradlew dir → behavior unchanged. This mirrors the same walk-up the workflow's "Install JS dependencies (RN samples)" step already does, keeping discovery and dependency- installation in sync. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Verified via test PR #160 that required status checks (without "Require workflows to succeed") block any PR that doesn't trigger every required workflow — paths-filtered workflows leave their check name in "Expected — waiting for status to be reported" forever.
This PR fixes that by:
paths:filter frompull_request:on every test workflow plusextract.yml. The workflows now always trigger, so the aggregator (<Framework> tests passed,validate) always reports.find-projectspath-aware in each test workflow. It computes the changed paths between the PR base and head withgit diff, then emits only the project directories whose subtree was touched. Empty matrix →test/buildjob skipped → aggregator passes viaresult == 'skipped'.test-js's find with-not -path "*/.angular/*"so Angular's Vite dep cache can't leak into the matrix if workflow ordering ever changes.Behavior matrix
test-jsmatrixtest-androidmatrixtest-iosmatrixREADME.md)[]→ skipped[]→ skipped[]→ skippedsamples/react/login-pkce/**["samples/react/login-pkce"][]→ skipped[]→ skippedsamples/android/**[]→ skipped["samples/android/<flow>"][]→ skippedWhy this approach
GitHub's "Require status checks to pass" rule gates by check name. A workflow filtered out by
paths:doesn't trigger → no check appears → required check stays "Expected" forever. The fix has to make every workflow trigger on every PR; then a cheapfind-projectsstep decides whether there's any real work.🤖 Generated with Claude Code