The kit ships four GitHub Actions workflows in .github/workflows/:
| Workflow | Purpose |
|---|---|
backend.yml | Builds, tests, and publishes the .NET backend (API container + NuGet packages). Runs only when backend code changes. |
frontend.yml | Lints, builds, and E2E-tests the two React apps. Runs only when clients/** changes. |
codeql.yml | CodeQL security analysis on backend PRs + a weekly scan. |
template-smoke.yml | Guards the distribution path: scaffolding from the dotnet new template must always build. |
Two design choices shape everything below:
- Path-scoped pipelines. A change under
clients/**never builds or tests the API, and a change undersrc/**never builds the React apps. You only pay for — and wait on — the checks relevant to what you touched. - A pinned SDK. A root
global.jsonpins the .NET 10 GA SDK (rollForward: latestFeature), and every workflow resolves it withsetup-dotnet’sglobal-json-file. CI is deterministic and never silently pulls a preview SDK. (global.jsonis excluded from thedotnet newtemplate, so scaffolded projects aren’t pinned to the kit’s SDK band.)
backend.yml — build, test, publish
Triggers on pushes to main, v* tags, and PRs to main, plus manual workflow_dispatch (with an optional version input). PR runs cancel in-progress duplicates.
| Job | Depends on | Runs when | What it does |
|---|---|---|---|
| Detect changes | — | always | A dorny/paths-filter step decides whether backend code changed. On any push/tag/dispatch it’s always true; on a PR it’s true only when src/**, global.json, coverage.runsettings, or the workflow itself changed. Every heavy job gates on this. |
| Unit Tests | Detect changes | backend changed | dotnet build -c Release -warnaserror (the build gate), an audit that fails on directly-referenced vulnerable packages (transitive advisories are reported, not blocking), then the 12 unit/architecture test projects with coverage collection. Uploads the coverage as an artifact. |
| Integration Tests | Detect changes | backend changed | Integration.Tests + Integration.Middleware.Tests via WebApplicationFactory + Testcontainers (Docker on the runner), with coverage collection. Uploads its coverage as an artifact. |
| DbMigrator Container Smoke | Detect changes | backend changed | Publishes the fsh-db-migrator image (Dockerfile-less SDK publish) and runs apply --catalog-only against an ephemeral Postgres 17 — asserts it finishes successfully. Catches container-publish and DI-graph regressions. |
| Coverage Gate | Unit + Integration | backend changed | Merges the coverage artifacts from the two test jobs with ReportGenerator and fails if line coverage drops below 80%. It does not re-run the tests — a ratchet; raise MIN_LINE as coverage grows. |
| Publish Dev Containers | Unit + Integration | push to main | Publishes the API image to GHCR tagged dev-<sha> and dev-latest. |
| Publish Release | Unit + Integration | v* tag, or workflow_dispatch on main | Packs NuGet packages + the template, pushes to NuGet.org, and publishes the API image to GHCR tagged <version> and latest. |
| Backend CI | all of the above | always | The single required status check. Always runs (even when the heavy jobs are skipped on a client-only PR) and fails only if a job it depends on actually failed or was cancelled — skipped is fine. |
Required status checks and the gate jobs
Because the pipelines are path-scoped, a client-only PR never starts the backend jobs — so a branch-protection rule that required, say, “Unit Tests” directly would block forever waiting on a check that never reports. To avoid that, each workflow ends with a small gate job — Backend CI and Frontend CI — that always runs and turns green when its side is skipped.
Require only Backend CI and Frontend CI in your branch ruleset on main — not the individual jobs. They resolve correctly for backend-only, frontend-only, and cross-cutting PRs alike.
Releasing
A release publishes both NuGet packages (the BuildingBlocks, selected module Contracts, the fsh CLI, and the dotnet new template) and the API container image. Two ways to trigger it:
# Tag-driven (recommended): the tag name is the versiongit tag v10.0.0 && git push origin v10.0.0…or run the Backend CI workflow manually from the Actions tab on main with a version input (e.g. 10.0.0-rc.1).
frontend.yml — lint, build, E2E
Triggers on pushes and PRs to main (and manual dispatch). A Detect changes job gates the work on clients/**. Both apps run as a matrix (admin, dashboard) on Node 22 with npm caching.
| Job | Runs when | What it does |
|---|---|---|
| Detect changes | always | Sets frontend = true on any push/dispatch, or on a PR when clients/** (or the workflow) changed. |
| Lint & Build (matrix) | frontend changed | npm ci, npm run lint (ESLint), then npm run build — which is tsc -b && vite build, so it’s the typecheck and the bundle gate. |
| E2E (matrix) | frontend changed | npm ci, installs the Playwright Chromium browser, then npm run test:e2e. The Playwright config boots its own Vite dev server and mocks all API calls via page.route(), so no backend is required. Uploads the Playwright HTML report on failure. |
| Frontend CI | always | The single required status check for the frontend side (same always-green-when-skipped behaviour as Backend CI). |
codeql.yml — security analysis
Runs CodeQL (csharp) on pull requests to main scoped to src/**, plus a scheduled weekly scan. Uses the pinned SDK via global.json.
template-smoke.yml — distribution guard
Runs on changes to .template.config/**, templates/**, clients/**, or src/**. It proves a freshly scaffolded project actually builds:
- Scaffold (Aspire + React) — packs the template, installs the
.nupkg, runsdotnet new fsh ... --aspire true --frontend true, builds the backend (-warnaserror), builds both React apps (npm ci && npm run buildon Node 22), and runs the Architecture tests on the scaffolded output. - Scaffold (minimal) — scaffolds backend-only (
--aspire false --frontend false) and builds it, guarding the//#if (frontend)/(!aspire)conditional gating inAppHost.csand the solution.
How CI feeds deployment
The image CI publishes is what you deploy. Reference it by its immutable tag in your environment’s terraform.tfvars:
container_image_tag = "v10.0.0" # or a dev-<sha> tag from a main buildThen deploy — see Deploy to AWS with Terraform. For a fully automated path, run the one-command deploy from your own deploy workflow after the image is published.
Local parity
Every backend CI check has a local equivalent, so you can reproduce a red build before pushing:
dotnet build src/FSH.Starter.slnx -c Release -warnaserror # Unit Tests build gatedotnet test src/FSH.Starter.slnx -c Release # Unit + Integration (needs Docker)For the frontend, in each of clients/admin and clients/dashboard:
npm ci && npm run lint && npm run build # Lint & Buildnpm run test:e2e # E2E (route-mocked; boots Vite itself)