Split the API 37 CI leg so the part that works can gate #56

Merged
JMR-dev merged 2 commits from ci/api37-split into main 2026-08-23 22:35:53 +00:00
JMR-dev commented 2026-08-23 21:59:20 +00:00 (Migrated from github.com)

Two commits. The first corrects docs/api-37-emulator-crash.md with what a CI
investigation measured; the second splits the API 37 leg so the part that works can gate.

1. Measure the API 36 control and record what CI cannot measure

Documentation only, plus one wrong figure in api37-debug.yml's own comments.

  • A third measured bullet: API 36 against API 37, back to back, same two tests, same
    renderer, same SystemUI-disable path. 37.0 fails both on c2.goldfish.h264.decoder
    (32660148155);
    36 passes both in 4.603 s with the same decoder in its logcat
    (32660152961).
    That falsifies "the stripped configuration is what breaks these tests". It carries its two
    uncontrolled variables rather than dropping them.
  • The decoder-mechanism bullet is unchanged and still labelled inference. This adds a
    measurement next to it; it retracts nothing.
  • The intact-SystemUI counterfactual is unmeasurable on a GitHub runner. Seven dispatches,
    zero verdicts, with a mechanism rather than bad luck: while the framework crash-loops the
    guest cannot reliably create per-user private directories, so an app installed during the
    loop has no cache dir and the fixture copy dies in @Before before any codec exists. The
    result XML masks this behind an UninitializedPropertyAccessException in tearDown, which
    reads as a defect in this repository and is not one.
  • Abort cadence corrected. "Roughly every 20 s" was the watchdog's sampling interval:
    measured gaps are 20–90 s, median 60–70 s, three to five per run.

2. Split the API 37 leg so the part that works can gate

CI has never run the API level this app targets, because two tests fail on the emulator image
and one row would be permanently red or permanently allow-listed.

job runs gates
E2E API 37 55 of 57 (notAnnotation) yes, must be green
E2E API 37 Media3 hardware transcode (advisory) the other 2 (annotation) no, continue-on-error
  • One marker, @FailsOnEmulatorApi37, drives both. Two lists would drift, and drift is
    silent in both directions — a test in neither job reads as green. Excluding by class was
    not an option: Media3EngineTest has four tests and two of them pass here.
  • The advisory job is named for what it runs. Both its tests drive a full H.264 → H.265
    hardware transcode, which is what distinguishes them from the two Media3EngineTest cases
    that pass — those never decode video. The goldfish-decoder theory is in a comment inside the
    job, where it can be corrected without renaming a check people have learned to look for.
  • The SystemUI disable moves into .github/scripts/e2e-run.sh behind
    E2E_DISABLE_SYSTEM_UI, unset on the other four legs, which therefore run byte-identical
    commands. It runs before the streamed logcat starts: adb shell stop would end that
    logcat and nothing restarts it. The body is probe v2 (three rounds, waits for
    system_server to actually be gone, verifies pm list packages -d, requires a zero-abort
    window) — the weaker one-round probe reported success on a run that then started SystemUI
    eight more times.
  • The caveat is next to the row: this leg runs with SystemUI disabled and the framework
    restarted under it, a configuration no other leg and no Pixel run uses. Anything touching
    system UI must not trust it.

What to check on this run

The count is the discriminator, not the failure count. If notAnnotation silently did not
apply you get 57 tests with 2 failures, which reads as "expected red" and is easy to wave
through.

  • E2E API 37 → tests="55" failures="0" errors="0" skipped="2"
    (grep -rho '@Test' app/src/androidTest | wc -l = 57 on this branch, minus the 2 marked)
  • advisory → tests="2" failures="2"
  • API 33–36 → unchanged.

Follow-up, deliberately NOT done here

E2E API 37 is not yet a required check. Adding a required context that does not exist on
main blocks every PR, so the ruleset change has to come after this merges:

ruleset 21117412 (JMR-dev/LibreMediaConverter, "main")
  rules[].type == required_status_checks
  → add { "context": "E2E API 37", "integration_id": 15368 }

The advisory job must not be added — it is continue-on-error by design.

CLAUDE.md is untouched. Its "CI's matrix therefore stops at API 36" clause is now false;
that correction is parked in the doc's existing "Correction owed to CLAUDE.md" section
alongside two others already waiting.

🤖 Generated with Claude Code

