Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 2m45s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Failing after 2s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
102 lines
5.4 KiB
Markdown
102 lines
5.4 KiB
Markdown
# Local live-E2E recipe
|
||
|
||
Purpose: how to stand up a real, running instance of this fork locally (dotnet host + built SPA)
|
||
for manual or Playwright-MCP-driven end-to-end verification — no live Docker/prod dependency.
|
||
**Update this doc (and `scripts/e2e-local.sh`) in the same PR that changes any convention below.**
|
||
|
||
This is for interactive/agent-driven verification, not CI (`docs/ci-cd.md` covers the CI pipeline,
|
||
which never runs the app itself).
|
||
|
||
## Why the steps are in this order
|
||
|
||
- **`ErsatzTV/Startup.cs`'s `/app` SPA middleware resolves its static-file root once, at startup**
|
||
(`SpaStaticFileRoot()`, checked with `Directory.Exists` and swapped to a `NullFileProvider` if
|
||
missing — see `Startup.cs` around the `app.MapWhen(... "/app" ...)` block). If `wwwroot/app`
|
||
doesn't exist yet when the process starts, **the SPA will 404 forever until you restart the
|
||
process** — copying the built files in after the fact does nothing for an already-running
|
||
instance.
|
||
- The dotnet host and the SPA share **one port** (default 8409, `ETV_UI_PORT` / `SystemEnvironment.
|
||
UiPort` in `ErsatzTV.Core/SystemEnvironment.cs`) — there's no separate dev server/proxy in this
|
||
workflow; you're testing the actual production static-hosting path.
|
||
- A fresh config folder per run avoids state bleed (leftover channels/schedules/DB) between test
|
||
sessions corrupting your assertions.
|
||
|
||
## Steps
|
||
|
||
1. **Build the prerequisites** (once, or after any source change):
|
||
```bash
|
||
dotnet build ErsatzTV.sln
|
||
cd web && npm run build && cd .. # → ErsatzTV/wwwroot/app (vite.config.ts outDir)
|
||
```
|
||
|
||
2. **Copy `wwwroot` into the build output** (the `dotnet build` output directory does not
|
||
automatically pick up `web/`'s build artifacts placed directly into the source `wwwroot`):
|
||
```bash
|
||
rm -rf ErsatzTV/bin/Debug/net10.0/wwwroot
|
||
cp -R ErsatzTV/wwwroot ErsatzTV/bin/Debug/net10.0/wwwroot
|
||
```
|
||
The `rm` matters: if the destination dir already exists (any prior run), `cp -R src dst`
|
||
copies **into** it (`dst/wwwroot/...`) and the server silently keeps serving the previous
|
||
run's stale assets. `scripts/e2e-local.sh` does the rm+copy for you.
|
||
If you rebuild the SPA (`npm run build`) while the dotnet process from step 4 is already
|
||
running, **re-copy and then restart the process** — see the "why" note above; it will not pick
|
||
up new files live.
|
||
|
||
3. **Use a fresh scratch config folder** — never reuse one across test runs:
|
||
```bash
|
||
CONFIG_DIR=$(mktemp -d)
|
||
```
|
||
|
||
4. **Run the app**, pointed at the scratch folder:
|
||
```bash
|
||
cd ErsatzTV/bin/Debug/net10.0
|
||
ETV_CONFIG_FOLDER="$CONFIG_DIR" dotnet ErsatzTV.dll
|
||
```
|
||
Wait for the log line **`Done migrating search index`** (emitted by
|
||
`RebuildSearchIndexHandler` in `ErsatzTV.Application/Search/Commands/`, with a
|
||
`... in {Duration}` suffix) — that's the last long-running startup step; before that, requests
|
||
may 404/error. The UI and the entire `/api/*` surface are served on the **same port**, 8409 by
|
||
default (override with `ETV_UI_PORT`).
|
||
|
||
5. **Seed data** as needed via the API — no API key is required for local mutating requests by
|
||
default (`ApiKeyAuthorizationFilter` only enforces the `X-Api-Key` header when
|
||
`Api:WriteKey` is configured; it's empty/unset in a fresh local config, so writes are open).
|
||
Examples:
|
||
- Create a channel: `POST /api/channels` — check the current
|
||
`ErsatzTV/wwwroot/openapi/v1.json` (or `CreateChannelRequest.cs`) for the exact required field
|
||
list before assuming these are complete, but as of this writing it requires (among plain
|
||
fields) these enums: `PlayoutSource: "Generated"`, `PlayoutMode: "Continuous"`,
|
||
`SongVideoMode: "Default"`, `TranscodeMode: "OnDemand"`, `IdleBehavior: "StopOnDisconnect"`.
|
||
- Create a playout for an existing channel: `POST /api/playouts` with
|
||
`{"channelId": <id>, "scheduleKind": "Block"}` (or `"Classic"`/`"Scripted"`/`"Sequential"` per
|
||
`ChannelPlayoutSource`/schedule-kind enums — check `v1.json` for the current set).
|
||
|
||
6. **Tear down**: kill the `dotnet ErsatzTV.dll` process and confirm the port is freed
|
||
(`lsof -i :8409` should return nothing) before starting another run — a stray process holding
|
||
the port will make the next run's health check hang or fail confusingly.
|
||
|
||
## Playwright MCP screenshots
|
||
|
||
If you're driving the browser via the Playwright MCP server for visual verification, screenshots
|
||
land in **the MCP server process's own cwd** (this repo's root, not wherever you ran the dotnet
|
||
process from) — expect stray `*.png` files at the repo root after a session; this is tolerated,
|
||
not a bug to fix, but don't check them in.
|
||
|
||
## Script: `scripts/e2e-local.sh`
|
||
|
||
A copy of this script is included in this doc's directory; it is intended to land at
|
||
`scripts/e2e-local.sh` in the repo. It automates steps 2–4 above (build is assumed already done —
|
||
run `dotnet build` / `npm run build` yourself first, since rebuilding on every invocation is slow
|
||
and this script is meant to be re-run often during a debugging session).
|
||
|
||
Usage:
|
||
```bash
|
||
scripts/e2e-local.sh [CONFIG_DIR]
|
||
```
|
||
- `CONFIG_DIR` defaults to a fresh `mktemp -d` if omitted.
|
||
- Copies `ErsatzTV/wwwroot` → `ErsatzTV/bin/Debug/net10.0/wwwroot`.
|
||
- Launches `dotnet ErsatzTV.dll` in the background with `ETV_CONFIG_FOLDER` set.
|
||
- Waits (up to 120s) for the `Done migrating search index` log line.
|
||
- Prints the PID and port, then **exits leaving the server running** — the caller is responsible
|
||
for killing the PID when done (`kill <PID>`).
|