Two backlog trees are complete: `docs/project/backlog/` (75 files, every WP done) and `docs/project/refactor-backlog-setup/` (the arc before it). Move both under `docs/project/archive/` with `git mv`, so history stays intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them, because it points at the now-archived backlog README. Add `docs/project/archive/README.md`. It states that these trees are historical and names the two directories that are still live. Repoint every inbound reference named in RD-30's Files table: CLAUDE.md, the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the `document-feature` and `new-ssp` skills, and the readable-codebase PLAN, README, and RD-19 ticket. Fix two upward-relative links inside the moved WP files (WP-68, WP-69) that gained a directory level and would otherwise break. Repoint `.prettierignore`'s two agent-prompt exclusions to their new path, so prettier keeps leaving those files' exact wording alone. Mark RD-30 done and check off its acceptance criteria; flip its README row to done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.2 KiB
WP-30 — CI performance follow-ups
Status: done (items 1, 3, 4, 5, 6; item 2 deliberately deferred — see "Status update" below) Phase: follow-on · CI/infra
Why
Tier-1 CI speedups shipped in 708d4c2 (CodeQL off the PR path, Playwright/NuGet caches,
npm ci flags) and the demo web image shrank to node:24-slim. These are the remaining
options that were deliberately deferred — bigger changes, policy calls, or things that need
Gitea runner-admin access. Revisit once there's an actual CI-timing breakdown to prioritise by,
or when someone confirms act_runner access.
Constraint carried over: CI runs are not observable from the agent's environment — validate
any workflow edit by watching a real Gitea run; ship one change at a time so a red run is easy to
bisect and revert. The docker compose images are NOT used by CI (CI = Gitea ubuntu-latest
runner image, set on the act_runner host).
Read first
.github/workflows/ci.yml(current 6 jobs + the Tier-1 caches already in place).- The
ci-and-local-gatenote (agent memory) — CI traps + what's already done. docker-compose.yml(demo images;node:24-slimdone, dotnet SDK still full).
Candidate items (pick per impact once measured)
- Skip
npm ciinstall via anode_modulescache.actions/cacheonnode_moduleskeyed bypackage-lock.jsonhash; on a hit,npm ciis near-instant across the 4 npm jobs. Bigger win than the existing npm-download cache, but a ~777 MB cache with a small staleness risk — best if the runner's cache storage is local/fast. Medium effort, low-medium risk. - Smaller CI runner image.
- Real fix (needs runner admin): point act_runner's
ubuntu-latest(or a new label) at a smaller image with node + dotnet preinstalled. Biggest startup win. Blocked on confirming act_runner access. - Repo-only partial:
container: node:24-slimon the node-only jobs (frontend,storybook-a11y), droppingsetup-node. Doesn't help the node+dotnet jobs (e2e,api-client-drift,backend) — a combined image would need building/pushing (new infra). Risky on act_runner, unverifiable locally → stage alone, last.
- Real fix (needs runner admin): point act_runner's
- Path-filtered jobs. Skip
backendon FE-only changes and vice-versa (workflowpaths:ordorny/paths-filter). Cuts compute on narrow PRs; watch required-check rules that expect every job to report a status. - Split a fast
lintjob (lint + format:check + check:tokens) for ~1 min fail-fast feedback — only worth it once item 1 (node_modules cache) lands, else it duplicatesnpm ci. - Lean deployable backend image (optional, not for the dev demo): multi-stage prod build on
mcr.microsoft.com/dotnet/aspnet:10.0(~220 MB) in a separatedocker-compose.prod.yml. The devdocker-compose.ymlkeeps the SDK image becausedotnet runhot-reload needs it. - Semgrep: triage findings + make it blocking. Semgrep replaced CodeQL (GitHub-only, couldn't
run on Gitea) and currently runs report-only — a local dry-run found 27 findings, mostly
CI/config policy (unpinned GitHub Actions in
ci.yml,.npmrcmin-release-age) rather than app-code vulns. Triage them (fix or# nosemgrep/.semgrepignorethe noise; consider a tighter ruleset thanp/defaultif the GitHub-Actions-policy rules aren't wanted), then add--errortosemgrep scanso it's a real gate.
Status update (2026-07-30)
Items 1, 3, 4, 5, 6 implemented, each as its own commit (item 6 526da76, item 1 e46b87b,
item 4 e02e8ce, item 3 e7db69d, item 5 see git log -- backend/Dockerfile): triaged real
local semgrep findings (25, not the 27 this
file remembered — dependabot cooldown, npm min-release-age, every GH Action pinned to SHA, 2
nosemgrep'd ReDoS false positives) and flipped the gate to --error; node_modules cache
(skips npm ci entirely on a hit) across all 4 npm jobs; a new fast-fail lint job split out
of frontend; a changes job (dorny/paths-filter) gating every downstream job's real steps
(not the whole job — the safer "skip steps" variant, so a required-status-check never waits on
a job that never ran) on which side changed; an optional backend/Dockerfile +
docker-compose.prod.yml (additive, unused by CI or the dev demo).
Item 2 (smaller runner image) deliberately skipped this round — the real fix needs
act_runner admin access (unconfirmed), and the repo-only partial (node:24-slim on
frontend/storybook-a11y) conflicts with storybook-a11y's deliberately-chosen
node:24-bookworm + memory-cap container (verified against a real OOM risk). Revisit once
act_runner access is confirmed.
Confirmed GREEN on a real Gitea run (2026-07-30, watched by the user). The push did catch
one real gap that only a live run could: backend/Dockerfile (item 5) predated the item-6
semgrep triage, so its own dockerfile.security.missing-user-entrypoint finding (container
running as root) was never locally verified — semgrep's --error gate correctly failed the
first push on it. Fixed (ebf1f8f): switched to the base image's built-in non-root app user
($APP_UID, uid 1654) with --chown on both COPY layers so SQLite (WP-22, a relative-path
connection string resolved against the container's /app cwd) can still write
bigregister.db; verified for real (rebuilt, confirmed whoami is app, hit a live
GET /brief/preview, confirmed the db file's actual ownership) before repushing. That run came
back all-green — path-filtering, the node_modules cache, and the split lint job all behave as
designed on the real runner.
Acceptance criteria
- Each chosen item verified GREEN on a real Gitea run (watched, since it's not observable from the agent env), landed as its own revertable commit.
npm run cistill passes locally after any workflow/script change (confirmed 2026-07-30, full run including backenddotnet test/dotnet formatand both drift checks).
Out of scope
- nx/turbo/remote build caching (overkill for this repo size).
- Rewriting the dev compose into prod images (would lose bind-mount hot reload).
Risks
- Unverifiable-from-agent workflow edits can only be confirmed on the runner — one change per commit, revert on red.
- act_runner-level changes depend on infra access this repo doesn't control.