Two commits. The first corrects `docs/api-37-emulator-crash.md` with what a CI investigation measured; the second splits the API 37 leg so the part that works can gate. ## 1. `Measure the API 36 control and record what CI cannot measure` Documentation only, plus one wrong figure in `api37-debug.yml`'s own comments. - **A third measured bullet**: API 36 against API 37, back to back, same two tests, same renderer, same SystemUI-disable path. 37.0 fails both on `c2.goldfish.h264.decoder` ([32660148155](https://github.com/JMR-dev/LibreMediaConverter/actions/runs/32660148155)); 36 passes both in 4.603 s with the same decoder in its logcat ([32660152961](https://github.com/JMR-dev/LibreMediaConverter/actions/runs/32660152961)). That falsifies "the stripped configuration is what breaks these tests". It carries its two uncontrolled variables rather than dropping them. - The decoder-mechanism bullet is **unchanged** and still labelled inference. This adds a measurement next to it; it retracts nothing. - **The intact-SystemUI counterfactual is unmeasurable on a GitHub runner.** Seven dispatches, zero verdicts, with a mechanism rather than bad luck: while the framework crash-loops the guest cannot reliably create per-user private directories, so an app installed during the loop has no cache dir and the fixture copy dies in `@Before` before any codec exists. The result XML masks this behind an `UninitializedPropertyAccessException` in `tearDown`, which reads as a defect in this repository and is not one. - **Abort cadence corrected.** "Roughly every 20 s" was the watchdog's *sampling* interval: measured gaps are 20–90 s, median 60–70 s, three to five per run. ## 2. `Split the API 37 leg so the part that works can gate` CI has never run the API level this app targets, because two tests fail on the emulator image and one row would be permanently red or permanently allow-listed. | job | runs | gates | |---|---|---| | `E2E API 37` | 55 of 57 (`notAnnotation`) | yes, must be green | | `E2E API 37 Media3 hardware transcode (advisory)` | the other 2 (`annotation`) | no, `continue-on-error` | - **One marker, `@FailsOnEmulatorApi37`**, drives both. Two lists would drift, and drift is silent in both directions — a test in neither job reads as green. Excluding by *class* was not an option: `Media3EngineTest` has four tests and two of them pass here. - **The advisory job is named for what it runs.** Both its tests drive a full H.264 → H.265 hardware transcode, which is what distinguishes them from the two `Media3EngineTest` cases that pass — those never decode video. The goldfish-decoder theory is in a comment inside the job, where it can be corrected without renaming a check people have learned to look for. - **The SystemUI disable moves into `.github/scripts/e2e-run.sh`** behind `E2E_DISABLE_SYSTEM_UI`, unset on the other four legs, which therefore run byte-identical commands. It runs **before** the streamed logcat starts: `adb shell stop` would end that logcat and nothing restarts it. The body is probe v2 (three rounds, waits for `system_server` to actually be gone, verifies `pm list packages -d`, requires a zero-abort window) — the weaker one-round probe reported success on a run that then started SystemUI eight more times. - **The caveat is next to the row**: this leg runs with SystemUI disabled and the framework restarted under it, a configuration no other leg and no Pixel run uses. Anything touching system UI must not trust it. ## What to check on this run The count is the discriminator, not the failure count. If `notAnnotation` silently did not apply you get 57 tests with 2 failures, which reads as "expected red" and is easy to wave through. - `E2E API 37` → **`tests="55" failures="0" errors="0" skipped="2"`** (`grep -rho '@Test' app/src/androidTest | wc -l` = 57 on this branch, minus the 2 marked) - advisory → **`tests="2" failures="2"`** - API 33–36 → unchanged. ## Follow-up, deliberately NOT done here `E2E API 37` is not yet a *required* check. Adding a required context that does not exist on `main` blocks every PR, so the ruleset change has to come **after** this merges: ``` ruleset 21117412 (JMR-dev/LibreMediaConverter, "main") rules[].type == required_status_checks → add { "context": "E2E API 37", "integration_id": 15368 } ``` The advisory job must **not** be added — it is `continue-on-error` by design. `CLAUDE.md` is untouched. Its "CI's matrix therefore stops at API 36" clause is now false; that correction is parked in the doc's existing "Correction owed to `CLAUDE.md`" section alongside two others already waiting. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
JMR-dev commented 2026-08-23 22:16:43 +00:00 (Migrated from github.com)

CI result — run 32669190757, conclusion success

The count was the thing to check, and it is right:

check result XML conclusion
E2E API 37 tests="55" failures="0" errors="0" skipped="2" time="24.261" success
E2E API 37 Media3 hardware transcode (advisory) tests="2" failures="2" errors="0" skipped="0" failure, continue-on-error
E2E API 33 / 34 / 35 / 36 Starting 57 tests each — the filter is inert there success
Unit tests, Static analysis, FFmpeg binary — success

55 = 57 − 2, so notAnnotation applied. Had it silently not applied, this would have been 57
tests with 2 failures, which reads as "expected red".

