diff --git a/.github/workflows/Build-Test-And-Deploy.yml b/.github/workflows/Build-Test-And-Deploy.yml index 7009fa10..56a12f94 100644 --- a/.github/workflows/Build-Test-And-Deploy.yml +++ b/.github/workflows/Build-Test-And-Deploy.yml @@ -3,15 +3,65 @@ name: Build, Test, and Deploy EssentialCSharp.Web on: push: branches: ["main"] + pull_request: + branches: ["main"] + merge_group: workflow_dispatch: permissions: - id-token: write 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 (${{ 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 + + - 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" steps: @@ -25,13 +75,6 @@ jobs: env: NUGET_AUTH_TOKEN: ${{ secrets.AZURE_DEVOPS_PAT }} - - name: Set up Node.js - uses: actions/setup-node@v7 - with: - node-version: 24 - cache: npm - cache-dependency-path: EssentialCSharp.Web/package-lock.json - - name: Set up dependency caching for faster builds uses: actions/cache@v6 id: nuget-cache @@ -48,7 +91,7 @@ jobs: run: dotnet restore - 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 @@ -112,9 +155,127 @@ 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: 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 + 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' + + 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 concurrency: group: deploy-development @@ -172,8 +333,9 @@ 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] concurrency: group: deploy-production diff --git a/.github/workflows/PR-Build-And-Test.yml b/.github/workflows/PR-Build-And-Test.yml deleted file mode 100644 index cbe6fab4..00000000 --- a/.github/workflows/PR-Build-And-Test.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: PR Build and Test EssentialCSharp.Web - -on: - pull_request: - branches: ["main"] - merge_group: - workflow_dispatch: - -jobs: - frontend-build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - 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: Install npm dependencies - working-directory: EssentialCSharp.Web - run: npm ci - - - name: Build frontend - working-directory: EssentialCSharp.Web - run: npm run build - - build-and-test: - runs-on: ubuntu-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 dependency caching for faster builds - uses: actions/cache@v6 - id: nuget-cache - 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: 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: Expose GitHub Actions Runtime - 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 - run: dotnet test --no-build --configuration Release --report-trx --coverage --results-directory ${{ runner.temp }} - - - name: Convert TRX to VS Playlist - if: failure() - uses: BenjaminMichaelis/trx-to-vsplaylist@v4 - with: - trx-file-path: '${{ runner.temp }}/*.trx' - output-directory: '${{ runner.temp }}/vsplaylists' - - - 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 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/EssentialCSharp.Web/.nvmrc b/EssentialCSharp.Web/.nvmrc new file mode 100644 index 00000000..6f4247a6 --- /dev/null +++ b/EssentialCSharp.Web/.nvmrc @@ -0,0 +1 @@ +26