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**/*.mdanddocs/**andscripts/**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 andgradle/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):
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 underevery, soskippablewas true only if a single changed file was simultaneously underapp/src/test/**and**/*.mdanddocs/**andscripts/**and.claude/**— impossible.skippablewas 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_skippablelists 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_harnessoverride 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)
app/src/test/**androidTest, never packaged into the app or the instrumented APK.**/*.mddocs/***.yml.proposed/ F-Droid metadata); not read by the build or E2E.scripts/**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/**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 andgradle/libs.versions.toml),gradle.properties,app/schemas/**,app/proguard-rules.pro,.github/workflows/**(incl. thisci.yml),.github/scripts/**(SDK/emulator provisioning),.claude/skills/preflight/**(local E2E harness, via thee2e_harnessoverride). Anything not in the skip-list forces E2E — the safe default.Verification
ci.ymlparses; the inlinefilters:block parses to exactly{non_skippable: [...5 negated...], e2e_harness: [preflight]}(inner comments stripped, so they don't break dorny's parse)..claudenon-preflight, root+nested markdown.app/src/main,app/src/androidTest(instrumented — correctly distinct fromapp/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.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 vialocal_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.
Merge Queue Status
2026-07-08 21:00 UTC· Rule:default· triggered by merge protections2026-07-08 21:46 UTC· atb98ae99abcfea249f0eaf2f622683fed23850355· mergeThis pull request spent 45 minutes 27 seconds in the queue, including 24 minutes 43 seconds running CI.
Required conditions to merge
-conflict-draftbase = maincheck-success = CI passedgithub-review-approved[🛡 GitHub repository ruleset rulemain]label != brokencheck-success = Debug buildcheck-neutral = Debug buildcheck-skipped = Debug buildcheck-success = Unit testscheck-neutral = Unit testscheck-skipped = Unit testscheck-success = CI passedcheck-neutral = CI passedcheck-skipped = CI passedmain]:check-success = @github-actions/CI passedcheck-neutral = @github-actions/CI passedcheck-skipped = @github-actions/CI passed