Skip to content

Commit a53fab5

Browse files
authored
Speed up CI/CD runs, scope the developer CLI, and harden workflow hygiene (#895)
### Summary & Motivation A coordinated refresh of the CI/CD pipelines and developer CLI: faster runs, scoped commands, better observability, and consistent workflow conventions. #### Test infrastructure `BackOfficeEndpointBaseTest` and `EndpointBaseTest<TContext>` built a fresh `WebApplicationFactory<Program>` per test instance. With `Meziantou.Xunit.ParallelTestFramework`, every Account-API and BackOffice test paid a full ASP.NET Core cold-start. Both bases now share one host per test class via `IClassFixture<...>`, routing per-test state (SqliteConnection, telemetry collector, MockStripeState, `IEmailClient`) through `AsyncLocal`. New types: `BackOfficeWebApplicationFactory` + `BackOfficeTestContext` (26 derived classes) and `AccountWebApplicationFactory` + `AccountTestContext` (60 derived classes). `MockStripeState` becomes transient so per-request resolution reads the per-test instance; `TestServer.PreserveExecutionContext = true` keeps the AsyncLocal flowing into request handling. #### Single Code Style workflow A new `code-style.yml` replaces the per-SCS code-style work in `account.yml`, `main.yml`, `app-gateway.yml`. Four jobs: - `detect-scope` — classifies the diff via inline `git diff`, outputs the backend scope and format mode for the downstream jobs. - `code-linting` — backend inspectcode + frontend build + frontend lint (oxlint). - `code-formatting` — backend cleanupcode + frontend format check (oxfmt). - `sonarcloud` — runs once per push (previously three times, once per SCS), sequentially per slnf to keep test assemblies in separate processes and avoid the `PortAllocation.Load()` race. Backend lint and format are scoped to a single SCS via `--self-contained-system <name>` when the diff touches only that SCS; cross-cutting changes fall back to the full solution. Code coverage is out of scope here — the dotCover path repeatedly exceeded 20 minutes against the new IClassFixture infrastructure and is tracked separately. #### AppGateway tests now run in CI `AppGateway.Tests` existed in the solution but was never executed — the slnf-scoped runs missed it, and `app-gateway.yml` had no test step. The workflow now runs `dotnet test AppGateway.Tests/AppGateway.Tests.csproj --no-build` with the same user-secret bootstrap the other workflows use. #### Developer CLI: `--gateway/-g` scope flag `build`, `test`, `format`, and `lint` accept `--gateway`/`-g` to scope to AppGateway only (mutually exclusive with `-s`). `pp test -g` targets `AppGateway.Tests/AppGateway.Tests.csproj`; lint/format scope JetBrains tools to `AppGateway/**` + `AppGateway.Tests/**`. Shared logic in `AppGatewayHelper`. #### Faster format and lint - `format` defaults to changed-only (`.cs` files diffed vs `origin/main`); `--all-files` for full sweep. CI auto-flips to `--all-files` when `application/dotnet-tools.json` changes, so JetBrains tool upgrades catch latent drift. - `lint --changed-only` is opt-in; CI lints full because inspectcode has cross-file rules. - Dropped the temp-`.slnf` workaround for `cleanupcode` — JetBrains 2026.1 accepts `.slnx` directly. #### Workflow hygiene - **Concurrency** on every top-level workflow with `cancel-in-progress: true` (and `false` for reusable deploy workflows so mid-deploy cancellations cannot leave a half-applied state). - **Inspection results on failure**: `code-style.yml` and `developer-cli.yml` dump `result.json` to the run console and `$GITHUB_STEP_SUMMARY`. Replaces the brittle `grep | tee` exit-code contract. - Removed leftover SonarScanner steps and Java JDK setup from `account.yml` and `main.yml` (centralized in `code-style.yml`). - Removed stale `_preview-migrations.yml` trigger-path references (file does not exist). - YAML hygiene pass across all workflows: blank lines, canonical argument order, trailing whitespace. #### Database Plan reuses Build and Test artifacts `build-and-test` uploads `application/**/bin` and `application/**/obj` as a `<scs>-build` artifact (when staging is enabled). `_migrate-database.yml` downloads it and restores NuGet, skipping the redundant checkout + setup + build chain. #### E2E test fix piggy-backed on this branch Two e2e tests inherited from the recently-merged feature-flags work failed on a default-seeded database (`feature-flag-flows.spec.ts:262`, `user-management-flows.spec.ts:473`). Root cause: `account-overview` and `compact-view` are registered with `isKillSwitchEnabled: true`, so the startup reconciler creates them inactive (`EnabledAt = null`); the configurable-flags handlers filter inactive rows out, the API returns empty, and `FeaturesSection` / `PreferencesFeatureFlagsSection` render `null`. The tests now activate the kill-switch flags via the back-office activate endpoint during setup — test-only fix, no production code, no seeder change. #### CI benchmark The biggest CI win comes from the **Database Staging migration-skip gate** on `account.yml` / `main.yml` — PRs that do not change EF migration files skip the Database Staging job entirely. | Workflow | BEFORE | AFTER, no migrations | AFTER, with migrations | Δ (no migrations) | Δ (with migrations) | |---|---|---|---|---|---| | Account | 9m00s | 2m49s | 6m17s | -69% | -30% | | Main | 6m41s | 1m55s | 5m40s | -71% | -15% | | AppGateway | 7m26s | 1m26s | 1m26s | -81% | -81% | | Code Style | - | 4m48s | 4m48s | new | new | | **Critical path per push** | **9m00s** | **4m48s** | **6m17s** | **-47%** | **-30%** | | Total CI minutes per push | 23m07s | 15m19s | 22m34s | -34% | -2% | AppGateway's delta is large because `code-linting` and `code-formatting` moved out of `app-gateway.yml` into the consolidated `code-style.yml`. On the no-migrations path the critical path now sits on `Code Style` (parallel lint/format/sonar jobs each paying setup); sharing the backend build artifact across them is an obvious follow-up. On the with-migrations path the critical path stays on `account.yml`. Downstream projects with a substantial Main SCS will see a bigger lift than this suggests — the slowest CI step used to be backend `cleanupcode` running on every file; it now inspects only changed `.cs` files. Main and Account now sit close in wall-clock despite Account having materially more code, because the npm + dotnet restore + build setup now dominates. #### Test-step benchmark (isolated) Test-host refactor measured in isolation (Code Style and Database Staging disabled; 5 samples per side): | | Run 1 | Run 2 | Run 3 | Run 4 | Run 5 | Mean | Stddev | |---|---|---|---|---|---|---|---| | AFTER (BackOffice + Account shared host) | 2m18s | 2m26s | 2m21s | 2m21s | 2m21s | **2m21s** | 2.9s | | BEFORE (per-test host, baseline) | 3m46s | 3m51s | 3m38s | 3m41s | 3m54s | **3m46s** | 6.7s | Δ -37% on the test step alone, stddev more than halves — the runs are markedly more stable too. ### Checklist - [x] I have added tests, or done manual regression tests - [x] I have updated the documentation, if necessary
2 parents ad4fc63 + 1bf55a9 commit a53fab5

124 files changed

Lines changed: 1354 additions & 732 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/format/SKILL.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ description: Auto-format code via the developer CLI - backend (.NET via JetBrain
66
# Format
77

88
```bash
9-
dotnet run --project developer-cli -- format [--backend] [--frontend] [--cli] [--self-contained-system <name>] [--no-build] --quiet
9+
dotnet run --project developer-cli -- format [--backend] [--frontend] [--cli] [--self-contained-system <name>] [--no-build] [--all-files] --quiet
1010
```
1111

1212
Use `developer-cli` exactly as written - do not expand to an absolute worktree path.
@@ -16,8 +16,9 @@ Use `developer-cli` exactly as written - do not expand to an absolute worktree p
1616
- `--cli` - the developer CLI itself
1717
- `--self-contained-system <name>` - narrows backend formatting to one SCS (e.g. `account`, `main`)
1818
- `--no-build` - skip the `dotnet tool restore` step (faster after a recent run)
19+
- `--all-files` - format every file in the solution. Default is to format only `.cs` files changed against `origin/main` (faster).
1920

20-
No arguments formats everything. Unformatted code fails CI - commit all changes, never revert.
21+
No arguments formats everything (changed-only by default). Unformatted code fails CI - commit all changes, never revert.
2122

2223
After `build` succeeds, run `format`, `lint`, `test` in parallel with `--no-build`.
2324

.claude/skills/lint/SKILL.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ description: Lint code via the developer CLI - backend (.NET via JetBrains inspe
66
# Lint
77

88
```bash
9-
dotnet run --project developer-cli -- lint [--backend] [--frontend] [--cli] [--self-contained-system <name>] [--no-build] --quiet
9+
dotnet run --project developer-cli -- lint [--backend] [--frontend] [--cli] [--self-contained-system <name>] [--no-build] [--changed-only] --quiet
1010
```
1111

1212
Use `developer-cli` exactly as written - do not expand to an absolute worktree path.
@@ -16,18 +16,28 @@ Use `developer-cli` exactly as written - do not expand to an absolute worktree p
1616
- `--cli` - the developer CLI itself
1717
- `--self-contained-system <name>` - narrows backend linting to one SCS (e.g. `account`, `main`)
1818
- `--no-build` - skip the rebuild step (faster after a recent build)
19+
- `--changed-only` - lint only `.cs` files changed against `origin/main` (much faster; see guidance below)
1920

20-
No arguments lints everything. Every finding fails CI regardless of severity - fix all of them.
21+
No arguments lints the whole solution. Every finding fails CI regardless of severity - fix all of them.
2122

2223
After `build` succeeds, run `format`, `lint`, `test` in parallel with `--no-build`. Backend lint is slow - run last. Frontend lint often needs code rewrites - run after each bigger change.
2324

25+
## When to use `--changed-only`
26+
27+
Inspectcode has cross-file rules ("unused public method", "member can be private", flow analysis across method calls). `--changed-only` only inspects the listed files - it doesn't catch issues in untouched files that became invalid because of edits elsewhere.
28+
29+
- **Routine work:** use `--changed-only`. Most lint findings are local (style, naming, hints) and the saving is large (~4m → ~30s).
30+
- **Larger changes that affect other files** (refactoring a public API, deleting a method's only caller, changing a widely-used type): omit `--changed-only` and lint the full solution.
31+
32+
CI always lints the full solution, so anything missed by a local `--changed-only` run gets caught before merge.
33+
2434
## Examples
2535

2636
```bash
27-
dotnet run --project developer-cli -- lint --quiet # everything
28-
dotnet run --project developer-cli -- lint --backend --quiet # all backend
29-
dotnet run --project developer-cli -- lint --frontend --quiet # frontend
30-
dotnet run --project developer-cli -- lint --backend --self-contained-system main --quiet # one SCS
37+
dotnet run --project developer-cli -- lint --quiet # everything (full solution)
38+
dotnet run --project developer-cli -- lint --backend --changed-only --quiet # backend, changed files only (recommended for routine work)
39+
dotnet run --project developer-cli -- lint --frontend --quiet # frontend
40+
dotnet run --project developer-cli -- lint --backend --self-contained-system main --quiet # one SCS, full
3141
```
3242

3343
## Always pass --quiet

.github/workflows/_deploy-container.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ on:
3838
required: true
3939
type: string
4040

41+
concurrency:
42+
group: ${{ inputs.image_name }}-${{ inputs.azure_environment }}-deploy
43+
cancel-in-progress: false
44+
4145
jobs:
4246
deploy:
4347
name: Deploy

.github/workflows/_deploy-infrastructure.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ on:
4949
type: string
5050
default: "-"
5151

52+
concurrency:
53+
group: ${{ inputs.unique_prefix }}-${{ inputs.azure_environment }}-infrastructure
54+
cancel-in-progress: false
55+
5256
jobs:
5357
plan:
5458
name: Plan
@@ -67,6 +71,7 @@ jobs:
6771
should_deploy="false"
6872
fi
6973
echo "should_deploy=$should_deploy" >> $GITHUB_OUTPUT
74+
7075
- name: Checkout Code
7176
uses: actions/checkout@v6
7277

.github/workflows/_migrate-database.yml

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ on:
3232
apply_migrations:
3333
required: true
3434
type: boolean
35+
build_artifact_name:
36+
description: "Name of the artifact uploaded by the caller's build-and-test job that contains application/**/bin and application/**/obj"
37+
required: true
38+
type: string
3539

3640
outputs:
3741
has_migrations_to_apply:
@@ -68,9 +72,18 @@ jobs:
6872
working-directory: application
6973
run: dotnet tool restore
7074

71-
- name: Build Backend Solution
75+
- name: Download Backend Build Artifacts
76+
uses: actions/download-artifact@v8
77+
with:
78+
name: ${{ inputs.build_artifact_name }}
79+
path: application
80+
81+
# The artifact carries bin/ and obj/ but not the global NuGet package cache.
82+
# `dotnet ef --no-build` loads compiled assemblies that reference packages by
83+
# absolute path under ~/.nuget/packages — those must exist on this runner.
84+
- name: Restore .NET Dependencies
7285
working-directory: application
73-
run: dotnet build ${{ inputs.relative_startup_project }}
86+
run: dotnet restore ${{ inputs.relative_startup_project }}
7487

7588
- name: Login to Azure
7689
uses: azure/login@v3
@@ -93,7 +106,7 @@ jobs:
93106
run: |
94107
ENTRA_USER=$(az postgres flexible-server microsoft-entra-admin list --resource-group ${{ env.CLUSTER_RESOURCE_GROUP_NAME }} --server-name ${{ env.POSTGRES_SERVER_NAME }} --query "[0].principalName" --output tsv)
95108
CONNECTION_STRING="Host=${{ env.POSTGRES_HOST }};Database=${{ inputs.database_name }};Username=$ENTRA_USER;Password=$(az account get-access-token --resource-type oss-rdbms --query accessToken --output tsv);Ssl Mode=VerifyFull;"
96-
109+
97110
echo "Checking for pending migrations..."
98111
MIGRATION_INFO=$(dotnet ef migrations list \
99112
--project ${{ inputs.relative_project_path }} \
@@ -102,12 +115,12 @@ jobs:
102115
--connection "$CONNECTION_STRING" \
103116
--no-build \
104117
--json)
105-
118+
106119
MIGRATION_JSON=$(echo "$MIGRATION_INFO" | sed -n '/^[{[]/,$p')
107120
PENDING_MIGRATIONS_JSON=$(echo "$MIGRATION_JSON" | jq '[.[] | select(.applied == false)]')
108121
PENDING_MIGRATIONS_COUNT=$(echo "$PENDING_MIGRATIONS_JSON" | jq '. | length')
109122
LAST_APPLIED_MIGRATION=$(echo "$MIGRATION_JSON" | jq -r '[.[] | select(.applied == true) | .id] | sort | last // "0"')
110-
123+
111124
if [ "$PENDING_MIGRATIONS_COUNT" -gt "0" ]; then
112125
LAST_PENDING_MIGRATION=$(echo "$PENDING_MIGRATIONS_JSON" | jq -r '.[-1].id')
113126
echo "$PENDING_MIGRATIONS_COUNT pending migration(s) detected:"
@@ -122,13 +135,13 @@ jobs:
122135
--idempotent \
123136
--no-build \
124137
--output migration.sql
125-
138+
126139
echo "has_migrations_to_apply=true" >> $GITHUB_OUTPUT
127-
140+
128141
echo "migration_script<<EOF" >> $GITHUB_OUTPUT
129142
cat migration.sql >> $GITHUB_OUTPUT
130143
echo "EOF" >> $GITHUB_OUTPUT
131-
144+
132145
echo "migration_json<<EOF" >> $GITHUB_OUTPUT
133146
echo "$PENDING_MIGRATIONS_JSON" >> $GITHUB_OUTPUT
134147
echo "EOF" >> $GITHUB_OUTPUT
@@ -165,22 +178,22 @@ jobs:
165178
with:
166179
script: |
167180
const migrationJson = JSON.parse(process.env.MIGRATION_JSON);
168-
181+
169182
const migrationsList = migrationJson.map(m => `- ${m.name} (${m.id})`).join('\n');
170-
183+
171184
const migrationInfo = `## Approve Database Migration \`${{ inputs.database_name }}\` database on \`${{ inputs.azure_environment }}\`
172-
185+
173186
The following pending migration(s) will be applied to the database when approved:
174187
${migrationsList}
175-
188+
176189
### Migration Script
177190
\`\`\`sql
178191
${process.env.MIGRATION_SCRIPT}
179192
\`\`\`
180193
`;
181-
194+
182195
console.log(migrationInfo);
183-
196+
184197
core.setOutput('markdown', migrationInfo);
185198
186199
- name: Add Migration Information to Pull Request

0 commit comments

Comments
 (0)