continue-on-error behaves as intended: the advisory check run stays visibly failure while
the workflow run conclusion is success, so it reports without blocking.

Both advisory failures are the known signature and nothing new — name=c2.goldfish.h264.decoder,
dequeueOutputBuffer(MediaCodec.java:4274).

The relocated disable works, and the hardening earned its keep immediately

E2E API 37, round 1:

--- SystemUI disable, round 1 ---
  pm attempt 1: Package com.android.systemui new state: disabled-user
  system_server down after ~16 s
  services back after ~10 s
  verified: com.android.systemui is in pm list packages -d
  abort rate, SystemUI disabled: 0 new in 45 s (total 2)
  final state: SystemUI disabled
Starting 55 tests on test(AVD) - 17
BUILD SUCCESSFUL in 2m 54s

The advisory job needed two rounds on this very run — the first did not verify — which is
exactly the case the one-round probe used to report as success. Cost: ~7 min for the whole job,
against ~6 at API 36.

The logcat-api37.txt artifact is 10.5 MB and its last entry is 22:06:14, one second after the
test XML's timestamp, with 208 TestRunner lines. That confirms the reason the disable runs
before the stream starts: it still covers the Gradle window rather than stopping at
adb shell stop.

Independent second measurement

Dispatch 32669193446 ran
the same notAnnotation filter at API 37 through api37-debug.yml's own probe path instead of the
new e2e-run.sh one: Starting 55 tests, BUILD SUCCESSFUL. So the filter and the relocated
disable are each confirmed by a run that does not depend on the other.

One flake, not this change

E2E API 35 failed on the first attempt with Instrumentation run failed due to Process crashed
after 8 tests. Its log shows Starting 57 tests, i.e. no filter reached it, and nothing in this
diff touches app/src/main or that leg's runtime path. It passed on rerun.

Tooling run against this diff

actionlint 1.7.12 (all three workflows), shellcheck 0.10.0 (e2e-run.sh, clean), PyYAML
structural check, and locally compileDebugAndroidTestKotlin + ktlintCheck + detekt +
lintDebug — all green.

## CI result — [run 32669190757](https://github.com/JMR-dev/LibreMediaConverter/actions/runs/32669190757), conclusion **success** The count was the thing to check, and it is right: | check | result XML | conclusion | |---|---|---| | `E2E API 37` | `tests="55" failures="0" errors="0" skipped="2" time="24.261"` | success | | `E2E API 37 Media3 hardware transcode (advisory)` | `tests="2" failures="2" errors="0" skipped="0"` | failure, `continue-on-error` | | `E2E API 33` / `34` / `35` / `36` | `Starting 57 tests` each — the filter is inert there | success | | `Unit tests`, `Static analysis`, `FFmpeg binary` | — | success | 55 = 57 − 2, so `notAnnotation` applied. Had it silently not applied, this would have been 57 tests with 2 failures, which reads as "expected red". **`continue-on-error` behaves as intended**: the advisory check run stays visibly `failure` while the workflow run conclusion is `success`, so it reports without blocking. Both advisory failures are the known signature and nothing new — `name=c2.goldfish.h264.decoder`, `dequeueOutputBuffer(MediaCodec.java:4274)`. ### The relocated disable works, and the hardening earned its keep immediately `E2E API 37`, round 1: ``` --- SystemUI disable, round 1 --- pm attempt 1: Package com.android.systemui new state: disabled-user system_server down after ~16 s services back after ~10 s verified: com.android.systemui is in pm list packages -d abort rate, SystemUI disabled: 0 new in 45 s (total 2) final state: SystemUI disabled Starting 55 tests on test(AVD) - 17 BUILD SUCCESSFUL in 2m 54s ``` The advisory job needed **two** rounds on this very run — the first did not verify — which is exactly the case the one-round probe used to report as success. Cost: ~7 min for the whole job, against ~6 at API 36. The `logcat-api37.txt` artifact is 10.5 MB and its last entry is `22:06:14`, one second after the test XML's timestamp, with 208 `TestRunner` lines. That confirms the reason the disable runs *before* the stream starts: it still covers the Gradle window rather than stopping at `adb shell stop`. ### Independent second measurement [Dispatch 32669193446](https://github.com/JMR-dev/LibreMediaConverter/actions/runs/32669193446) ran the same `notAnnotation` filter at API 37 through `api37-debug.yml`'s own probe path instead of the new `e2e-run.sh` one: `Starting 55 tests`, `BUILD SUCCESSFUL`. So the filter and the relocated disable are each confirmed by a run that does not depend on the other. ### One flake, not this change `E2E API 35` failed on the first attempt with `Instrumentation run failed due to Process crashed` after 8 tests. Its log shows `Starting 57 tests`, i.e. no filter reached it, and nothing in this diff touches `app/src/main` or that leg's runtime path. It passed on rerun. ### Tooling run against this diff `actionlint 1.7.12` (all three workflows), `shellcheck 0.10.0` (`e2e-run.sh`, clean), PyYAML structural check, and locally `compileDebugAndroidTestKotlin` + `ktlintCheck` + `detekt` + `lintDebug` — all green.
JMR-dev commented 2026-08-23 22:29:18 +00:00 (Migrated from github.com)

