ci(path-filter): skip E2E for scripts/docs/.claude-only PRs (#420) #460

Merged
JMR-dev merged 1 commits from ci-420-path-filter-scripts into main 2026-07-08 21:46:06 +00:00
JMR-dev commented 2026-07-08 20:23:24 +00:00 (Migrated from github.com)

Closes #420.

Root cause (deeper than the issue text)

The skippable set was not missing scripts/** — it was already there (added in #399). The real bug is the dorny/paths-filter semantics.

predicate-quantifier: 'every' makes a filter's per-file predicate "this file matches every pattern in the list", and the filter's boolean output is true iff at least one file satisfies that predicate. The #399 filter listed five positive allow-globs under every, so skippable was true only if a single changed file was simultaneously under app/src/test/** and **/*.md and docs/** and scripts/** and .claude/** — impossible. skippable was therefore always false, and the full E2E matrix ran for every PR since #399 (test-only and docs PRs too), not just scripts-only ones like #419.

Fix

Rewrite the filter the way dorny documents every (their README example uses negated globs): non_skippable lists the same safe allow-globs, each negated. A file forces E2E iff it matches none of them (matches every !-pattern), i.e. it is outside the allow-list. e2e_needed = non_skippable, preserving the fail-safe rule "run E2E unless every changed file is provably irrelevant" — a mixed PR still runs the matrix.

An e2e_harness override forces E2E for .claude/skills/preflight/** (the local instrumented-test harness) even though .claude/** is otherwise skippable.

Skip-list (E2E SKIPPED — provably cannot affect the app build or instrumented tests)

Path Why safe
app/src/test/** JVM unit tests / resources; a separate source set from androidTest, never packaged into the app or the instrumented APK.
**/*.md Markdown docs; never compiled or run.
docs/** Documentation dir (md + inert *.yml.proposed / F-Droid metadata); not read by the build or E2E.
scripts/** Only scripts/device-testing/ — stdlib-Python dev harness + fixtures; never invoked by the CI build or E2E jobs. (This is #420's target.)
.claude/** except .claude/skills/preflight/** Claude Code dev config (agents, hooks, settings, other skills); CI's build/E2E never runs it.

Keep-E2E list (matrix STILL RUNS — anything affecting the build or the E2E harness)

app/src/main/**, app/src/androidTest/**, app/build.gradle.kts, root *.gradle* / settings.gradle*, gradle/** (wrapper and gradle/libs.versions.toml), gradle.properties, app/schemas/**, app/proguard-rules.pro, .github/workflows/** (incl. this ci.yml), .github/scripts/** (SDK/emulator provisioning), .claude/skills/preflight/** (local E2E harness, via the e2e_harness override). Anything not in the skip-list forces E2E — the safe default.

Verification

  • PyYAML: ci.yml parses; the inline filters: block parses to exactly {non_skippable: [...5 negated...], e2e_harness: [preflight]} (inner comments stripped, so they don't break dorny's parse).
  • actionlint: clean (exit 0).
  • dorny-semantics simulation over representative changed-file sets (parsed from the actual committed config, no hardcoded copy):
    • SKIP: scripts-only (#419's exact file set), docs-only, unit-test-only, .claude non-preflight, root+nested markdown.
    • RUN: app/src/main, app/src/androidTest (instrumented — correctly distinct from app/src/test), gradle/libs.versions.toml/schemas, .github/workflows/ci.yml (this PR), .github/scripts/**, .claude/skills/preflight/** (incl. a preflight *.md), and every mixed PR.
  • Self-check (DoD #3): this PR changes only ci.yml, which is outside the allow-list → non_skippable=true → the full matrix runs on this very PR.

Notes / debatable calls (flagging for review)

  • .claude/** blanket skip is broad. Narrowed only by the preflight override. Rest of .claude/ (agents/hooks/settings) is dev-only and CI never runs it during build/E2E, but it is the widest skip entry.
  • .claude/skills/preflight/** forced to RUN: honors the task HAZARD. Strictly, CI's E2E jobs don't invoke the preflight scripts (they're local-only via local_instrumented.py/api37_e2e.py), so this is deliberately conservative rather than strictly necessary.
  • **/*.md: a markdown file rendered at runtime and asserted by an E2E test could in theory be skipped, but no such test exists; markdown is never compiled. Pre-existing from #399, kept as-is.

No app-source/androidTest surface to add a test against; the "test" for a path-filter is a real dorny run, which this PR triggers on itself. Draft-hold as requested — no auto-merge armed.

Closes #420. ## Root cause (deeper than the issue text) The skippable set was **not** missing `scripts/**` — it was already there (added in #399). The real bug is the dorny/paths-filter semantics. `predicate-quantifier: 'every'` makes a filter's **per-file** predicate "this file matches **every** pattern in the list", and the filter's boolean output is true iff **at least one** file satisfies that predicate. The #399 filter listed five **positive** allow-globs under `every`, so `skippable` was true only if a single changed file was simultaneously under `app/src/test/**` **and** `**/*.md` **and** `docs/**` **and** `scripts/**` **and** `.claude/**` — impossible. `skippable` was therefore **always false**, and the full E2E matrix ran for **every** PR since #399 (test-only and docs PRs too), not just scripts-only ones like #419. ## Fix Rewrite the filter the way dorny documents `every` (their README example uses negated globs): `non_skippable` lists the same safe allow-globs, each **negated**. A file forces E2E iff it matches **none** of them (matches every `!`-pattern), i.e. it is outside the allow-list. `e2e_needed = non_skippable`, preserving the fail-safe rule "run E2E unless every changed file is provably irrelevant" — a mixed PR still runs the matrix. An `e2e_harness` override forces E2E for `.claude/skills/preflight/**` (the local instrumented-test harness) even though `.claude/**` is otherwise skippable. ## Skip-list (E2E SKIPPED — provably cannot affect the app build or instrumented tests) | Path | Why safe | |---|---| | `app/src/test/**` | JVM unit tests / resources; a separate source set from `androidTest`, never packaged into the app or the instrumented APK. | | `**/*.md` | Markdown docs; never compiled or run. | | `docs/**` | Documentation dir (md + inert `*.yml.proposed` / F-Droid metadata); not read by the build or E2E. | | `scripts/**` | Only `scripts/device-testing/` — stdlib-Python dev harness + fixtures; never invoked by the CI build or E2E jobs. (This is #420's target.) | | `.claude/**` **except** `.claude/skills/preflight/**` | Claude Code dev config (agents, hooks, settings, other skills); CI's build/E2E never runs it. | ## Keep-E2E list (matrix STILL RUNS — anything affecting the build or the E2E harness) `app/src/main/**`, `app/src/androidTest/**`, `app/build.gradle.kts`, root `*.gradle*` / `settings.gradle*`, `gradle/**` (wrapper **and** `gradle/libs.versions.toml`), `gradle.properties`, `app/schemas/**`, `app/proguard-rules.pro`, **`.github/workflows/**`** (incl. this `ci.yml`), **`.github/scripts/**`** (SDK/emulator provisioning), **`.claude/skills/preflight/**`** (local E2E harness, via the `e2e_harness` override). Anything not in the skip-list forces E2E — the safe default. ## Verification - **PyYAML**: `ci.yml` parses; the inline `filters:` block parses to exactly `{non_skippable: [...5 negated...], e2e_harness: [preflight]}` (inner comments stripped, so they don't break dorny's parse). - **actionlint**: clean (exit 0). - **dorny-semantics simulation** over representative changed-file sets (parsed from the *actual committed* config, no hardcoded copy): - SKIP: scripts-only (#419's exact file set), docs-only, unit-test-only, `.claude` non-preflight, root+nested markdown. - RUN: `app/src/main`, `app/src/androidTest` (instrumented — correctly distinct from `app/src/test`), gradle/`libs.versions.toml`/schemas, `.github/workflows/ci.yml` (**this PR**), `.github/scripts/**`, `.claude/skills/preflight/**` (incl. a preflight `*.md`), and every mixed PR. - **Self-check (DoD #3)**: this PR changes only `ci.yml`, which is outside the allow-list → `non_skippable=true` → the full matrix runs on this very PR. ## Notes / debatable calls (flagging for review) - **`.claude/**` blanket skip** is broad. Narrowed only by the preflight override. Rest of `.claude/` (agents/hooks/settings) is dev-only and CI never runs it during build/E2E, but it is the widest skip entry. - **`.claude/skills/preflight/**` forced to RUN**: honors the task HAZARD. Strictly, CI's E2E jobs don't invoke the preflight scripts (they're local-only via `local_instrumented.py`/`api37_e2e.py`), so this is deliberately conservative rather than strictly necessary. - **`**/*.md`**: a markdown file rendered at runtime and asserted by an E2E test could in theory be skipped, but no such test exists; markdown is never compiled. Pre-existing from #399, kept as-is. No app-source/androidTest surface to add a test against; the "test" for a path-filter is a real dorny run, which this PR triggers on itself. Draft-hold as requested — no auto-merge armed.
mergify[bot] commented 2026-07-08 21:00:44 +00:00 (Migrated from github.com)

Merge Queue Status

This pull request spent 45 minutes 27 seconds in the queue, including 24 minutes 43 seconds running CI.

Required conditions to merge
<!--- DO NOT EDIT -*- Mergify Payload -*- {"version": 1, "state": "merged", "queue_rule_name": "default", "queued_at": "2026-07-08T21:00:42.618225+00:00", "estimated_time_of_merge": null, "speculative_check_pr": null, "required_conditions": []} -*- Mergify Payload End -*- --> # Merge Queue Status - ✅ **Entered queue** — `2026-07-08 21:00 UTC` · Rule: `default` · triggered by merge protections - ✅ **Checks passed** · on draft #462 - ✅ **Merged** — `2026-07-08 21:46 UTC` · at `b98ae99abcfea249f0eaf2f622683fed23850355` · merge This pull request spent **45 minutes 27 seconds** in the queue, including **24 minutes 43 seconds** running CI. <details> <summary>Required conditions to merge</summary> - `-conflict` - [X] #460 - `-draft` - [X] #460 - [X] `base = main` - [X] `check-success = CI passed` - `github-review-approved` [🛡 GitHub repository ruleset rule `main`] - [X] #460 - `label != broken` - [X] #460 - [X] any of [🛡 GitHub branch protection]: - [X] `check-success = Debug build` - [ ] `check-neutral = Debug build` - [ ] `check-skipped = Debug build` - [X] any of [🛡 GitHub branch protection]: - [X] `check-success = Unit tests` - [ ] `check-neutral = Unit tests` - [ ] `check-skipped = Unit tests` - [X] any of [🛡 GitHub branch protection]: - [X] `check-success = CI passed` - [ ] `check-neutral = CI passed` - [ ] `check-skipped = CI passed` - [X] any of [🛡 GitHub repository ruleset rule `main`]: - [X] `check-success = @github-actions/CI passed` - [ ] `check-neutral = @github-actions/CI passed` - [ ] `check-skipped = @github-actions/CI passed` </details>
Sign in to join this conversation.