From 5af2d27751a6ad0b78ab5535a6f04f061cf6a1b3 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Thu, 17 Sep 2026 17:05:14 -0700 Subject: [PATCH 1/5] feat(ci): add Windows build and test workflow Add a lightweight windows-latest CI job mirroring the existing macOS workflow (restore, build, test) to catch Windows-specific platform regressions alongside the Ubuntu and macOS checks. --- .github/workflows/windows-build-and-test.yml | 50 ++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/windows-build-and-test.yml diff --git a/.github/workflows/windows-build-and-test.yml b/.github/workflows/windows-build-and-test.yml new file mode 100644 index 00000000..e7619346 --- /dev/null +++ b/.github/workflows/windows-build-and-test.yml @@ -0,0 +1,50 @@ +name: Windows Build and Test EssentialCSharp.Web + +on: + pull_request: + branches: ["main"] + merge_group: + workflow_dispatch: + +jobs: + build-and-test: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v7 + + - name: Set up .NET Core + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: "26" + cache: npm + cache-dependency-path: EssentialCSharp.Web/package-lock.json + + - name: Set up dependency caching for faster builds + uses: actions/cache@v6 + with: + path: | + ~/.nuget/packages + ${{ github.workspace }}/**/obj/project.assets.json + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + ${{ runner.os }}-nuget- + + - name: Restore with dotnet + run: dotnet restore /p:AccessToNugetFeed=false + + - name: Install npm dependencies + working-directory: EssentialCSharp.Web + run: npm ci + + - name: Build with dotnet + run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false + + - name: Run .NET Tests + run: dotnet test --no-build --configuration Release From 67df8b9fc2afa1cbd1e416bfbd542594477c58eb Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Thu, 17 Sep 2026 21:42:13 -0700 Subject: [PATCH 2/5] refactor(ci): consolidate platform validation Use one cross-platform .NET matrix alongside explicit frontend, EF Core, and container validation jobs. Align Node setup through .nvmrc, add CI concurrency and timeouts, and prepare CodeQL for merge queues. --- .github/workflows/Build-Test-And-Deploy.yml | 26 +++- .github/workflows/codeql.yml | 13 +- .github/workflows/copilot-setup-steps.yml | 15 ++- .github/workflows/macos-build-and-test.yml | 50 -------- ...R-Build-And-Test.yml => pr-validation.yml} | 120 +++++++++++++++--- .github/workflows/windows-build-and-test.yml | 50 -------- EssentialCSharp.Web/.nvmrc | 1 + 7 files changed, 146 insertions(+), 129 deletions(-) delete mode 100644 .github/workflows/macos-build-and-test.yml rename .github/workflows/{PR-Build-And-Test.yml => pr-validation.yml} (53%) delete mode 100644 .github/workflows/windows-build-and-test.yml create mode 100644 EssentialCSharp.Web/.nvmrc diff --git a/.github/workflows/Build-Test-And-Deploy.yml b/.github/workflows/Build-Test-And-Deploy.yml index 7009fa10..767fc688 100644 --- a/.github/workflows/Build-Test-And-Deploy.yml +++ b/.github/workflows/Build-Test-And-Deploy.yml @@ -6,12 +6,12 @@ on: workflow_dispatch: permissions: - id-token: write contents: read jobs: build-and-test: runs-on: ubuntu-latest + timeout-minutes: 45 environment: "BuildAndUploadImage" steps: @@ -28,10 +28,20 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v7 with: - node-version: 24 + node-version-file: EssentialCSharp.Web/.nvmrc cache: npm cache-dependency-path: EssentialCSharp.Web/package-lock.json + - name: Verify Node version matches the Docker build + shell: bash + run: | + expected_node_version="$(tr -d '[:space:]' < EssentialCSharp.Web/.nvmrc)" + docker_node_version="$(sed -nE 's/^FROM node:([0-9]+)-.*/\1/p' EssentialCSharp.Web/Dockerfile | head -n 1)" + if [[ "$expected_node_version" != "$docker_node_version" ]]; then + echo "::error::EssentialCSharp.Web/.nvmrc specifies Node $expected_node_version, but the Dockerfile uses Node $docker_node_version." + exit 1 + fi + - name: Set up dependency caching for faster builds uses: actions/cache@v6 id: nuget-cache @@ -47,8 +57,16 @@ jobs: - name: Restore with dotnet run: dotnet restore + - name: Install npm dependencies + working-directory: EssentialCSharp.Web + run: npm ci + + - name: Build frontend + working-directory: EssentialCSharp.Web + run: npm run build + - name: Build with dotnet - run: dotnet build -p:ContinuousIntegrationBuild=True -p:ReleaseDateAttribute=True --configuration Release --no-restore + run: dotnet build -p:ContinuousIntegrationBuild=True -p:ReleaseDateAttribute=True -p:SkipFrontendBuild=true --configuration Release --no-restore - name: Expose GitHub Actions Runtime uses: actions/github-script@v9 @@ -115,6 +133,7 @@ jobs: deploy-development: if: github.event_name != 'pull_request_target' && github.event_name != 'pull_request' runs-on: ubuntu-latest + timeout-minutes: 45 needs: build-and-test concurrency: group: deploy-development @@ -174,6 +193,7 @@ jobs: deploy-production: if: github.event_name != 'pull_request_target' && github.event_name != 'pull_request' runs-on: ubuntu-latest + timeout-minutes: 45 needs: [deploy-development] concurrency: group: deploy-production diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9d3cdf5d..5d0fc69a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -5,14 +5,19 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + merge_group: schedule: - cron: '21 15 * * 5' +concurrency: + group: codeql-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: analyze-csharp: name: Analyze C# (CodeQL) runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + timeout-minutes: 60 permissions: actions: read contents: read @@ -36,6 +41,8 @@ jobs: - name: Set up .NET Core uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json - name: Set up dependency caching for faster builds uses: actions/cache@v6 @@ -52,7 +59,7 @@ jobs: - name: Restore with dotnet run: | dotnet restore /p:AccessToNugetFeed=false - dotnet build --configuration Release --no-restore --no-incremental /p:AccessToNugetFeed=false + dotnet build --configuration Release --no-restore --no-incremental /p:AccessToNugetFeed=false /p:SkipFrontendBuild=true - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 @@ -62,7 +69,7 @@ jobs: analyze-non-compiled-languages: name: Analyze Non-Compiled Languages (CodeQL) runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + timeout-minutes: 30 permissions: actions: read contents: read diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 215c78fd..d0c1fb29 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -15,6 +15,7 @@ permissions: jobs: copilot-setup-steps: runs-on: ubuntu-latest + timeout-minutes: 30 permissions: contents: read steps: @@ -31,7 +32,9 @@ jobs: - name: Set up Node.js for frontend development uses: actions/setup-node@v7 with: - node-version: '24' + node-version-file: EssentialCSharp.Web/.nvmrc + cache: npm + cache-dependency-path: EssentialCSharp.Web/package-lock.json - name: Set up dependency caching for faster builds uses: actions/cache@v6 @@ -61,8 +64,16 @@ jobs: - name: Restore with dotnet run: dotnet restore + - name: Install npm dependencies + working-directory: EssentialCSharp.Web + run: npm ci + + - name: Build frontend + working-directory: EssentialCSharp.Web + run: npm run build + - name: Build with dotnet - run: dotnet build -p:ContinuousIntegrationBuild=True -p:ReleaseDateAttribute=True --configuration Release --no-restore + run: dotnet build -p:ContinuousIntegrationBuild=True -p:ReleaseDateAttribute=True -p:SkipFrontendBuild=true --configuration Release --no-restore - name: Run .NET Tests run: dotnet test --no-build --configuration Release diff --git a/.github/workflows/macos-build-and-test.yml b/.github/workflows/macos-build-and-test.yml deleted file mode 100644 index cc88c0ed..00000000 --- a/.github/workflows/macos-build-and-test.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: macOS Build and Test EssentialCSharp.Web - -on: - pull_request: - branches: ["main"] - merge_group: - workflow_dispatch: - -jobs: - build-and-test: - runs-on: macos-latest - - steps: - - uses: actions/checkout@v7 - - - name: Set up .NET Core - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Set up Node.js - uses: actions/setup-node@v7 - with: - node-version: "26" - cache: npm - cache-dependency-path: EssentialCSharp.Web/package-lock.json - - - name: Set up dependency caching for faster builds - uses: actions/cache@v6 - with: - path: | - ~/.nuget/packages - ${{ github.workspace }}/**/obj/project.assets.json - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - restore-keys: | - ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - ${{ runner.os }}-nuget- - - - name: Restore with dotnet - run: dotnet restore /p:AccessToNugetFeed=false - - - name: Install npm dependencies - working-directory: EssentialCSharp.Web - run: npm ci - - - name: Build with dotnet - run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false - - - name: Run .NET Tests - run: dotnet test --no-build --configuration Release diff --git a/.github/workflows/PR-Build-And-Test.yml b/.github/workflows/pr-validation.yml similarity index 53% rename from .github/workflows/PR-Build-And-Test.yml rename to .github/workflows/pr-validation.yml index cbe6fab4..f7a0ef83 100644 --- a/.github/workflows/PR-Build-And-Test.yml +++ b/.github/workflows/pr-validation.yml @@ -1,4 +1,4 @@ -name: PR Build and Test EssentialCSharp.Web +name: PR Validation EssentialCSharp.Web on: pull_request: @@ -6,16 +6,36 @@ on: merge_group: workflow_dispatch: +permissions: + contents: read + +concurrency: + group: pr-validation-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: frontend-build: + name: Build frontend runs-on: ubuntu-latest + timeout-minutes: 15 + steps: - uses: actions/checkout@v7 + - name: Verify Node version matches the Docker build + shell: bash + run: | + expected_node_version="$(tr -d '[:space:]' < EssentialCSharp.Web/.nvmrc)" + docker_node_version="$(sed -nE 's/^FROM node:([0-9]+)-.*/\1/p' EssentialCSharp.Web/Dockerfile | head -n 1)" + if [[ "$expected_node_version" != "$docker_node_version" ]]; then + echo "::error::EssentialCSharp.Web/.nvmrc specifies Node $expected_node_version, but the Dockerfile uses Node $docker_node_version." + exit 1 + fi + - name: Set up Node.js uses: actions/setup-node@v7 with: - node-version: '26' + node-version-file: EssentialCSharp.Web/.nvmrc cache: npm cache-dependency-path: EssentialCSharp.Web/package-lock.json @@ -28,18 +48,30 @@ jobs: run: npm run build build-and-test: - runs-on: ubuntu-latest + name: Build and test (${{ matrix.runner }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + collect_coverage: true + - runner: macos-latest + collect_coverage: false + - runner: windows-latest + collect_coverage: false + steps: - uses: actions/checkout@v7 - - name: Set up .NET Core + - name: Set up .NET uses: actions/setup-dotnet@v6 with: global-json-file: global.json - - name: Set up dependency caching for faster builds + - name: Set up NuGet cache uses: actions/cache@v6 - id: nuget-cache with: path: | ~/.nuget/packages @@ -49,37 +81,83 @@ jobs: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} ${{ runner.os }}-nuget- - - name: Restore with dotnet + - name: Restore .NET dependencies run: dotnet restore /p:AccessToNugetFeed=false - - name: Restore local dotnet tools - run: dotnet tool restore - - - name: Build with dotnet - run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false - - - name: Check for pending EF Core model changes - run: dotnet tool run dotnet-ef -- migrations has-pending-model-changes --project EssentialCSharp.Web --configuration Release --no-build - env: - ASPNETCORE_ENVIRONMENT: Development + - name: Build .NET + run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false /p:SkipFrontendBuild=true - name: Expose GitHub Actions Runtime + if: matrix.collect_coverage uses: actions/github-script@v9 with: script: | core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN']); core.exportVariable('ACTIONS_RESULTS_URL', process.env['ACTIONS_RESULTS_URL']); - - name: Run .NET Tests + - name: Run .NET tests with coverage + if: matrix.collect_coverage run: dotnet test --no-build --configuration Release --report-trx --coverage --results-directory ${{ runner.temp }} - + + - name: Run .NET tests + if: ${{ !matrix.collect_coverage }} + run: dotnet test --no-build --configuration Release + - name: Convert TRX to VS Playlist - if: failure() + if: ${{ failure() && matrix.collect_coverage }} uses: BenjaminMichaelis/trx-to-vsplaylist@v4 with: trx-file-path: '${{ runner.temp }}/*.trx' output-directory: '${{ runner.temp }}/vsplaylists' + ef-core-model-validation: + name: Validate EF Core model + needs: build-and-test + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v7 + + - name: Set up .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Set up NuGet cache + uses: actions/cache@v6 + with: + path: | + ~/.nuget/packages + ${{ github.workspace }}/**/obj/project.assets.json + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + ${{ runner.os }}-nuget- + + - name: Restore .NET dependencies + run: dotnet restore /p:AccessToNugetFeed=false + + - name: Restore local .NET tools + run: dotnet tool restore + + - name: Build .NET + run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false /p:SkipFrontendBuild=true + + - name: Check for pending EF Core model changes + run: dotnet tool run dotnet-ef -- migrations has-pending-model-changes --project EssentialCSharp.Web --configuration Release --no-build + env: + ASPNETCORE_ENVIRONMENT: Development + + container-validation: + name: Build container image + needs: [frontend-build, build-and-test] + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - uses: actions/checkout@v7 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 id: buildx @@ -100,7 +178,7 @@ jobs: cache-dir: buildkit-cache skip-extraction: ${{ steps.buildkit-cache.outputs.cache-hit }} - - name: Build Container Image + - name: Build container image uses: docker/build-push-action@v7 with: file: ./EssentialCSharp.Web/Dockerfile diff --git a/.github/workflows/windows-build-and-test.yml b/.github/workflows/windows-build-and-test.yml deleted file mode 100644 index e7619346..00000000 --- a/.github/workflows/windows-build-and-test.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Windows Build and Test EssentialCSharp.Web - -on: - pull_request: - branches: ["main"] - merge_group: - workflow_dispatch: - -jobs: - build-and-test: - runs-on: windows-latest - - steps: - - uses: actions/checkout@v7 - - - name: Set up .NET Core - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Set up Node.js - uses: actions/setup-node@v7 - with: - node-version: "26" - cache: npm - cache-dependency-path: EssentialCSharp.Web/package-lock.json - - - name: Set up dependency caching for faster builds - uses: actions/cache@v6 - with: - path: | - ~/.nuget/packages - ${{ github.workspace }}/**/obj/project.assets.json - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - restore-keys: | - ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - ${{ runner.os }}-nuget- - - - name: Restore with dotnet - run: dotnet restore /p:AccessToNugetFeed=false - - - name: Install npm dependencies - working-directory: EssentialCSharp.Web - run: npm ci - - - name: Build with dotnet - run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false - - - name: Run .NET Tests - run: dotnet test --no-build --configuration Release diff --git a/EssentialCSharp.Web/.nvmrc b/EssentialCSharp.Web/.nvmrc new file mode 100644 index 00000000..6f4247a6 --- /dev/null +++ b/EssentialCSharp.Web/.nvmrc @@ -0,0 +1 @@ +26 From a14b53277cb45d1d774a0c75487eaded473c96d2 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Thu, 17 Sep 2026 21:48:08 -0700 Subject: [PATCH 3/5] refactor(ci): unify PR and deployment workflow Run platform validation from the existing build, test, and deploy workflow while restricting deployment to main pushes and manual dispatches. --- .github/workflows/Build-Test-And-Deploy.yml | 219 +++++++++++++++++--- .github/workflows/pr-validation.yml | 191 ----------------- 2 files changed, 192 insertions(+), 218 deletions(-) delete mode 100644 .github/workflows/pr-validation.yml diff --git a/.github/workflows/Build-Test-And-Deploy.yml b/.github/workflows/Build-Test-And-Deploy.yml index 767fc688..b2c30b0d 100644 --- a/.github/workflows/Build-Test-And-Deploy.yml +++ b/.github/workflows/Build-Test-And-Deploy.yml @@ -3,13 +3,56 @@ name: Build, Test, and Deploy EssentialCSharp.Web on: push: branches: ["main"] + pull_request: + branches: ["main"] + merge_group: workflow_dispatch: permissions: contents: read +concurrency: + group: ${{ github.event_name == 'pull_request' && format('build-test-deploy-pr-{0}', github.event.pull_request.number) || github.run_id }} + cancel-in-progress: true + jobs: + frontend-build: + name: Build frontend + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v7 + + - name: Verify Node version matches the Docker build + shell: bash + run: | + expected_node_version="$(tr -d '[:space:]' < EssentialCSharp.Web/.nvmrc)" + docker_node_version="$(sed -nE 's/^FROM node:([0-9]+)-.*/\1/p' EssentialCSharp.Web/Dockerfile | head -n 1)" + if [[ "$expected_node_version" != "$docker_node_version" ]]; then + echo "::error::EssentialCSharp.Web/.nvmrc specifies Node $expected_node_version, but the Dockerfile uses Node $docker_node_version." + exit 1 + fi + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version-file: EssentialCSharp.Web/.nvmrc + cache: npm + cache-dependency-path: EssentialCSharp.Web/package-lock.json + + - name: Install npm dependencies + working-directory: EssentialCSharp.Web + run: npm ci + + - name: Build frontend + working-directory: EssentialCSharp.Web + run: npm run build + build-and-test: + name: Build and test (Ubuntu) + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + needs: frontend-build runs-on: ubuntu-latest timeout-minutes: 45 environment: "BuildAndUploadImage" @@ -25,23 +68,6 @@ jobs: env: NUGET_AUTH_TOKEN: ${{ secrets.AZURE_DEVOPS_PAT }} - - name: Set up Node.js - uses: actions/setup-node@v7 - with: - node-version-file: EssentialCSharp.Web/.nvmrc - cache: npm - cache-dependency-path: EssentialCSharp.Web/package-lock.json - - - name: Verify Node version matches the Docker build - shell: bash - run: | - expected_node_version="$(tr -d '[:space:]' < EssentialCSharp.Web/.nvmrc)" - docker_node_version="$(sed -nE 's/^FROM node:([0-9]+)-.*/\1/p' EssentialCSharp.Web/Dockerfile | head -n 1)" - if [[ "$expected_node_version" != "$docker_node_version" ]]; then - echo "::error::EssentialCSharp.Web/.nvmrc specifies Node $expected_node_version, but the Dockerfile uses Node $docker_node_version." - exit 1 - fi - - name: Set up dependency caching for faster builds uses: actions/cache@v6 id: nuget-cache @@ -57,14 +83,6 @@ jobs: - name: Restore with dotnet run: dotnet restore - - name: Install npm dependencies - working-directory: EssentialCSharp.Web - run: npm ci - - - name: Build frontend - working-directory: EssentialCSharp.Web - run: npm run build - - name: Build with dotnet run: dotnet build -p:ContinuousIntegrationBuild=True -p:ReleaseDateAttribute=True -p:SkipFrontendBuild=true --configuration Release --no-restore @@ -130,8 +148,155 @@ jobs: name: essentialcsharpwebimage path: ${{ github.workspace }}/essentialcsharpwebimage.tar + pr-build-and-test: + name: Build and test (${{ matrix.runner }}) + if: github.event_name == 'pull_request' || github.event_name == 'merge_group' + needs: frontend-build + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + collect_coverage: true + - runner: macos-latest + collect_coverage: false + - runner: windows-latest + collect_coverage: false + + steps: + - uses: actions/checkout@v7 + + - name: Set up .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Set up NuGet cache + uses: actions/cache@v6 + with: + path: | + ~/.nuget/packages + ${{ github.workspace }}/**/obj/project.assets.json + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + ${{ runner.os }}-nuget- + + - name: Restore .NET dependencies + run: dotnet restore /p:AccessToNugetFeed=false + + - name: Build .NET + run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false /p:SkipFrontendBuild=true + + - name: Expose GitHub Actions Runtime + if: matrix.collect_coverage + uses: actions/github-script@v9 + with: + script: | + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN']); + core.exportVariable('ACTIONS_RESULTS_URL', process.env['ACTIONS_RESULTS_URL']); + + - name: Run .NET tests with coverage + if: matrix.collect_coverage + run: dotnet test --no-build --configuration Release --report-trx --coverage --results-directory ${{ runner.temp }} + + - name: Run .NET tests + if: ${{ !matrix.collect_coverage }} + run: dotnet test --no-build --configuration Release + + - name: Convert TRX to VS Playlist + if: ${{ failure() && matrix.collect_coverage }} + uses: BenjaminMichaelis/trx-to-vsplaylist@v4 + with: + trx-file-path: '${{ runner.temp }}/*.trx' + output-directory: '${{ runner.temp }}/vsplaylists' + + ef-core-model-validation: + name: Validate EF Core model + if: github.event_name == 'pull_request' || github.event_name == 'merge_group' + needs: pr-build-and-test + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v7 + + - name: Set up .NET + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Set up NuGet cache + uses: actions/cache@v6 + with: + path: | + ~/.nuget/packages + ${{ github.workspace }}/**/obj/project.assets.json + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + ${{ runner.os }}-nuget- + + - name: Restore .NET dependencies + run: dotnet restore /p:AccessToNugetFeed=false + + - name: Restore local .NET tools + run: dotnet tool restore + + - name: Build .NET + run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false /p:SkipFrontendBuild=true + + - name: Check for pending EF Core model changes + run: dotnet tool run dotnet-ef -- migrations has-pending-model-changes --project EssentialCSharp.Web --configuration Release --no-build + env: + ASPNETCORE_ENVIRONMENT: Development + + container-validation: + name: Build container image + if: github.event_name == 'pull_request' || github.event_name == 'merge_group' + needs: [frontend-build, pr-build-and-test] + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - uses: actions/checkout@v7 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + id: buildx + + - name: Restore BuildKit cache mounts + uses: actions/cache@v6 + id: buildkit-cache + with: + path: buildkit-cache + key: buildkit-cache-${{ hashFiles('EssentialCSharp.Web/Dockerfile', 'Directory.Packages.props', '**/*.csproj', 'EssentialCSharp.Web/package-lock.json') }} + restore-keys: buildkit-cache- + + - name: Inject BuildKit cache mounts + uses: reproducible-containers/buildkit-cache-dance@v3 + with: + builder: ${{ steps.buildx.outputs.name }} + dockerfile: EssentialCSharp.Web/Dockerfile + cache-dir: buildkit-cache + skip-extraction: ${{ steps.buildkit-cache.outputs.cache-hit }} + + - name: Build container image + uses: docker/build-push-action@v7 + with: + file: ./EssentialCSharp.Web/Dockerfile + context: . + push: false + cache-from: | + type=gha,scope=essentialcsharpweb-main + type=gha,scope=essentialcsharpweb-pr + cache-to: type=gha,mode=min,scope=essentialcsharpweb-pr + build-args: ACCESS_TO_NUGET_FEED=false + deploy-development: - if: github.event_name != 'pull_request_target' && github.event_name != 'pull_request' + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest timeout-minutes: 45 needs: build-and-test @@ -191,7 +356,7 @@ jobs: az account clear deploy-production: - if: github.event_name != 'pull_request_target' && github.event_name != 'pull_request' + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest timeout-minutes: 45 needs: [deploy-development] diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml deleted file mode 100644 index f7a0ef83..00000000 --- a/.github/workflows/pr-validation.yml +++ /dev/null @@ -1,191 +0,0 @@ -name: PR Validation EssentialCSharp.Web - -on: - pull_request: - branches: ["main"] - merge_group: - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: pr-validation-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - frontend-build: - name: Build frontend - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - uses: actions/checkout@v7 - - - name: Verify Node version matches the Docker build - shell: bash - run: | - expected_node_version="$(tr -d '[:space:]' < EssentialCSharp.Web/.nvmrc)" - docker_node_version="$(sed -nE 's/^FROM node:([0-9]+)-.*/\1/p' EssentialCSharp.Web/Dockerfile | head -n 1)" - if [[ "$expected_node_version" != "$docker_node_version" ]]; then - echo "::error::EssentialCSharp.Web/.nvmrc specifies Node $expected_node_version, but the Dockerfile uses Node $docker_node_version." - exit 1 - fi - - - name: Set up Node.js - uses: actions/setup-node@v7 - with: - node-version-file: EssentialCSharp.Web/.nvmrc - cache: npm - cache-dependency-path: EssentialCSharp.Web/package-lock.json - - - name: Install npm dependencies - working-directory: EssentialCSharp.Web - run: npm ci - - - name: Build frontend - working-directory: EssentialCSharp.Web - run: npm run build - - build-and-test: - name: Build and test (${{ matrix.runner }}) - runs-on: ${{ matrix.runner }} - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - include: - - runner: ubuntu-latest - collect_coverage: true - - runner: macos-latest - collect_coverage: false - - runner: windows-latest - collect_coverage: false - - steps: - - uses: actions/checkout@v7 - - - name: Set up .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Set up NuGet cache - uses: actions/cache@v6 - with: - path: | - ~/.nuget/packages - ${{ github.workspace }}/**/obj/project.assets.json - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - restore-keys: | - ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - ${{ runner.os }}-nuget- - - - name: Restore .NET dependencies - run: dotnet restore /p:AccessToNugetFeed=false - - - name: Build .NET - run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false /p:SkipFrontendBuild=true - - - name: Expose GitHub Actions Runtime - if: matrix.collect_coverage - uses: actions/github-script@v9 - with: - script: | - core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN']); - core.exportVariable('ACTIONS_RESULTS_URL', process.env['ACTIONS_RESULTS_URL']); - - - name: Run .NET tests with coverage - if: matrix.collect_coverage - run: dotnet test --no-build --configuration Release --report-trx --coverage --results-directory ${{ runner.temp }} - - - name: Run .NET tests - if: ${{ !matrix.collect_coverage }} - run: dotnet test --no-build --configuration Release - - - name: Convert TRX to VS Playlist - if: ${{ failure() && matrix.collect_coverage }} - uses: BenjaminMichaelis/trx-to-vsplaylist@v4 - with: - trx-file-path: '${{ runner.temp }}/*.trx' - output-directory: '${{ runner.temp }}/vsplaylists' - - ef-core-model-validation: - name: Validate EF Core model - needs: build-and-test - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - uses: actions/checkout@v7 - - - name: Set up .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Set up NuGet cache - uses: actions/cache@v6 - with: - path: | - ~/.nuget/packages - ${{ github.workspace }}/**/obj/project.assets.json - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - restore-keys: | - ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - ${{ runner.os }}-nuget- - - - name: Restore .NET dependencies - run: dotnet restore /p:AccessToNugetFeed=false - - - name: Restore local .NET tools - run: dotnet tool restore - - - name: Build .NET - run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false /p:SkipFrontendBuild=true - - - name: Check for pending EF Core model changes - run: dotnet tool run dotnet-ef -- migrations has-pending-model-changes --project EssentialCSharp.Web --configuration Release --no-build - env: - ASPNETCORE_ENVIRONMENT: Development - - container-validation: - name: Build container image - needs: [frontend-build, build-and-test] - runs-on: ubuntu-latest - timeout-minutes: 30 - - steps: - - uses: actions/checkout@v7 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - id: buildx - - - name: Restore BuildKit cache mounts - uses: actions/cache@v6 - id: buildkit-cache - with: - path: buildkit-cache - key: buildkit-cache-${{ hashFiles('EssentialCSharp.Web/Dockerfile', 'Directory.Packages.props', '**/*.csproj', 'EssentialCSharp.Web/package-lock.json') }} - restore-keys: buildkit-cache- - - - name: Inject BuildKit cache mounts - uses: reproducible-containers/buildkit-cache-dance@v3 - with: - builder: ${{ steps.buildx.outputs.name }} - dockerfile: EssentialCSharp.Web/Dockerfile - cache-dir: buildkit-cache - skip-extraction: ${{ steps.buildkit-cache.outputs.cache-hit }} - - - name: Build container image - uses: docker/build-push-action@v7 - with: - file: ./EssentialCSharp.Web/Dockerfile - context: . - push: false - cache-from: | - type=gha,scope=essentialcsharpweb-main - type=gha,scope=essentialcsharpweb-pr - cache-to: type=gha,mode=min,scope=essentialcsharpweb-pr - build-args: ACCESS_TO_NUGET_FEED=false From b9160cf15011d0652c8a31d96058804fa05f8819 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Thu, 17 Sep 2026 21:55:43 -0700 Subject: [PATCH 4/5] refactor(ci): fold EF validation into Ubuntu tests Run EF Core model-drift validation before the Ubuntu test leg so it reuses the existing restore and build output. --- .github/workflows/Build-Test-And-Deploy.yml | 50 +++++---------------- 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/.github/workflows/Build-Test-And-Deploy.yml b/.github/workflows/Build-Test-And-Deploy.yml index b2c30b0d..8b5d5ba9 100644 --- a/.github/workflows/Build-Test-And-Deploy.yml +++ b/.github/workflows/Build-Test-And-Deploy.yml @@ -190,6 +190,16 @@ jobs: - name: Build .NET run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false /p:SkipFrontendBuild=true + - name: Restore local .NET tools + if: matrix.collect_coverage + run: dotnet tool restore + + - name: Check for pending EF Core model changes + if: matrix.collect_coverage + run: dotnet tool run dotnet-ef -- migrations has-pending-model-changes --project EssentialCSharp.Web --configuration Release --no-build + env: + ASPNETCORE_ENVIRONMENT: Development + - name: Expose GitHub Actions Runtime if: matrix.collect_coverage uses: actions/github-script@v9 @@ -213,46 +223,6 @@ jobs: trx-file-path: '${{ runner.temp }}/*.trx' output-directory: '${{ runner.temp }}/vsplaylists' - ef-core-model-validation: - name: Validate EF Core model - if: github.event_name == 'pull_request' || github.event_name == 'merge_group' - needs: pr-build-and-test - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - uses: actions/checkout@v7 - - - name: Set up .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json - - - name: Set up NuGet cache - uses: actions/cache@v6 - with: - path: | - ~/.nuget/packages - ${{ github.workspace }}/**/obj/project.assets.json - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - restore-keys: | - ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - ${{ runner.os }}-nuget- - - - name: Restore .NET dependencies - run: dotnet restore /p:AccessToNugetFeed=false - - - name: Restore local .NET tools - run: dotnet tool restore - - - name: Build .NET - run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false /p:SkipFrontendBuild=true - - - name: Check for pending EF Core model changes - run: dotnet tool run dotnet-ef -- migrations has-pending-model-changes --project EssentialCSharp.Web --configuration Release --no-build - env: - ASPNETCORE_ENVIRONMENT: Development - container-validation: name: Build container image if: github.event_name == 'pull_request' || github.event_name == 'merge_group' From 4f127e10648edd5533174f4ad90ec4a53fa36067 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Thu, 17 Sep 2026 22:18:09 -0700 Subject: [PATCH 5/5] test(ci): build frontend on all platforms Run the explicit Node and Vite build on Ubuntu, macOS, and Windows to validate supported local frontend development environments. --- .github/workflows/Build-Test-And-Deploy.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Build-Test-And-Deploy.yml b/.github/workflows/Build-Test-And-Deploy.yml index 8b5d5ba9..56a12f94 100644 --- a/.github/workflows/Build-Test-And-Deploy.yml +++ b/.github/workflows/Build-Test-And-Deploy.yml @@ -17,9 +17,16 @@ concurrency: jobs: frontend-build: - name: Build frontend - runs-on: ubuntu-latest - timeout-minutes: 15 + name: Build frontend (${{ matrix.runner }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + runner: + - ubuntu-latest + - macos-latest + - windows-latest steps: - uses: actions/checkout@v7