Force-pushed: three prose over-claims corrected, then re-run clean

A review pass caught three places where the new text outran its evidence — the exact failure mode
commit 1 exists to stop, and one this file has already been corrected for three times
(792286a, 775a447, 961cfa7). Corrected in the commit that introduced each, so both commits
still stand alone. No behavioural change: the tree is otherwise byte-identical to the one that
produced the green run above.

  1. The CI environment line claimed one system image for a table whose control row is API 36.
    c2 is the API 36 control and runs that level's own image — which is the point of it. Now stated
    per-row instead of blanket.
  2. "the suite runs under either renderer once SystemUI is gone" had no row behind it. The
    swangle+disabled cell is c5 (32645543238, 57/2/0/2); it had been trimmed out of the table
    while the claim it supported stayed. c5 is back.
  3. "the same SystemUI-disable path" was claimed across API 33–36, which never ran it.
    E2E_DISABLE_SYSTEM_UI is empty on those legs and run-e2e.sh gates its local equivalent on
    37 | 37.*. Only the API 36 control (32660152961) ran that path. Both the doc bullet and the
    advisory job's comment now say the accurate — and stronger — thing: pass at API 36 under the
    same renderer and the same disable path, and pass at 33–36 without needing that path at all,
    because nothing below 37 has the bug it works around.

Also added, not blocking: E2E API 37 Media3 hardware transcode (advisory) going green is now a
"When to revisit" trigger in the doc. continue-on-error means nothing announces it — the
image fixing itself would look exactly like a check nobody reads quietly ceasing to be red.

Run 32670321869 — conclusion success, no reruns

--- SystemUI disable, round 1 ---
  verified: com.android.systemui is in pm list packages -d
  abort rate, SystemUI disabled: 0 new in 45 s (total 2)
  final state: SystemUI disabled
Starting 55 tests on test(AVD) - 17
BUILD SUCCESSFUL in 2m 57s

All eight other checks green including E2E API 35, which confirms its earlier failure was the
flake it looked like. The advisory check is red by design and does not block: the run conclusion
is success.

## Force-pushed: three prose over-claims corrected, then re-run clean A review pass caught three places where the new text outran its evidence — the exact failure mode commit 1 exists to stop, and one this file has already been corrected for three times (`792286a`, `775a447`, `961cfa7`). Corrected in the commit that introduced each, so both commits still stand alone. **No behavioural change: the tree is otherwise byte-identical to the one that produced the green run above.** 1. **The CI environment line claimed one system image for a table whose control row is API 36.** c2 is the API 36 control and runs that level's own image — which is the point of it. Now stated per-row instead of blanket. 2. **"the suite runs under either renderer once SystemUI is gone" had no row behind it.** The swangle+disabled cell is c5 (`32645543238`, 57/2/0/2); it had been trimmed out of the table while the claim it supported stayed. c5 is back. 3. **"the same SystemUI-disable path" was claimed across API 33–36, which never ran it.** `E2E_DISABLE_SYSTEM_UI` is empty on those legs and `run-e2e.sh` gates its local equivalent on `37 | 37.*`. Only the API 36 control (`32660152961`) ran that path. Both the doc bullet and the advisory job's comment now say the accurate — and stronger — thing: *pass at API 36 under the same renderer and the same disable path, and pass at 33–36 without needing that path at all, because nothing below 37 has the bug it works around.* Also added, not blocking: `E2E API 37 Media3 hardware transcode (advisory)` going green is now a **"When to revisit"** trigger in the doc. `continue-on-error` means nothing announces it — the image fixing itself would look exactly like a check nobody reads quietly ceasing to be red. ### [Run 32670321869](https://github.com/JMR-dev/LibreMediaConverter/actions/runs/32670321869) — conclusion **success**, no reruns ``` --- SystemUI disable, round 1 --- verified: com.android.systemui is in pm list packages -d abort rate, SystemUI disabled: 0 new in 45 s (total 2) final state: SystemUI disabled Starting 55 tests on test(AVD) - 17 BUILD SUCCESSFUL in 2m 57s ``` All eight other checks green including `E2E API 35`, which confirms its earlier failure was the flake it looked like. The advisory check is red by design and does not block: the run conclusion is `success`.
Sign in to join this conversation.