efadbec297220560cf32bf4db8c02ee82cb8878d
58
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a7d91bf15a |
fix(876): sweep session narrative out of hooks, workflows, scripts, tests and code comments; grow the detector to the process corpus
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 35s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 57s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 37s
PR Gates / Docs update reminder (pull_request) Successful in 1m0s
PR Gates / decisions lifecycle (pull_request) Successful in 20s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 17s
review-verdict/h10 Review-verdict: MERGEABLE @ a7d91bf (base: main)
Review verdict / Set review-verdict status (pull_request_target) Successful in 45s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m17s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 19m27s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m4s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
`docs.no-session-narrative` reaches every durable artifact, but its detector scanned only `docs/**/*.md` and root markdown, and nothing had ever swept the rest. The issue named four sites from one grep and called them a floor. Deriving the population instead — a whitespace-joined sweep over every tracked file outside the detector, for the detector's own phrasings plus the attribution and review-round class #812 found — gave 453 sites in 108 files at `fb5592971`, and a second pass for phrasings the first list missed (hyphenated `round-N`, "an earlier version", "the reviewer proved") added residuals in the same files. Every site was classified with #812's three dispositions (CUT / SEVER / KEEP with its sub-kind) under the who-benefits test; the per-site manifests are on the PR. The rejected designs, tested-and-rejected fixtures, measurements and traps stay; the attribution of who found them and the round in which they were found go. The detector's population grows to `.claude/`, `.gitea/`, `.husky/` and `scripts/` regardless of extension, minus the detector and its own test (whose fixtures ARE the phrasings) and minus `scripts/tests/fixtures/` (test data, including decision-record copies — the same reasoning as the records' own exemption, and what keeps the record's depth measurement true), and `--all` lists tracked REGULAR files only — a symlink's content is its target and a gitlink has none. The #812 argument for leaving `docs/superpowers/**` in the population runs the other way here: `--diff` sees only ADDED lines, and 287 of the 453 sites were under 30 days old — this corpus is where narrative is being added, so the advisory nudge has reach. Density agrees: 56 line-mode hits over the 113 regular files the predicate admits, against 9 over 66 docs files before #812. `web/` and C# stay out on the same measurement (3 of 74 PATTERNS-matching sites, ~4,600 files). The predicate did not grow: PATTERNS matched 74 of 453 sites, and widening the word list to the attribution class is the treadmill the withdrawn parity test ran on. The population oracle is restated over segments with the new arms, the synthetic cross product gains the process heads and non-markdown extensions, a fixture witnesses that a tracked symlink is neither scanned nor counted, a `.py.bak` axis separates a by-name exemption from a `startswith` over the same tuple, and eight mutants (drop the process arm, drop the by-name exemption, exempt by `startswith`, drop or add a prefix, drop the fixtures exemption, list only markdown, drop the symlink filter, test the mode per row instead of per path) each redden it. A pre-existing silent drop in `--diff` goes with it: git tab-terminates a `+++` filename that contains a space, and the kept tab made `is_scanned_path` refuse the file with no notice — fixed, with a positive control and its own mutant. Code is unchanged by construction, measured per file type against `origin/main`: Python modules are AST-equal with docstrings stripped, except `#` lines inside the embedded fixture programs (string literals) of three test modules; workflows differ only in `#` lines inside `run:` block scalars; shell, C#, TypeScript and jq are equal with comment lines stripped. The stated exceptions: the detector and its test, 26 vitest titles that carried review-round or severity labels or a reviewer attribution (call sites whose title changed — every changed title line walked back to its `it(` / `it.each(...)(` anchor, so a `' + '` concatenation counts once), two registry note strings and the mutation manifest's prose fields. scripts/tests: 1565 passed. Web: lint, typecheck, 1319 tests green. Closes #876. Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEcBoFw7ctrf3Nb7R7x7wk |
||
|
|
94a3d13495 |
fix(836): never pass --depth to a checkout that may already be complete (#884)
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 11s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 32s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 16m47s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 9m7s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 8m7s
Build ErsatzTV Image / Build & push image (amd64) (push) Failing after 2m0s
`git fetch --depth=N` grafts a complete clone shallow. `scripts/ci-detect-docs-only.sh` applied a depth chosen for its three `fetch-depth: 2` consumers to `build`'s `fetch-depth: 0` checkout, so the `git describe --tags` in the next step found no reachable tag and a `|| echo v0.0.0` fallback turned that into a version: every `:latest` image shipped `InformationalVersion 0.0.0-<sha>` from 2026-07-17 (#416) until now. Both fetch sites now go through `fetch_ref`, which passes `--depth` only when the checkout is already shallow. `Compute version and tags` fails the job instead of defaulting, so no `:latest` is published rather than a mislabelled one; releases are unaffected because the tag path never calls `describe`. Ships a guard that drives the real script over real `file://` clones with a negative control, a declared clause mutation, and a decision record `ci.fetch-depth-never-grafts-a-complete-clone`. fixes #836 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
4b3f6f6c90 |
fix(786,789): workflows declare their own per-job metadata; guard populations derive from it (#872)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 7s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m41s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m21s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m58s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m8s
Closes #786 and #789, bundled because working either alone would build the artifact the other removes. Every job in all six tracked workflows declares `env.CI_JOB_ROLE` (guard/report-only/none); the `docker-build.yml` jobs also declare `env.CI_EXECUTION_CLASS` (toolchain/bare-runner). Both guard populations derive from those markers; the `TOOLCHAIN_JOBS`/`BARE_RUNNER_JOBS` literals are deleted. A missing or unrecognised marker is a hard failure in both checkers. #789's literal had a real justification — set equality between two DERIVED sets is blind to a member leaving both at once — so the marker is the anchor that replaces it, and the cost (proximity to the `container:` block) is paid by a THIRD derivation from each job's own steps, which is also the only check that sees the failure #789 filed: a .NET step moved into a bare-runner job, where no set changes. The residual is disclosed: drop the block, flip the marker AND hide the tool behind a script and all three go blind, bounded by the failure mode being a loud missing-binary crash. #786's guard jobs join a machine-checked population: a new `test_workflow_job_guards.py` asserts set equality both ways against a new "Workflow-job guards" table, and the four jobs with no dropped-step guard each carry a recorded decision. Two issue claims were refuted by measurement: #789's "editing docker-build.yml re-points the pin" (the pathspec is `docker/ci` only) and #786's job count (17, not 15). Four cold adversarial review rounds across two model families; rounds 1-3 BLOCKED, all findings fixed and each fix demonstrated by reproducing the reviewer's own test. The recurring defect class was prose drifting from code, including a mechanism claim in the decision record that execution refuted. All five mutation proofs redden when their shipped detector is disarmed. New decision record: `testing.workflow-declares-its-own-job-metadata`. Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
5fb9c8537a |
docs(747): re-verify the Gitea 1.25.4-pinned CI claims on 1.27.1, and measure the merge-gate semantics that were source-attested (#867)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 10s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 17s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m20s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m25s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m48s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m10s
Population derived from `git ls-files`, not the issue's 9-key list (~21 claim sites). Re-confirmed unchanged on 1.27.1: the distinct `skipped` commit-status state; `compare` serving no `files`; no agent-side cancel route (REST route + swagger only); `branches: [main]` suppressing the run off a non-main base. Newly measured on four throwaway scratch bases, `main`'s rule never PATCHed: an absent required context blocks an ORDINARY merge without needing `block_admin_merge_override` (that field governs the FORCE path only), and `enable_bypass_allowlist` with an empty list is NOT a substitute for it. Trap recorded: the PR API reports `mergeable: true` while such a merge is refused. Left explicitly dated with reasons: push-supersession auto-cancel, `pull_request_target` overlap, `--depth=1` no-merge-base, and the scope-enum/`reqRepoWriter`/403 items. Not a corpus sweep, and `ci.actions-credential-scoping` now says so. `review-verdict.yml` untouched — #763 holds that file. Five adversarial review rounds (21/12/9/6/2). Caveat: all same-model-family; Codex was rate-limited. fixes #747 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
8aebba4d89 |
fix(748): declare permissions: on all six workflows, and prove the declaration binds (#860)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 7s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 18s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m11s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m4s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m20s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
f2551b778e |
fix(746): drop the persisted checkout credential; unmask the base-ref fetches (#842)
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 24s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 30s
Build ErsatzTV Image / Build & test (.NET) (push) Failing after 1m37s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m14s
Build ErsatzTV Image / Build & push image (amd64) (push) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m1s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
5ba442c11c |
fix(772,792): name the missing toolchain image, and stop a refusal leaving a verdict comment
#772 — the pinned CI toolchain image can be deleted out from under us, and when it was (2026-08-11..13) all five `container:` jobs died at image pull, both required contexts included, with the cause buried in each job's log. Root cause is registry-side and is now established rather than guessed: an owner-level Gitea package cleanup rule (keep_count 15, remove_days 1, remove_pattern `.*`, keep_pattern no 7-hex sha can match) deletes a sha tag once 15 newer versions exist, and `ExecuteCleanupRules` ran nightly through the window. The `ersatztv` package carries the same rule's fingerprint exactly — every sha tag older than the 15-slot window is gone, every keep_pattern tag back to 26.3.1 survives. Version deletes leave no audit row, so the specific run cannot be replayed; that limit is stated where the claim is made. The durable fix belongs to the registry's repo: server-management#842. What lands here is what a consumer of someone else's registry can do: * `toolchain-preflight`, a container-free job (a job consuming the image could not run to report it missing) resolving every pin against the registry and failing with a message that names the tag and the recovery. Not a `needs:` of the jobs it diagnoses — gating five jobs behind a checkout and one curl taxes every green run to speed up a rare red one, and they already fail fast. * Only HTTP 404 means gone. Everything else is could-not-tell, and rejected credentials fail rather than pass as unknown — "the check could not run" must never present as "the pin is fine". * A recovery path that does not need CI: rebuild the SAME tag from the commit it names and push it. The push half was verified against this registry on 2026-08-22 with a throwaway package (created, resolved 200, deleted). #792 — the reported defect was the exit code, and re-measuring says that premise is false: every no-status path already exits 1, and eight refusal modes now assert it against the real predecessor, where they pass. The observed 0 came from the invocation, not the script. What WAS broken is the half-state the issue describes second: the comment was written before the status, so every refusal left `Review-verdict: MERGEABLE @ <head>` on a PR with no gating status behind it. The two writes are now ordered status-then-comment, which makes the only reachable half-state the safe one — a status with no comment leaves the merge hook's condition (c) with nothing to classify, which is an `ask`. The refusals themselves are untouched. Ordering rather than compensating deletion: an orphaned-comment cleanup needs a Gitea call, and these refusals are usually caused by Gitea being unreachable. Proof for the ordering is the split against origin/main's script: the 8 orphan/ordering tests go red there, the 8 exit-code tests stay green. fixes #772 fixes #792 Refs: server-management#842 Decisions-Edit: yes |
||
|
|
cfdab63a3a |
fix(767): gate the release path on the delimiter ban with a prerequisite job (#770)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 19s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m40s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m30s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m27s
The delimiter ban protecting `build`'s `Smoke + IPTV E2E` was enforced only by a pytest in `script-tests` — `on: pull_request`, not a required context — so nothing re-checked it on a `v*` tag push, which is exactly when the candidate image is published. A `scan` job now runs the ban test and `build` lists it in `needs:`, so a red `scan` skips `build` and no image is built. Measured both directions without cutting a release: run 1928 (poisoned Smoke) → scan failed, `Build & push` skipped; run 1929 (control) → scan green, build ran. The gate rests on three different KINDS of check, because each single kind was defeated in review: the ban test; an execution probe against a poisoned copy with all three `env:` tiers layered; and `scripts/ci-prove-ban-detects.sh`, which is not a test — it poisons the real checkout and vouches only for the ban test's `build` parametrisation failing. Eight review rounds; rounds 1-5 each found a real defect in the previous fix. Refs: #767 Decisions-Edit: yes Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
884ac8a7e9 |
fix(756): extend the dropped-step guard to docker-build.yml's required jobs, where a drop is fail-OPEN (#768)
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m0s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m24s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Failing after 6m8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m42s
A `run:` body the runner declines to interpolate is dropped, and the job still concludes `success` (#751). #751 fixed that in review-verdict.yml, where the failure is fail-CLOSED. This closes the two places where it is fail-OPEN: `Build & test (.NET)` and `EF migration integrity (SQLite + MySql)` are the other two required contexts on `main`, so a dropped step there sends a required check green having done no work. Per-STEP markers, not per-job as proposed: a marker on the first step only proves the job began, while the drop that costs something is `Test`, `Build` or a migration replay. The trailing guard carries no `if:` — with a dozen steps, `always()` would announce a false "these steps never executed" on every ordinary red build; the default `success()` is correct because guard-skipped implies job-red. Plus a ban on the raw `${{` opener in `test`, `migrations` and `build`, which makes the class unreachable rather than merely caught. `build` is included because its Smoke step runs AFTER the image is pushed. Measured live on the build lane in both directions: probe #765 (drop caught, sole failure in the job) and #766 (a failing continue-on-error step does not skip the guard). 510 tests, 30 mutations killed across two harnesses, five cold review rounds across two model families. Residual tracked as #767: the `build` ban is review-time only, not fail-closed on the release path. fixes #756 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
960145348b |
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] |
||
|
|
83cd36e0de |
test(491): run the dedupe fixture against MySql in CI; correct the collation claim
The dedupe DML had zero automated coverage on MySql: the migrations job only applies migrations to a fresh EMPTY database, so no dedupe row ever executed there. Two MySql-only collation defects escaped that gate in this session and were caught only by hand-run containers. Parameterize LibraryFolderDedupeMigrationTests over both providers from ONE fixture body - same seeded rows, same expected survivors - rather than adding a MySql-only copy that would drift and recreate the gap. Assertions no longer use WHERE Path = '...', which is itself collation-dependent and would quietly mean something different per provider; rows are read once and compared ordinally in memory. A new step in the existing migrations job runs it against that job's mysql:8.4 service, on a per-test database of its own. Proven red when the collation is wrong: restoring COLLATE utf8mb4_bin fails the MySql half with survivors [1,4,5,6,7,9] - the trailing-space sibling deleted - while SQLite stays green. Proven non-skippable: without ETV_TEST_MYSQL_CONNECTION the fixture ignores visibly, and with ETV_REQUIRE_MYSQL_TESTS=1 (which CI sets) that skip becomes a hard failure, so it cannot pass having connected to nothing. Local runs need no MySql. Also correct an overstated comment. The schema pins only the utf8mb4 charset, never a collation, so the effective comparison is the server default: always case-insensitive, but PAD SPACE only on utf8mb4_general_ci - 8.4's default utf8mb4_0900_ai_ci is NO PAD, verified on the real column. The migration bug was independent of that because the old code applied an EXPLICIT utf8mb4_bin, which is PAD SPACE everywhere; the runtime simply tolerates both. Refs #488 #308 fix #491 |
||
|
|
9798877631 |
ci(445): re-point the toolchain pin after the second rebase (1652fc5 -> 32747a0)
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 16s
PR Gates / Docs update reminder (pull_request) Successful in 16s
PR Gates / decisions lifecycle (pull_request) Successful in 18s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m58s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 23s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m34s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Completes the second pin-recovery cycle. main moved twice during this branch's review and each rebase rewrites the sha of the commit that touched docker/ci, so the pin has to be re-pointed each time. Simulated the guard's full logic before pushing, including the length check main added in #598 (which came from this session's #594): length = 7 (guard requires exactly 7) expected = git log -1 --format=%H -- docker/ci .gitea/workflows/ci-image.yml = |
||
|
|
e7bac06d1b |
ci(445): re-point the toolchain pin after the rebase (e9fd26f -> 1652fc5)
Second half of the post-rebase pin recovery. The preceding commit touched
docker/ci, so ci-image.yml tagged it and published ersatztv-ci:1652fc5; this
commit points all five container jobs (plus the header comment) at it.
Simulated ci-image-pin's own logic before pushing rather than guessing:
expected = git log -1 --format=%H -- docker/ci .gitea/workflows/ci-image.yml
= 1652fc568e53184c92c7e0bc5a41546aed19744d
pin = 1652fc5 -> resolves to the same commit
=> WOULD PASS
Verified the published image is the one CI will actually consume: pulled
:1652fc5 on the runner host, /ms-playwright holds chromium_headless_shell-1234,
chromium launches (151.0.7922.34), dotnet 10.0.302 intact.
Refs #445
|
||
|
|
d8c0b3e752 |
feat(445,533): headless Playwright UI-E2E flows + fix e2e-local readiness probe [decisions-edit]
Adds the last deferred #299/#363 follow-up: the flows that CANNOT be expressed
as curl calls. Scope rule (the durable part) — assert only what the curl
harness structurally cannot reach:
1. client-side form validation (the Setup confirm-password gate is pure React
state and makes no request, so there is no HTTP contract to assert)
2. AuthGate's RENDERED states (Setup vs Login vs app)
3. the session cookie authenticating the SPA's OWN /api XHRs — curl proves the
cookie works for curl, not that the app sends it
4. sign-out through the UserMenu back to the login gate
New: web/e2e/boot-gate.spec.ts, web/playwright.config.ts, scripts/e2e-ui.sh
(owns the whole lifecycle: fresh config dir -> boot -> specs -> always kill).
Runs as a second step of the EXISTING advisory `functional-e2e` job rather than
a new job: the dominant cost there is `npm ci` + the Release build, both already
done, so this adds ~5s instead of duplicating a heavy job. It boots its own
fresh instance on port 8410 because the first spec asserts the one-shot Setup
gate that the curl step has already claimed on its config dir.
Determinism (the issue asked for it explicitly): `serial`, `workers: 1`,
`retries: 0` even in CI — a retry would let a flaky flow merge looking green.
Measured 5 consecutive clean runs, ~2s each.
Pins all five `container:` jobs to the toolchain image built by the preceding
commit, which bakes `chromium-headless-shell`.
Non-obvious coupling fixed: vitest's default include glob would have collected
web/e2e/*.spec.ts and run it under jsdom. Excluded `e2e/**` by spreading
`configDefaults.exclude` rather than narrowing `include` to `src/**`, because
web/scripts/ holds a real vitest test an src-only include would silently stop
running.
`RebuildSearchIndexHandler` logs one of two mutually-exclusive lines just before
`SystemStartup.SearchIndexIsReady()`:
fresh config -> "Done migrating search index in {Duration}"
reused config -> "Search index is already version {Version}"
The probe watched only the first, so a reused dir waited out the full 120s
timeout and then killed a perfectly healthy server. Widened to a `grep -Eq`
alternation; the handler's if/else is exhaustive, so the pair covers every path
to readiness.
Verified with a negative control: on a reused dir the server is ready in 2s via
the "already version" line, and the OLD probe string is genuinely ABSENT from
that run's log — so the old code would have hung, i.e. the fix is load-bearing
rather than incidentally passing.
The "prefer a fresh config dir" guidance stays: that guards state bleed, which
is a separate concern from the probe hanging.
- `wait "$PID"` in the cleanup trap was a NO-OP: the server is a grandchild
(launched in e2e-local.sh's subshell, which then exits), so `wait` fails
instantly and was swallowed by `|| true` — cleanup did not actually ensure the
port was released, exactly what its comment claimed. Replaced with a bounded
`kill -0` poll, then SIGKILL.
- Added a port pre-flight check: previously an occupied port surfaced as a 120s
readiness timeout that reads like a broken build. Now fails in 0s naming the
PIDs, and warns against blanket-killing `dotnet ErsatzTV.dll` (that reaps
other sessions' servers).
- UI-E2E: 5x clean (3 specs, ~2s); back-to-back runs pass with no manual cleanup
- curl harness unaffected by the boot-script change: 45/45 PASS
- web: 983 tests / 105 files green; typecheck + lint clean
- vitest collection verified: excludes web/e2e, still collects web/scripts
- Dockerfile sequence + browser launch validated verbatim in a container on the
real amd64 base before committing; chromium launches as root with NO sandbox
opt-out needed
- decisions validator green; catalog regenerated
- docs/decisions.md TOC repaired: it had drifted to 69 of 97 records and held a
dangling anchor to the #72 record that #415 superseded into archive/.
Regenerated with a generator validated against the 68 existing anchors (0
mismatches) -> 97/97, no dangling, no duplicates.
Docs: docs/e2e-local.md (new "UI-E2E harness" section), docs/ci-cd.md (toolchain
image + UI-E2E step), docs/testing.md, docs/README.md, docs/decisions.md
(new `ci.ui-e2e-harness` record; `ci.functional-e2e-harness` amended — its Rule
said "curl-only", now accurate).
Refs #445 #533
|
||
|
|
b4ac46fce1 |
ci(535): split PR-only git gates into pr-checks.yml so release tags don't red
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 9s
PR Gates / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 12s
PR Gates / decisions lifecycle (pull_request) Successful in 17s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m24s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 16m39s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 19m49s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 21m20s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Three PR-only git-diff gates (ci-image-pin, docs-reminder, decisions-guard) lived in docker-build.yml, which also triggers on push to main and v* tags. Gitea dispatches a job as a runner task even when its `if` skips it, so on every tag/main push these three were dispatched to the `small` lane just to evaluate the skip. On the v26.12.0 tag those dispatched skip-tasks wedged in act's setup phase and were killed by a runner restart mid-setup, reporting `failure` (no logs) and reddening the tag's overall commit status even though the release built, scanned, and deployed fine. The two identical-`if:` jobs on ubuntu-latest (api-docs, format) skipped cleanly — the job logic was never the problem; the kill lands in the dispatch window before any step or skip runs, so tweaking the `if:`/step logic could not fix it. Relocate exactly those three (pure checkout + git-diff, no container:, no image pin) verbatim into a dedicated pr-checks.yml that triggers `on: pull_request` only. Gitea evaluates a workflow's trigger before creating any job, so on a tag/main push this workflow produces zero jobs: no dispatch, no kill, no spurious red — for the whole class, permanently. - ci-image-pin carries no pin and still greps docker-build.yml, where all five pin-bearing jobs (test/migrations/functional-e2e/api-docs/format) remain, so its validation is unchanged. - None of the three are required checks (only Build & test + EF migration integrity are), so the status-context prefix change (Build ErsatzTV Image / ... -> PR Gates / ...) does not affect merges; the merge-consent hook reads the prefix-agnostic combined status. - pr-checks.yml declares `defaults: run: shell: bash` (ci-image-pin uses mapfile / set -o pipefail). docs/ci-cd.md: new "PR gates workflow" section + cross-references. fixes #535 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
962dc2a31a |
feat(520): parallel orientation+selection startup; retire #237 as live state; #642 retrieval bullets [decisions-edit]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b1e7e08884 |
feat(521): rework decisions guard — lifecycle validator + narrow [decisions-edit]; header + CI rewrite [decisions-edit]
Retire the line-level append-only mechanic (ersatztv#303 H9) in favor of the lifecycle validator built in Tasks 1-5. .claude/hooks/decisions-guard.sh is now a thin fail-open shim around scripts/decisions_validate.py; .husky/pre-commit calls it for the structural (working-tree) checks, .husky/commit-msg drops the old staged/[decisions-edit]-deletion block and keeps only the Co-Authored-By check. The Gitea decisions-guard job is renamed "decisions lifecycle" and now runs decisions_validate.py --base/--head (structural + body-diff + no-vanish) and build_decisions_catalog.py --check (active catalog drift), with actions/setup-python@v5 added since the bare `small` lane doesn't guarantee python3; the old 1800-line consolidation-floor step is removed (replaced by the validator's aggregate active-corpus budget). docs/decisions.md's header is rewritten from append-only to lifecycle framing (metadata schema, statuses, generated catalog, archive, same-PR supersession); [decisions-edit] is re-scoped (not removed) to rationale-prose edits/factual corrections only. docs/ci-cd.md's release ritual and hook/job descriptions are rewritten to match. Also fixes a pre-existing validator false-positive surfaced while sanity-checking against origin/main: Task 6's #303 H9/H3 split (commit d09be57e) renamed the archived record's heading away from the pre-split original, which the validator's heading-based relocation check reads as "removed without an archive copy." Restored the archived heading to match the original text (functionally unchanged — still status: superseded, same key) and updated the two prose cross-references (migration-map.md, release-ci-governance.md) that pointed at the old anchor. PR1 scope only (per brief): does NOT wire the kickoff-guard CI step or touch the kickoff/README/select-queue docs — that's Task 8/PR2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5709bf5a2c |
ci(508): bump the CI toolchain pin to the image this branch published
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 11s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m50s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 22s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 16s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m18s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m20s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Editing .gitea/workflows/ci-image.yml is itself a trigger path for ci-image.yml, so the previous commit republished the toolchain image at its own sha. `ci-image-pin` then correctly failed: the pin still named |
||
|
|
4263cf7919 |
ci(508): move both docker build jobs off the small lane
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Failing after 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 14s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 14s
Build CI Toolchain Image / Build & push CI image (push) Successful in 2m37s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 5s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m54s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m11s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 19m27s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
`runs-on: small` carried two jobs that are not small: docker-build.yml's image build, and ci-image.yml's toolchain buildx. The second reads as lightweight because it is "docker-only, no toolchain needed — it *builds* the toolchain", which is true and yet describes the heaviest job in the lane. A lane's per-job memory cap is set by its worst member, not its median, so these two pinned `small` at --memory=10g. On bumblebee's 25 GiB — also the prod media host — that permits exactly ONE slot, and four jobs shared it. So "widen the lane" and "keep the heavy jobs" were never simultaneously available. The symptom that forces the issue is not queue wait. A saturated lane also wedges DISPATCHED jobs in act's setup phase: >10 min in_progress, no log file written at all, then failure, before Checkout runs. That is where "decisions.md is a known flake, just rerun it" came from — the rerun works only because it lands after load clears, so a capacity problem read as a bug in the guard. With both builds on ubuntu-latest, `small` is a checkout plus a `git diff` and server-management#639 caps it at 1 GiB, widening it to 4 slots across two hosts while committing LESS RAM to CI than the single slot did. so it cannot be dispatched until the jobs it would queue behind have finished. refs #508 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c5369b1d69 |
ci(412): sample true peak-anon in the test job, not cache-inflated memory.peak
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m15s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m10s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m12s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 13m47s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m12s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The test-job memory instrument (#411) reported memory.peak — the high-water mark of memory.current, which charges reclaimable page cache to the cgroup. A build does heavy NuGet/npm/obj/bin/coverage I/O, so cache can dominate the peak, and page cache is reclaimed under a tighter cap rather than OOM-killed. Sizing a per-job cap (server-management#604) off memory.peak therefore inverts the decision. The OOM-forcing quantity is peak anon, which the kernel exposes no counter for and which the end-of-job split misses (a job that peaks mid-dotnet-test then frees reports a low anon). New scripts/ci-peak-anon.sh: a `start` step (before Build/Test/Coverage) launches a detached background sampler tracking the high-water mark of cgroup anon; a `report` step (last) stops it and prints the sampled peak anon as the headline, keeping memory.peak + end-of-job split as a cache-inflated ceiling and reference. Both continue-on-error + fail-open so they never redden a build. Validated on bumblebee: survives step-boundary re-execs, catches a transient 2.5 GiB anon spike the snapshot reports as 0, stops cleanly on kill, degrades gracefully. Compiler-server A/B (swap-off, sampled peak-anon, n=2 interleaved): OFF (CI config) ~5.84 GiB consistent; ON (defaults) 6.3-7.6 GiB, always higher, + a ~3 GiB resident VBCSCompiler. Disabling the servers is worth it, but OFF sits right at 6 GiB for the build phase alone and the test job adds test+coverage, so #406's "budget loosens well under 6 GiB" premise is not supported. Size the cap off the live test-job sampler. Docs: ci-cd.md instrument section rewritten (peak-anon headline + A/B table + premise verdict); decisions.md entry added. No .cs touched. fixes #412 |
||
|
|
eafb2e39e2 |
perf(469): format gate uses dotnet format whitespace --folder (~480s → ~0.5s)
Build ErsatzTV Image / decisions.md append-only (pull_request) Waiting to run
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 17s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m15s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m28s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Has been cancelled
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been cancelled
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Has been cancelled
The blocking `format` CI job and the Husky pre-commit hook verified changed .cs files with `dotnet format ErsatzTV.sln --no-restore --verify-no-changes --include <files>`. `--include` only narrows *which* files are checked, never what gets loaded: the full recipe loaded the ~10-project MSBuild workspace and built a Roslyn compilation per project before checking a single line (~480s locally, whole-solution). Switch both to `dotnet format whitespace . --folder --verify-no-changes --include <files>`, which treats the tree as a plain folder of files, skips MSBuild/Roslyn entirely (~0.5s), and needs no `dotnet restore` (NuGet-cache + Restore steps removed). Coverage is unchanged: folder mode reads .editorconfig and enforces exactly the gate's purpose — whitespace + charset (BOM). Proven non-vacuous (error WHITESPACE on a trailing-space line, error CHARSET on a prepended BOM, exit 0 clean). The full gate never enforced the style/analyzer pass either — a warning-severity naming violation passes the full solution format (exit 0) — and the analyzers that must block (NU1904, S3981) are enforced at compile via WarningsAsErrors, not by this job. Docs: ci-cd.md Formatting section + the obsolete #406 memory note; decisions.md. fixes #469 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
eb12176fe1 | ci(420): skip re-validating an already-green identical tree on merge-to-main | ||
|
|
8a85f9ddb5 |
test(363): functional-E2E harness — add deterministic scan-lock + collections-lock 409 flows
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 10s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 28s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m24s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 3m45s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m16s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m46s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Extends scripts/e2e-functional.sh with the two IEntityLocker 409 contracts the first cut (ersatztv#299) deferred as "racy", made DETERMINISTIC by firing the racing request only once the lock is provably held (no sleep-and-hope): - library-scan "already scanning" 409: seed ~60 tiny ffmpeg clips into the built-in Shows library so the scanner subprocess runs a few seconds, poll GET /libraries/scan-status until the library is active (that window is a strict subset of the scan lock's held window — StartScan after LockLibrary, EndScan before UnlockLibrary), then a second POST .../scan is a guaranteed 409. Self-skips (advisory) when ffmpeg is absent. - external-collections "already scanning" 409: seed a Jellyfin media-source row pointing at a non-routable address so the background sync hangs and the per-family lock stays held; the lock is taken synchronously before the 202, so the 202 proves it held. collections-scan-status corroborates; unknown source 404. Seeding uses python3's stdlib sqlite3 (already a harness dep) to insert rows the API can't create (LibraryPath, media-source); WAL mode tolerates the second writer. No new CI step/dependency — ffmpeg ships in the toolchain image. Verified: 4/4 fresh-instance runs green (38/38), lock section deterministic. Still deferred to #363 follow-ups: the playout-build lock 409 + isLocked projection (#215) and the UI-interactive Playwright flows. Docs updated same PR: docs/e2e-local.md, docs/ci-cd.md, the functional-e2e job comment in .gitea/workflows/docker-build.yml. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5ba737bec7 |
ci(416): skip heavy jobs on docs-only changes
Docs-only changes (docs/** or *.md) ran the full docker-build matrix (~9 min). Each heavy job (test, migrations, functional-e2e, build) now runs scripts/ci-detect-docs-only.sh as its first post-checkout step and gates every real step on docs_only!='true'. The jobs still RUN and report success in seconds, so the two required contexts keep reporting — a docs-only PR stays mergeable (never an if:-skipped required job; Gitea 1.25.4 reports if-skip as 'skipped', verified with a throwaway probe PR). build skips its image steps on a docs-only push to main; tag builds force docs_only=false. Detection uses --no-renames so a code->docs rename can never be misclassified as docs-only. Refs #416 |
||
|
|
469d725559 |
ci(406): apply the memory-swap rule to our own two sites; stop leaning on the peak reading [decisions-edit]
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m15s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 17s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 15s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m20s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 8m13s
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 4s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Re-review of the fix commits came back MERGEABLE with three findings worth acting on.
MEDIUM -- the PR documented a standing rule ("--memory without --memory-swap silently
grants 2x in swap") and then didn't apply it to the two sites this repo owns:
docker-build.yml's smoke container and scripts/migration-smoke.sh, both `--memory 2g` with
no --memory-swap. Pre-existing rather than a regression, but a rule you don't follow in
your own repo isn't a rule. The migration-smoke one matters most: it runs on the PROD host
in the release path, so a runaway migration should die against its cap rather than quietly
swap out the box serving media.
LOW -- and this is the important one: the docs leaned "peak 8305 MiB is probably mostly
reclaimable cache". An independent probe (full solution build, same CI image, shared
compilation off) measured peak 9457 MiB / anon 7134 MiB / file 421 MiB. ANON DOMINATED.
Having verified the *mechanism* (peak overstates because it counts page cache), I guessed
the *magnitude* in the direction I preferred -- the exact failure this entry criticises,
committed inside the entry criticising it. Corrected in ci-cd.md, decisions.md and on
server-management#604 (where the previous comment could have led to an unsafe 6g cap).
Consequences now recorded honestly: a 6g cap looks UNSAFE, #570's "6g proved too tight" is
the rule not an outlier, and #406's premise ("if this brings peak RSS well under 6 GiB the
whole budget loosens") is looking DEAD -- the 7134 MiB anon was measured with shared
compilation already off. The switches remain right; the looser budget they were meant to
buy does not follow.
NIT -- dropped the unverified claim that this also disables the Razor build server. The
UseRazorBuildServer -> UseSharedCompilation fallback is .NET 5-era; Razor has been an
in-process source generator since .NET 6, so there is likely no separate server to disable
on .NET 10. Unverified, zero impact, so it has no business in a doc arguing for
measurement over assumption.
[decisions-edit]: the touched docs/decisions.md lines were added by this PR's own earlier
commits, not settled entries on main -- net vs origin/main remains a pure insertion (0
deletions, verified). Also the sanctioned reason: the entry was factually wrong (see LOW).
Verified: both workflows parse; migration-smoke.sh passes bash -n; the parsed mysql option
string is `--memory=2g --memory-swap=2g --cpus=2`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7859510c78 |
ci(406): report anon/file split — memory.peak counts page cache, not just RSS [decisions-edit]
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 15s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 13s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m35s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 13m30s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m1s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The peak-memory step from the previous commit worked on its first real CI run and immediately produced a number that would have led #604 to the WRONG decision: peak container memory: 8305 MiB (8708448256 bytes, from /sys/fs/cgroup/memory.peak) Read naively that says "the test job needs 8.1 GiB, so the 10g cap must stay and #406's 'the budget loosens' premise is dead". That reading is probably wrong, because `memory.peak` is the high-water mark of `memory.current`, which charges PAGE CACHE to the cgroup as well as anonymous memory. It is not peak RSS, and a build job does heavy NuGet/npm/obj/bin/coverage I/O. Proven on bumblebee -- a container with ZERO anonymous memory that merely reads an 800MB file: memory.peak : 826 MiB anon : 0 MiB file : 800 MiB This inverts the decision: page cache is RECLAIMED under a tighter cap, not OOM-killed, so a large peak that is mostly `file` is NOT evidence that the cap must stay high. `anon` is what forces an OOM. So the step now prints the anon/file split and says, in the log and the docs, to size caps on anon rather than peak. Honest about what this still isn't: the split is read at end-of-job, so it is the composition then, not at the peak instant -- a job peaking mid-`dotnet test` and freeing after would under-report anon. Sampling a true peak-anon is ersatztv#412, filed with the options. Also recorded there and in ci-cd.md: there is NO pre-change baseline from this instrument (7.8 GB VBCSCompiler was measured host-wide across concurrent jobs, not inside one job container), so #406's premise is neither confirmed nor killed yet. What IS established: no persistent compiler server survives a build, and migrations is green with mysql capped at 2g with swap disabled. The irony is the point, and it is now written into decisions.md: this repo's CI perf work keeps stating numbers from plausibility rather than measurement (#390's "2-4min" apt-ffmpeg estimate; real 110s). Measuring the wrong quantity precisely is the same failure wearing a lab coat -- and it bit this very change, whose whole purpose was to replace an inherited number with a measured one. Verified: shellcheck clean; the real step body run inside a --memory=2g container under `bash -e -o pipefail` reads memory.peak, prints the split, writes the step summary, exits 0; and it degrades to a skip + exit 0 where no cgroup file exists. [decisions-edit]: the modified docs/decisions.md lines are ones THIS PR added in its own earlier commits, not a settled entry on main -- net vs origin/main is still a pure insertion (0 deletions, verified). It is also the sanctioned reason: the entry as written was factually wrong about what memory.peak measures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
04e3a5249d |
ci(406): disable swap on the capped mysql; fix peak-step placement and failure mode [decisions-edit]
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 16s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 14s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 14m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 13m43s
Review fixes from the cold adversarial pass on PR #411. Verdict was MERGEABLE, but one
HIGH is a genuine defect worth the round-trip.
[decisions-edit] rationale: the 12 "modified" lines in docs/decisions.md are lines THIS PR
added in its own previous commit (
|
||
|
|
e33205e8eb |
ci(406): disable persistent compiler servers, cap the mysql service, report peak RSS
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 9s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m22s
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 5s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m9s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 18m23s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m45s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Refs #406, server-management#604. Items 1 and 2 of #406 (item 3 landed in PR #407). Root cause of the 2026-07-17 bumblebee incident (load 340, 21 GiB swapped, prod media down until reboot) is CI-induced memory pressure, and the largest single consumer was Roslyn's persistent VBCSCompiler at 7.8 GB RSS. It is a compiler *server*: it outlives the build that started it to keep its heap warm for the next one. In CI there is no next build -- each job container is torn down at the end of the run -- so it is pure cost, and it is the actual reason each job needed a 10 GiB cap. 1. Disable the compiler servers via the workflow's top-level env (UseSharedCompilation, DOTNET_CLI_USE_MSBUILD_SERVER, MSBUILDDISABLENODEREUSE). MSBuild surfaces env vars as properties and only defaults UseSharedCompilation when empty, so the env var wins and every dotnet call is covered without touching each call site. Also set as ENV in the Dockerfile's SDK stage: the workflow env reaches runner-side jobs only, and the `build` job compiles inside `docker build` where it does not propagate -- that is the job server-management#570 measured pegging 5.999/6 GiB, so it is the one that most needs this. Build stage only; the final image is FROM runtime-base, so nothing lands in the shipped image. Verified locally: a default build leaves 1 VBCSCompiler alive, the same build under these vars leaves 0, and ErsatzTV.sln still builds clean (0 errors). Trade-off: csc runs per project instead of via the server, costing some build time. 2. Cap the mysql service (--memory=2g --cpus=2). A runner's container.options applies to the job container ONLY: a live migrations job reported HostConfig.Memory=10737418240 for the job and mem=0 nanocpus=0 for its mysql service, i.e. unbounded. 2g is sized on measurement -- mysql:8.4 with this exact env peaked at 543 MiB during init, 481 MiB idle (probed on bumblebee) -- not on inheritance. Also add a `Report peak container memory` step to the test job (cgroup memory.peak, always(), never fails a build). server-management#604 sizes both runners' caps on that number and it was inherited rather than measured; now every run reports it. Docs: ci-cd.md gains the compiler-server + services-cap sections and the lane table is corrected (PR #407 reverted the #390 lane move without updating it). decisions.md records all three, incl. the standing rule that a services: container never inherits a cap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
17d65909b6 |
ci: move api-docs and format back to ubuntu-latest (refs #406)
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m29s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 11s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 11s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m14s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
#390 moved these two onto the `small` lane to dodge a ~29 min queue in the contended `ubuntu-latest` lane. The queue was real, but the lane was the wrong place to fix it, and #390's own rationale flagged why: Caveat: on an API-touching PR this job does a full `dotnet build`, so it is not always a "small" job; capacity 4 absorbs that. "Capacity 4 absorbs that" held only because nothing enforced the SUM of the runners' per-job memory caps. Each job container is correctly capped at --memory=10g, but 6 slots x 10g = 60 GiB sat on a 25 GiB host that also runs prod media. On 2026-07-17 it went to load 713 with 21 GiB swapped, and telegraf could no longer get scheduled to report it. server-management#604 fixes that at the source rather than by shuffling lanes: the `ubuntu-latest` lane now has 5 slots (ci-runner grown to 48 GiB at capacity 4, plus a bumblebee overflow slot) against this repo's 5 ubuntu-latest jobs, and the `small` lane is sized for genuinely-tiny jobs. So the queue #390 was dodging is gone, and these two dotnet jobs go back where they can actually be given memory. Note this does NOT by itself shrink the `small` lane's caps: `build` (Build & push image) still runs there and is the very job #570 measured pegging 5.999/6 GiB, so that lane keeps a 10g-capable slot either way. The real unlock is disabling Roslyn's persistent VBCSCompiler in CI (#406) — 7.8 GB RSS observed — which would let every cap drop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ed52892d68 |
ci(390): bump pin to ersatztv-ci:07048b8; compare full shas in pin guard
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 11s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 13s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 14s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m32s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 4m34s
Pin bump for the image rebuilt from the review-fix commit (the two-step working as designed — the new ci-image-pin guard demanded it). Also fixes a real bug in that guard, caught locally before CI: it compared abbreviated shas, but git auto-scales abbreviation length with object count, so the tag ci-image.yml builds from a fetch-depth:1 shallow clone is 7 chars while %h on the guard's full clone is 8 — it would have failed on every run. Now resolves both sides to full shas via rev-parse, which also catches a pin that names no commit at all. Verified non-vacuous locally: PASS on the real pin, FAIL on a stale pin, FAIL on an unresolvable one. Refs #390 |
||
|
|
07048b8c96 |
ci(390): address cold review — pin-drift guard, honest cron, doc fixes
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Failing after 13s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 15s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 16s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 15s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 13s
Build CI Toolchain Image / Build & push CI image (push) Successful in 29s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 6m6s
Cold adversarial review findings: - MEDIUM (Renovate generates pin drift): add a blocking ci-image-pin job. Renovate manages docker/ci/Dockerfile's base pins but cannot bump an opaque :<sha> in container.image, so a base bump would publish a new image, test the OLD one, and merge with the Dockerfile disagreeing with the pin. The guard fails when the pin isn't the last commit to touch docker/ci, or when the 5 jobs pin different tags — making the documented two-step enforced rather than remembered. - MEDIUM (cron was a no-op): the weekly rebuild updated nothing (jobs pin :<sha>) and buildcache would have restored the apt layer verbatim, collecting none of the base updates it existed for. Added no-cache on the schedule path and rewrote the comment to state what it actually is: a build canary + a fresh :latest for the next bump. - LOW: FFMPEG_TAG was referenced in the Dockerfile + docs but never existed (the FROM is hardcoded); reworded both. - LOW: paths: filtered the exact file while the docs claimed docker/ci/** — use **. - NIT: docs oversold ENTRYPOINT reset as a gotcha; act overrides it anyway. Marked defensive. Refs #390 |
||
|
|
84756eebab |
ci(390): default run steps to bash — act_runner uses sh inside container:
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 17s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 17s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 18s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 19s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 6m9s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m59s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 8m0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The migrations job died in 0.13s with 'set: Illegal option -o pipefail'. Root
cause: inside a container: act_runner does not default to bash, it falls back to
'sh -e {0}' (dash), since it cannot assume bash exists in an arbitrary image.
Every multi-line script in this workflow is bash (set -o pipefail, arrays,
shopt, mapfile), so dash fails them immediately.
The format job passed only because it already declared shell: bash explicitly.
Declare it once at the workflow level instead of per step; non-container jobs
already defaulted to bash, so nothing changes for them.
Documented in docs/ci-cd.md as a container gotcha — it surfaces only once a job
is containerized and does not look like a shell problem.
Refs #390
|
||
|
|
afa42b0b6f |
ci(390): run toolchain jobs in the shared CI image; drop 110s apt-ffmpeg; rebalance lanes
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 13s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Failing after 4m31s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m22s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Failing after 6m8s
Converts test/migrations/functional-e2e/api-docs/format to run inside ersatztv-ci:6a9e27c via container:, removing per-run setup-dotnet, setup-node, apt-get ffmpeg and dotnet tool install. NuGet/npm caches stay (project deps); setup-node's implicit npm cache is replaced with an explicit actions/cache. Rolled-up wins found while auditing every job's steps (evidence in #390): - Drop the apt-ffmpeg step (110s/run). It was never load-bearing: the app's only startup ffmpeg touch is a LogWarning from FFmpegLocatorService, and the harness drives curl-only contracts that never transcode. The image ships prod-identical ffmpeg 8.1.2 anyway, which #299's seeded-media follow-ups will need. - Bake dotnet-ef + reportgenerator into the image (2 per-run tool installs gone; versions now live in one tracked place). - Move api-docs (5s) and format (37s) to the idle 'small' lane — they were each queueing ~29min in the saturated ubuntu-latest lane. Also drops that lane from 5 jobs to 3. Only possible because container: makes them self-contained. Measured, not assumed: the image itself is worth only ~15-40s/job. The dominant costs are queue wait (server-management#604) and 742s of redundant compilation (#398) — both filed rather than silently bundled here. Refs #390 |
||
|
|
6a9e27c6b8 |
ci(390): fix CI-image smoke test — reportgenerator --version exits 1
Build CI Toolchain Image / Build & push CI image (push) Successful in 2m9s
The build-time smoke test failed the image build even though the toolchain was
healthy: reportgenerator prints its version banner then exits 1 ("No report
files specified"), so it has no clean version probe. Probe the dotnet-tool shim
on PATH instead. dotnet --info confirmed the SDK + .NET 10.0.10 runtimes resolve
correctly on the ffmpeg base (ICU/glibc fine).
Refs #390
|
||
|
|
e78aa01c05 |
ci(coverage): exclude generated EF migrations to stop coverage OOM (exit 137)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m26s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 16s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 17s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m5s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 3m46s
The shared Build & test job OOM-killed (exit 137) with coverage enabled: the whole-solution Architecture.Tests process instruments both EF migration folders (~2.59M generated lines vs ~200k authored), ballooning coverlet's in-memory hit-count arrays past the runner's limit. Add coverlet.runsettings excluding **/Migrations/*.cs (+ generated/compiler attributes) and pass it via --settings on the Test step. This cuts the instrumented surface ~126x (2,510,185 -> 19,963 coverable lines in the Architecture.Tests process, verified locally) and makes the reported percentage reflect authored code. Documented in docs/ci-cd.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a886fd2824 |
ci(coverage): make ReportGenerator install idempotent + pinned (review follow-up)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 10s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 3m59s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 7m55s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 8m38s
Cold-review should-fix on PR #370: `dotnet tool install` errors under set -e if the tool is already present (retried step / cached runner image), which would silently degrade the continue-on-error summary step to a no-op. Use `dotnet tool update` (install-or-update) pinned to 5.5.10 for reproducible output. Also order coverlet.collector after CliWrap to keep the ItemGroup alphabetical (nit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2090f7865c |
ci(coverage): collect code coverage and publish a summary (refs #15)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 9s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 22s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 36s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m29s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 6m17s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (pull_request) Has been cancelled
#15 scope item 4 ("collect with coverlet and publish a summary; decide on a floor later"). coverlet.collector was already referenced by four of the six test projects but the CI Test step never passed --collect and nothing summarized the result. - Add coverlet.collector to ErsatzTV.Core.Tests and ErsatzTV.Tests so coverage is uniform across all *.Tests projects. - CI Test step now runs --collect:"XPlat Code Coverage" --results-directory ./coverage, then a best-effort Coverage summary step merges the per-project Cobertura reports with ReportGenerator (TextSummary to the log, MarkdownSummaryGithub to the job step summary). No floor is enforced yet; continue-on-error keeps it purely informational. - gitignore /coverage/; document the step in docs/ci-cd.md (test job). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0d7803079c |
ci: add advisory functional-E2E curl harness (fixes #299)
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m54s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 17s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 18s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 4m17s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m22s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m38s
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 6m2s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m41s
Codify the manual live-E2E curl flows sessions have been re-running by hand into a CI regression net: a new `functional-e2e` job boots the app from source (via scripts/e2e-local.sh, parameterized for Release) and drives scripts/ e2e-functional.sh against it. First-cut contracts (all curl-only, deterministic, no seeded media/ffmpeg/browser): - legacy->SPA redirect sweep + the /api,/artwork never-redirect exemption - auth/CSRF/security-stamp flow (setup-claim, read-gate, CSRF, login, logout+revoke) - library-scan status contract (404/202/scan-status) - optimistic-concurrency If-Match/412 round-trip Advisory by design (separate job, not a `build` dependency, not a required check) so a functional-E2E flake can't block the unit-test gate; promote once proven, mirroring the migrations-job rollout. SQLite default -> no DB service. Deferred to #299 follow-ups (need scanner+seeded media or a browser to be deterministic): the racy 409 re-trigger, playout-build lock 409, Playwright UI flows. Assertions verified 30/30 green against a real Release-built instance; caught /artwork/* returning 400 (not the 404 a static read suggested). Docs updated same PR: docs/ci-cd.md (new job), docs/e2e-local.md (harness), docs/decisions.md (append-only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3d07c6818d |
ci(process): #311 H11 rebase-before-push hook + PR-scoped format-verify CI job
- H11: .husky/pre-push calls .claude/hooks/prepush-rebase-check.sh, which blocks a push whose branch is behind origin/main (rebase first; do not merge main in — a merge drags in files you didn't touch, e.g. legacy-BOM .cs, tripping the format hook on code that isn't yours). Fail-open; escape ETV_SKIP_REBASE_CHECK=1. - New blocking `format` CI job: dotnet format --verify-no-changes scoped to the PR's changed .cs only (style + charset=utf-8/no-BOM), enforcing fix-as-you-touch without a big-bang reformat of the ~2500 legacy BOM files. .cs-free PRs skip and pass (always reports a status). Closes the "CI never checks charset" gap that let #269 land 17 BOM files (#310). Docs (contributing.md §7 / decisions.md / lore) follow in the next commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6e92a951eb |
feat(process): #303 decisions.md consolidation size-floor (read-cost, non-blocking) [decisions-edit]
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Failing after 9s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 15s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (pull_request) Has been cancelled
Timothy's refinement: the between-releases consolidation floor triggers on the file's READ COST — its line count, i.e. the context an agent burns reading the log — not entry count. The decisions-guard CI job now emits a non-blocking ::warning:: once decisions.md exceeds 1800 lines (the point past which it no longer fits one default 2000-line agent Read). Documented in the decisions.md header, the H9 entry, and ci-cd.md (job description + release-checklist note). Touches committed H9 lines, hence [decisions-edit]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
56ef1df936 |
feat(process): #303 H3 root-screenshot guard + H9 decisions.md append-only guard
H3 (Husky pre-commit): reject a staged root-level *.png — belt-and-suspenders with the .gitignore screenshot rule so `git add -f` still can't land a review artifact. H9 (append-only decisions.md): new shared hook `.claude/hooks/decisions-guard.sh`, wired into Husky commit-msg (staged mode) and a new blocking `decisions-guard` CI job (range mode). Blocks any commit/PR that deletes or modifies an existing line of docs/decisions.md — detected via `git diff --numstat` deleted-count, robust to markdown `-` list markers — unless the message carries the `[decisions-edit]` token. Pure insertions (a normal new entry) always pass. One implementation for local + CI so they can't drift. Fail-open on any tooling trouble. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
aa2e13fa51 |
ci: #303 H4/H5 blocking api-docs gate — fail on stale OpenAPI artifacts
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 14s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m15s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adds a blocking `api-docs` CI job: when a PR diff touches the API surface (ErsatzTV/Controllers/Api/** or ErsatzTV.Core/Api/**) it rebuilds the generated artifacts from source — v1.json, v1.d.ts, endpoint-index.md — and fails if any is stale in the diff. Mechanizes the "docs-update in the same PR" rule for the API contract (docs-reminder stays a non-blocking route-parity nudge). Path-gated INSIDE the job (per-step `if:` on a detect output), not via a top-level `if:`, so the check always reports a status on every PR and is safe as a required check: API-free PRs skip the dotnet/node setup + regen and pass trivially. Verified the gate reproduces the committed baseline: a fresh build regenerates v1.json byte-identical to HEAD (incl. all 244 auth security/401 blocks). The only footgun is local — update-openapi.sh runs dotnet-getdocument against the already-built assembly, so a stale bin/ emits a stale spec; api-conventions.md §5 now flags "build first". CI is immune (fresh checkout has no bin/). Docs: api-conventions.md §5 (two-place CI enforcement + stale-assembly note), decisions.md (new entry). Refs #303. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fa2d787ac1 |
ci: make the MySql migration-apply resilient to concurrent-runner contention
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m14s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m12s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m24s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
Root cause (diagnosed from run logs 513/515/516): the EF migration-integrity
job's "MySql apply all migrations to a fresh DB" step flakes when two migration
jobs land on the SAME runner host at once — each `services: mysql:8.4` container
starves the other, so the 787-migration replay either exceeds MySqlConnector's
30s default command timeout ("Command Timeout expired", run 513 on ci-runner) or
has its connection dropped mid-replay ("MySqlEndOfStreamException", run 516 on
bumblebee-runner). It's pure infra contention: `has-pending-model-changes` (the
model check) passes both providers, and the identical tree passes on a quieter
host (run 515). Both runners have both passed and failed — not one bad runner.
Fix (runner-agnostic, repo-owned workflow only — no runner-host change needed):
- Raise `DefaultCommandTimeout` to 300s in the MySql connection string.
- Wrap the apply in a 3× retry that resumes from `__EFMigrationsHistory` (EF
commits each migration in its own transaction, so an interrupted one rolls back
and the retry continues). A real migration failure fails on every attempt, so
the retry can't mask a genuine problem.
Docs: ci-cd.md migration-integrity section documents the contention + retry.
Refs #13 #236
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
1b5efd7b9d |
ci: prod follows :prod (remove version-pin bump-prod-compose job)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 18s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m2s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m56s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m1s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m1s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m24s
Timothy reversed the version-pin decision: prod's media-servers compose now follows the floating :prod tag, redeployed by Komodo Global Auto Update. The bump-prod-compose job (#275) rewrote a :<version> pin, which would flip :prod -> :26.8.0 on the next release — remove it. docs/ci-cd.md reconciled to the :prod model (+ flags the open caveat: verify Global Auto Update runs the #553 pre-deploy backup, else releases deploy without a backup). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0ff21d5b8e |
ci: restore bump-prod-compose auto-deploy job on v* release
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m32s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m44s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Bump prod compose tag (server-management) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 9m59s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 7m9s
Build ErsatzTV Image / Bump prod compose tag (server-management) (push) Has been skipped
The auto-pin-to-prod job designed on the unmerged `ci/auto-bump-prod-compose`
branch (
|
||
|
|
1bef9c9076 |
ci: small-runner lane + per-ref concurrency (queue starvation fix)
- build & docs-reminder jobs -> runs-on: small (dedicated small-jobs runner, server-management#574). Gitea dispatches a job as a runner task even when its 'if' skips it; the PR-run skip of 'Build & push image' waited up to 31 min for an ubuntu-latest slot, stalling every PR run. - concurrency scoped per event+ref with cancel-in-progress for PRs: runs parallelize across PRs, superseded syncs auto-cancel. Previously one global group serialized ALL runs (single-runner relic). Main/tag builds still serialize within their ref; don't push main + v* tag simultaneously (shared :buildcache / smoke container) — tag after main is green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ef8915f1ca |
ci: unpin MySQL service host port 3306 (concurrent-run collision)
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 6m48s
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m40s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
The migrations job connects via Server=mysql on the shared runner network; the host-port publish was unused and made overlapping runs fail with "Bind for 0.0.0.0:3306: port is already allocated" (bit PR #222 tonight, backlogged since #216). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6bdb7dae1c |
docs+ci: harden the docs-first rule + add a non-blocking parity-doc reminder
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m4s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 11m56s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
CLAUDE.md Conventions: turn "read docs before exploring" into a hard rule with red-flag framing (do NOT reverse-engineer conventions from source; applies to subagents too) and a "which doc to update in the same PR" table. Add a lightweight, PR-only, NON-BLOCKING `docs-reminder` CI job that warns when a screen/route change skips docs/blazor-route-parity.md. No dotnet/node in that job (no actions/cache), so it can't hit the VM-127 cache-save issues (server-management#570). Documented in docs/ci-cd.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e68aa5834c |
ci: speed up build pipeline (NuGet cache, shallow checkout, jar layer reorder)
Three CI wins from ersatztv#190, all config-only: - Cache ~/.nuget/packages via actions/cache@v4 in both the `test` and `migrations` jobs, keyed on Directory.Packages.props + global.json, so dotnet restore no longer starts from scratch every run. - Shallow-checkout the `test` job (fetch-depth: 1) since it never runs git describe/log; only `build` needs full history for versioning. The `migrations` job's checkout was already effectively shallow (default fetch-depth 1), documented explicitly. - Reorder the Dockerfile so the openapi-generator jar wget happens before the openapi spec COPY, decoupling the ~30MB download layer from spec changes (e.g. v1.json) that previously busted it on every API PR. Documented all three in docs/ci-cd.md alongside the existing job/Dockerfile notes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
439272b405 |
feat(web): SPA cutover — root route + legacy redirects (#91)
Make the ChicoryTV SPA the default UI: GET/HEAD requests to / and to legacy Blazor routes with SPA equivalents (channels, schedules, playouts, libraries, all settings pages, channel add) now 302 to their /app counterparts, preserving query strings and the ETV_BASE_URL path base. 302 not 301: the map will grow as parity lands and permanent- redirect caching would make rollback painful. Blazor-only functionality (collections, media browse/search, trakt, filler presets, watermarks, ffmpeg profiles, blocks/decos/templates, playout detail editors, logs, troubleshooting, channel edit) keeps serving Blazor; the Blazor home stays reachable at /system/health. Parity gaps are tracked in #140-#146; Blazor removal is phase (b). Also adds an /app smoke assertion to the docker build workflow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |