feat: implement PoBrawl presidents ladder leaderboard with CRUD opera… #173
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy to Azure | |
| # CI/CD policy (2026-07-04): | |
| # - Unit tests (tests/PoMiniGames.Unit) DO run here — they have zero infra | |
| # dependencies (no Azurite, no Playwright) so they are a cheap regression gate | |
| # on the one branch that auto-deploys. Deploy needs both `build` and `test`. | |
| # - Integration + E2E (API/UI) tests remain out-of-band: they require Azurite | |
| # (Testcontainers) and Playwright Chromium and run locally via scripts/test-all.ps1. | |
| # - master is the only branch that triggers deploy. Local + remote feature | |
| # branches are merged into master via PRs. Branch-hygiene invariant: | |
| # scripts/branch-hygiene.ps1. | |
| on: | |
| push: | |
| branches: [ master ] | |
| pull_request: | |
| branches: [ master ] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| id-token: write | |
| concurrency: | |
| group: deploy-${{ github.repository }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| DOTNET_VERSION: '10.0.x' | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| # `az deployment sub what-if` requires --location (the deployment record | |
| # region; *not* the resource region, which lives in main.parameters.json). | |
| # Must also match infra/main.bicep targetScope='subscription'. | |
| AZURE_LOCATION: 'westus2' | |
| jobs: | |
| build: | |
| name: Build (master only — no tests) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| # Required for MinVer to read the most recent v* tag (instead of | |
| # stamping every build with 0.0.0-alpha.0.0). fetch-depth=0 is the | |
| # cheapest setting that still gives MinVer a tag list. | |
| fetch-depth: 0 | |
| - name: Check MinVer v* tag at HEAD | |
| shell: bash | |
| run: | | |
| # MinVer stamps the assembly version from the nearest reachable v* tag. | |
| # A tag exactly at HEAD produces a clean SemVer (e.g. v1.2.3); a commit | |
| # ahead of the nearest tag produces a pre-release suffix (e.g. 1.2.4-alpha.1.0). | |
| # Both are valid — pre-release builds are still identifiable in App Insights. | |
| # We emit a warning (not a failure) when HEAD is untagged so hotfixes and | |
| # config-only pushes are never blocked by a missing release tag. | |
| git fetch --tags --force origin '+refs/tags/*:refs/tags/*' | |
| TAGS=$(git tag --points-at HEAD | grep -E '^v[0-9]' || true) | |
| if [ -z "$TAGS" ]; then | |
| echo "::warning::No v* tag at HEAD ($(git rev-parse --short HEAD)). MinVer will stamp this build with a pre-release version." | |
| echo "::warning::To create a release build: git tag -a vX.Y.Z <sha> && git push origin vX.Y.Z" | |
| else | |
| echo "MinVer will resolve to: $TAGS" | |
| fi | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.nuget/packages | |
| key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/global.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-nuget- | |
| - name: Restore | |
| run: dotnet restore PoMiniGames.slnx | |
| # Compile every project in the solution — src + tests — so a broken test | |
| # file cannot slip into master. TreatWarningsAsErrors=true (Directory.Build.props) | |
| # ensures zero warnings reach the artifact stage. | |
| # | |
| # PR builds stay in Debug to keep CI fast (no publish, no artifact upload); | |
| # only push-to-master runs the Release publish + upload that feeds the | |
| # deploy job. This halves PR build minutes without sacrificing the | |
| # "broken test file fails master" guarantee. | |
| - name: Build | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| dotnet build PoMiniGames.slnx --no-restore --configuration Debug | |
| else | |
| dotnet build PoMiniGames.slnx --no-restore --configuration Release | |
| fi | |
| # §7 #NET_CLEAN_10: trim-audit for the WASM client. The host's `dotnet publish` | |
| # exercises server-side AOT but never compiles the Blazor client with trimming | |
| # on, so a regression in PoShared / PoMiniGamesClient trim safety would only | |
| # surface at the consumer's first deploy. This step runs the same | |
| # PublishTrimmed + EnableTrimAnalyzer pipeline locally — if the analyzer | |
| # surfaces IL2xxx warnings under Debug, TreatWarningsAsErrors fails the build. | |
| # Release adds trimming at publish time; both must pass. | |
| - name: Trim audit (WASM client) | |
| run: | | |
| dotnet publish src/PoMiniGames.Client/PoMiniGamesClient.csproj \ | |
| -c ${{ github.event_name == 'pull_request' && 'Debug' || 'Release' }} \ | |
| -p:PublishTrimmed=true \ | |
| -p:TrimMode=partial \ | |
| -o ./client-trim-audit \ | |
| --no-restore | |
| # §6.6 mobile-first budget: the trimmed WASM _framework payload gates first | |
| # paint on phones. Fail the build if it regresses past the budget so a new | |
| # reflection-heavy DTO can't silently bloat the download. Baseline 2026-07-04 | |
| # ≈ 20 MB uncompressed — inherent to shipping the PoSurvive client-side | |
| # simulation engine (by design; see AGENT.MD). Budget set with headroom to | |
| # catch regression, not to squeeze the baseline. | |
| - name: WASM bundle-size budget | |
| shell: bash | |
| env: | |
| FRAMEWORK_BUDGET_MB: 25 | |
| run: | | |
| SIZE_MB=$(du -sm ./client-trim-audit/wwwroot/_framework | cut -f1) | |
| echo "Trimmed _framework size: ${SIZE_MB} MB (budget ${FRAMEWORK_BUDGET_MB} MB)" | |
| if [ "$SIZE_MB" -gt "$FRAMEWORK_BUDGET_MB" ]; then | |
| echo "::error::WASM _framework ${SIZE_MB} MB exceeds budget ${FRAMEWORK_BUDGET_MB} MB." | |
| exit 1 | |
| fi | |
| - name: Verify formatting (non-blocking) | |
| run: dotnet format PoMiniGames.slnx --verify-no-changes --verbosity minimal | |
| continue-on-error: true | |
| - name: Publish (push-to-master only) | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/master' | |
| run: dotnet publish src/PoMiniGames/PoMiniGames/PoMiniGames.csproj -c Release -o ./publish | |
| - name: Upload publish artifact (push-to-master only) | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/master' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: api-publish | |
| path: ./publish | |
| test: | |
| name: Unit tests (no infra) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.nuget/packages | |
| key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/global.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-nuget- | |
| # Unit tier only — zero external dependencies. Integration/E2E stay local | |
| # (Azurite + Playwright). See the CI/CD policy header. | |
| - name: Run unit tests | |
| run: dotnet test tests/PoMiniGames.Unit/PoMiniGames.Unit.csproj --configuration Debug | |
| deploy: | |
| name: Deploy to Azure App Service | |
| needs: [build, test] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/master' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| environment: | |
| name: production | |
| permissions: | |
| id-token: write | |
| contents: read | |
| env: | |
| # Override at job scope in case the workflow env is overridden by a | |
| # caller (workflow_dispatch with custom inputs). | |
| AZURE_LOCATION: 'westus2' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| # Required for Bicep what-if: the deploy job otherwise only has the | |
| # api-publish artifact and `infra/main.bicep` doesn't exist on the | |
| # runner (run 28288927457 failed with ENOENT on it). | |
| fetch-depth: 0 | |
| - name: Download publish artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: api-publish | |
| path: ./publish | |
| - name: Azure Login | |
| uses: azure/login@v2 | |
| with: | |
| client-id: ${{ secrets.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ secrets.AZURE_TENANT_ID }} | |
| subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| - name: Bicep what-if (preview infra changes) | |
| shell: bash | |
| # Preview infra changes before any artifact reaches App Service. If | |
| # Bicep would delete, recreate, or downgrade a shared resource (KV, | |
| # App Insights, AI Foundry) we surface it in the run log here. | |
| # | |
| # continue-on-error: this is a SOFT gate. The deploy SP needs | |
| # Microsoft.Authorization/roleAssignments/write on the target | |
| # resources (and the subscription) to even *simulate* the | |
| # assignments, and that permission is granted out-of-band on the | |
| # Azure portal — bootstrapping it from CI is the chicken-and-egg | |
| # problem that blocks the deploy entirely if this is a hard gate. | |
| # A failed what-if is logged as a warning; a clean what-if shows | |
| # the change set. Either way the App Service deploy proceeds. | |
| run: | | |
| az deployment sub what-if \ | |
| --location "${{ env.AZURE_LOCATION }}" \ | |
| --template-file infra/main.bicep \ | |
| --parameters infra/main.parameters.json \ | |
| --name "pominigames-preview-${{ github.run_id }}" \ | |
| --result-format FullResourcePayloads || \ | |
| echo "::warning::Bicep what-if failed (likely missing Microsoft.Authorization/roleAssignments/write on the deploy SP). The App Service deploy proceeds anyway." | |
| continue-on-error: true | |
| - name: Deploy to App Service | |
| shell: bash | |
| run: | | |
| # Self-healing target resolution: discover the API web app from its | |
| # resource group instead of hard-coding a name. A future resource | |
| # rename can no longer silently 404 the deploy (the original failure mode). | |
| APP_NAME=$(az webapp list --resource-group PoMiniGames --query "[0].name" -o tsv) | |
| if [ -z "$APP_NAME" ]; then echo "::error::No web app found in resource group PoMiniGames"; exit 1; fi | |
| echo "Resolved App Service: $APP_NAME" | |
| echo "APP_NAME=$APP_NAME" >> "$GITHUB_ENV" | |
| # Zip the publish *contents* (not the parent folder) so the host finds | |
| # PoMiniGames.dll at the deployment root, not nested under publish/. | |
| # Combined with WEBSITE_RUN_FROM_PACKAGE=1 (set in resources.bicep), the | |
| # zip is mounted atomically — no file-copy step on the host. | |
| ( cd publish && zip -qr ../app-deploy.zip . ) | |
| az webapp deploy \ | |
| --resource-group PoMiniGames \ | |
| --name "$APP_NAME" \ | |
| --src-path app-deploy.zip \ | |
| --type zip \ | |
| --track-status true | |
| - name: Prewarm (absorb F1 cold-start) | |
| shell: bash | |
| # F1 cannot enable AlwaysOn; the first request after a fresh deploy pays | |
| # the full cold-start tax (~10–30s on a Windows F1). Hammering the cold | |
| # code paths here keeps the retry-loop failures out of the next four | |
| # smoke steps. We probe ping (cheapest), liveness (alias), structured | |
| # /api/health, and /api/diag in order — every health surface must be | |
| # warmed before the smoke tests run. | |
| run: | | |
| for path in "/api/health/ping" "/api/health/liveness" "/api/health" "/api/diag"; do | |
| curl -fsS --retry 15 --retry-all-errors --retry-delay 15 \ | |
| "https://${APP_NAME}.azurewebsites.net${path}" || true | |
| done | |
| - name: Smoke Test - Health Ping | |
| shell: bash | |
| run: | | |
| curl -fsS --retry 6 --retry-all-errors --retry-delay 10 \ | |
| "https://${APP_NAME}.azurewebsites.net/api/health/ping" || exit 1 | |
| - name: Smoke Test - Health Liveness | |
| shell: bash | |
| run: | | |
| curl -fsS --retry 6 --retry-all-errors --retry-delay 10 \ | |
| "https://${APP_NAME}.azurewebsites.net/api/health/liveness" || exit 1 | |
| - name: Smoke Test - Full Health | |
| shell: bash | |
| run: | | |
| curl -fsS --retry 6 --retry-all-errors --retry-delay 10 \ | |
| "https://${APP_NAME}.azurewebsites.net/api/health" || exit 1 | |
| - name: Smoke Test - Diag (masked KV + subsystem integrations) | |
| shell: bash | |
| run: | | |
| curl -fsS --retry 6 --retry-all-errors --retry-delay 10 \ | |
| "https://${APP_NAME}.azurewebsites.net/api/diag" || exit 1 |