Skip to content
fullstackhero

Reference

CI/CD Pipeline

The GitHub Actions setup — path-scoped backend and frontend pipelines, a pinned SDK, deduplicated test + coverage, CodeQL, the template smoke guard, and how container images and NuGet packages are published.

views 0 Last updated

The kit ships four GitHub Actions workflows in .github/workflows/:

WorkflowPurpose
backend.ymlBuilds, tests, and publishes the .NET backend (API container + NuGet packages). Runs only when backend code changes.
frontend.ymlLints, builds, and E2E-tests the two React apps. Runs only when clients/** changes.
codeql.ymlCodeQL security analysis on backend PRs + a weekly scan.
template-smoke.ymlGuards 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 under src/** 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.json pins the .NET 10 GA SDK (rollForward: latestFeature), and every workflow resolves it with setup-dotnet’s global-json-file. CI is deterministic and never silently pulls a preview SDK. (global.json is excluded from the dotnet new template, 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.

JobDepends onRuns whenWhat it does
Detect changesalwaysA 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 TestsDetect changesbackend changeddotnet 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 TestsDetect changesbackend changedIntegration.Tests + Integration.Middleware.Tests via WebApplicationFactory + Testcontainers (Docker on the runner), with coverage collection. Uploads its coverage as an artifact.
DbMigrator Container SmokeDetect changesbackend changedPublishes 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 GateUnit + Integrationbackend changedMerges 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 ContainersUnit + Integrationpush to mainPublishes the API image to GHCR tagged dev-<sha> and dev-latest.
Publish ReleaseUnit + Integrationv* tag, or workflow_dispatch on mainPacks NuGet packages + the template, pushes to NuGet.org, and publishes the API image to GHCR tagged <version> and latest.
Backend CIall of the abovealwaysThe 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 jobBackend 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:

Terminal window
# Tag-driven (recommended): the tag name is the version
git 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.

JobRuns whenWhat it does
Detect changesalwaysSets frontend = true on any push/dispatch, or on a PR when clients/** (or the workflow) changed.
Lint & Build (matrix)frontend changednpm 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 changednpm 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 CIalwaysThe 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, runs dotnet new fsh ... --aspire true --frontend true, builds the backend (-warnaserror), builds both React apps (npm ci && npm run build on 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 in AppHost.cs and 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 build

Then 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:

Terminal window
dotnet build src/FSH.Starter.slnx -c Release -warnaserror # Unit Tests build gate
dotnet test src/FSH.Starter.slnx -c Release # Unit + Integration (needs Docker)

For the frontend, in each of clients/admin and clients/dashboard:

Terminal window
npm ci && npm run lint && npm run build # Lint & Build
npm run test:e2e # E2E (route-mocked; boots Vite itself)