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>
102 lines
6.2 KiB
Markdown
102 lines
6.2 KiB
Markdown
# 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-gate` note (agent memory) — CI traps + what's already done.
|
|
- `docker-compose.yml` (demo images; `node:24-slim` done, dotnet SDK still full).
|
|
|
|
## Candidate items (pick per impact once measured)
|
|
|
|
1. **Skip `npm ci` install via a `node_modules` cache.** `actions/cache` on `node_modules`
|
|
keyed by `package-lock.json` hash; on a hit, `npm ci` is 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.
|
|
2. **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-slim` on the node-only jobs (`frontend`,
|
|
`storybook-a11y`), dropping `setup-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.
|
|
3. **Path-filtered jobs.** Skip `backend` on FE-only changes and vice-versa (workflow `paths:`
|
|
or `dorny/paths-filter`). Cuts compute on narrow PRs; watch required-check rules that expect
|
|
every job to report a status.
|
|
4. **Split a fast `lint` job** (lint + format:check + check:tokens) for ~1 min fail-fast
|
|
feedback — only worth it once item 1 (node_modules cache) lands, else it duplicates `npm ci`.
|
|
5. **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 separate `docker-compose.prod.yml`. The
|
|
dev `docker-compose.yml` keeps the SDK image because `dotnet run` hot-reload needs it.
|
|
6. **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`, `.npmrc` min-release-age) rather than
|
|
app-code vulns. Triage them (fix or `# nosemgrep`/`.semgrepignore` the noise; consider a
|
|
tighter ruleset than `p/default` if the GitHub-Actions-policy rules aren't wanted), then add
|
|
`--error` to `semgrep scan` so 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
|
|
|
|
- [x] 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.
|
|
- [x] `npm run ci` still passes locally after any workflow/script change (confirmed 2026-07-30,
|
|
full run including backend `dotnet test`/`dotnet format` and 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.
|