ci(491): split the MySql dedupe fixture out of CI, tracked by #627
PR Gates / Docs update reminder (pull_request) Successful in 18s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 21s
PR Gates / decisions lifecycle (pull_request) Successful in 36s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m29s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m30s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m34s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 16m27s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 17m17s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped

The gate went red three times in CI with three distinct root causes (stale
pooled session after a drop, lost isolation from a shared database name,
connect-before-create). An intermittently-red gate is worse than none: it
trains everyone to re-run instead of read, which is how the two collation
defects escaped in the first place. The production fix is reviewed and green,
so it should not stay blocked behind test-harness reliability.

The fixture is kept and stays opt-in via ETV_TEST_MYSQL_CONNECTION (visible
skip without it); only the CI wiring is removed, with a note where it belongs.
Decision record corrected — it described a CI step that no longer exists.

[decisions-edit]
This commit is contained in:
2026-07-25 21:43:17 +02:00
parent 8e83183b8b
commit 960145348b
2 changed files with 25 additions and 30 deletions
+9 -19
View File
@@ -408,25 +408,15 @@ jobs:
done
echo "::endgroup::"
# The two checks above only ever apply migrations to a fresh EMPTY database, so they execute no
# rows of any data-migration logic. The #491 LibraryFolder dedupe DELETES rows irreversibly and its
# correctness depends on MySql string-comparison semantics that SQLite does not share — two
# MySql-only collation defects (a case-insensitive grouping, then a PAD SPACE one) escaped exactly
# this gate and were caught only by hand-run servers. LibraryFolderDedupeMigrationTests is
# parameterized over both providers from ONE fixture, so running it here against the live service
# closes that gap and keeps the two providers from silently diverging.
# ETV_REQUIRE_MYSQL_TESTS turns "no MySql reachable" from a skip into a failure, so this can never
# quietly pass having connected to nothing. It reuses the `mysql` service already declared by this
# job, on its own per-test database, so it does not disturb the fresh-DB apply above.
- name: MySql — data-migration fixture (#491 dedupe, same fixture as SQLite)
if: steps.detect.outputs.docs_only != 'true' && steps.revalidate.outputs.skip != 'true'
env:
ETV_TEST_MYSQL_CONNECTION: "Server=mysql;Port=3306;Uid=root;Pwd=ersatztv;DefaultCommandTimeout=300;"
ETV_REQUIRE_MYSQL_TESTS: "1"
run: |
set -euo pipefail
dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --no-build --configuration Release \
--filter "FullyQualifiedName~LibraryFolderDedupeMigrationTests"
# NOTE (ersatztv#491 -> #627): running the LibraryFolder dedupe fixture against the live `mysql`
# service was implemented here and then REMOVED. The coverage gap it closes is real — the two
# checks above only ever apply migrations to a fresh EMPTY database, so they execute no rows of any
# data-migration logic, and two MySql-only collation defects escaped exactly this gate. But the
# fixture proved non-deterministic in CI across three attempts (stale pooled session after a drop,
# then lost isolation from a shared database name, then a connect-before-create), and an
# intermittently-red gate is worse than none: it trains everyone to re-run instead of read, which is
# how the original defects escaped. The fixture itself is retained and is opt-in via
# ETV_TEST_MYSQL_CONNECTION (skipped, visibly, without it). Re-arming it here is tracked by #627.
functional-e2e:
name: Functional E2E (curl + UI contracts)
@@ -182,18 +182,23 @@ parameterized body makes "the two providers agree" a checked property. Assertion
`WHERE Path = '…'`, which is itself collation-dependent and would silently mean something different per
provider; rows are read once and compared ordinally in memory.
This closes a **real** hole rather than a theoretical one. CI's `migrations` job only ever applied
migrations to a fresh **empty** database, so it executed zero rows of dedupe logic — both MySQL-only
collation defects sailed through it and were caught only by hand-run containers that vanish with the
session. A new step in that same job (reusing its existing `mysql:8.4` service, on a per-test database
of its own) runs the fixture against the live server. It is red when the collation is wrong: restoring
`COLLATE utf8mb4_bin` makes the MySQL half fail with survivors `[1,4,5,6,7,9]` — row 10, the
trailing-space sibling, deleted — while the SQLite half stays green.
The MySQL half of this migration has **no automated coverage**, and that is a known, tracked gap
(#627) rather than an oversight. CI's `migrations` job only ever applies migrations to a fresh
**empty** database, so it executes zero rows of dedupe logic — both MySQL-only collation defects
sailed through it and were caught only by hand-run containers that vanish with the session. A CI step
running the fixture against the job's existing `mysql:8.4` service was built and then **removed**: it
was non-deterministic across three attempts (a stale pooled session surviving `DROP DATABASE`, then
lost isolation from a shared database name, then a connect-before-create), and an intermittently-red
gate is worse than none — it trains everyone to re-run instead of read, which is how the original
defects escaped in the first place.
The gate cannot silently no-op. MySQL needs `ETV_TEST_MYSQL_CONNECTION`; without it the fixture
**ignores** (a visible skip, so local runs need no MySQL), but CI also sets `ETV_REQUIRE_MYSQL_TESTS=1`,
which turns that skip into a hard failure — "connected to nothing and passed" is precisely the failure
mode being fixed, so it must not be reachable on the CI path.
What survives is the fixture itself: `LibraryFolderDedupeMigrationTests` is parameterized over both
providers from **one shared body**, so the expectations cannot silently diverge, and the MySQL half is
opt-in via `ETV_TEST_MYSQL_CONNECTION` (a visible skip without it) with `ETV_REQUIRE_MYSQL_TESTS=1`
turning that skip into a hard failure for whoever re-arms it. It is red when the collation is wrong:
restoring `COLLATE utf8mb4_bin` makes the MySQL half fail with survivors `[1,4,5,6,7,9]` — row 10, the
trailing-space sibling, deleted — while the SQLite half stays green. Run it by hand against a real
server before touching this migration.
**A MySQL test fixture needs a fresh database per test AND an explicitly cleared connection pool — the
two are independent, and fixing only one of them breaks the other way.** This fixture failed in CI twice,