build: Fallout 10.4 across the repo group, pinned exactly (#321) #465
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: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| checks: write # publish-unit-test-result-action needs this | |
| pull-requests: write | |
| packages: read # consume SatisfactorySaveNet from GitHub Packages (nuget.config) | |
| # Surface GITHUB_TOKEN to dotnet restore so nuget.config's %GITHUB_TOKEN% | |
| # placeholder resolves to the workflow's installation token. GitHub Packages | |
| # NuGet always requires auth — even for public packages — so this is needed | |
| # by every job that runs `dotnet restore` / `dotnet build`. | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # Detect change scope (#102). Emits `code=true` when any build/test/CI input | |
| # changed, `code=false` for doc-only changes. Downstream jobs gate on it. | |
| # | |
| # Required checks (Lint, Build & Test) still RUN — they short-circuit each | |
| # step so branch protection's required-status gate is satisfied on doc-only | |
| # PRs. Non-required jobs use a job-level `if` and skip cleanly. workflow_dispatch | |
| # always runs the full lane. | |
| # --------------------------------------------------------------------------- | |
| changes: | |
| name: Detect change scope | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| outputs: | |
| code: ${{ steps.gate.outputs.code }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Detect code paths | |
| id: filter | |
| if: github.event_name != 'workflow_dispatch' | |
| uses: dorny/paths-filter@v3 | |
| with: | |
| filters: | | |
| code: | |
| - 'src/**' | |
| - 'test/**' | |
| - 'vendor/**' | |
| - '.github/workflows/**' | |
| - 'build/**' | |
| - '*.props' | |
| - '*.sln' | |
| - '*.slnx' | |
| - 'global.json' | |
| - 'version.json' | |
| - 'Directory.Build.props' | |
| - 'nuget.config' | |
| - 'build.sh' | |
| - 'build.cmd' | |
| - 'build.ps1' | |
| - name: Compute gate | |
| id: gate | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "code=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "code=${{ steps.filter.outputs.code }}" >> "$GITHUB_OUTPUT" | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # Lint — runs alongside Build & Test for parallel feedback. | |
| # --------------------------------------------------------------------------- | |
| lint: | |
| name: Lint (dotnet format) | |
| needs: changes | |
| # GH-hosted: trade marginal warm-cache speed for runner uptime. `setup-dotnet@v4` | |
| # provides .NET on demand; nothing else is needed beyond the runner image. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Skip (doc-only change) | |
| if: needs.changes.outputs.code != 'true' | |
| run: echo "Doc-only change — required check satisfied without running lint." | |
| - name: Checkout | |
| if: needs.changes.outputs.code == 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # NB.GV needs full history for version-height calc | |
| - name: Initialize vendored SatisfactorySaveNet submodule | |
| if: needs.changes.outputs.code == 'true' | |
| run: git -c submodule.vendor/SatisfactorySaveNet.update=checkout submodule update --init --recursive | |
| - name: Cache NuGet packages | |
| if: needs.changes.outputs.code == 'true' | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.nuget/packages | |
| key: ubuntu-latest-nuget-${{ hashFiles('**/*.csproj', '**/global.json') }} | |
| restore-keys: ubuntu-latest-nuget- | |
| - name: Setup .NET | |
| if: needs.changes.outputs.code == 'true' | |
| uses: actions/setup-dotnet@v4 | |
| env: | |
| # Pin install location to the runner user's home so setup-dotnet | |
| # finds the pre-installed SDKs at /home/runner/.dotnet (skip), | |
| # or installs there cleanly (writable) if a fresh runner. | |
| DOTNET_INSTALL_DIR: /home/runner/.dotnet | |
| with: | |
| dotnet-version: 8.0.x | |
| global-json-file: global.json | |
| - name: Format check | |
| if: needs.changes.outputs.code == 'true' | |
| run: ./build.sh Format | |
| # --------------------------------------------------------------------------- | |
| # Build & Test — Linux only. OS-specific bugs surface locally (Chris dev's | |
| # on Windows + Mac), so CI's job is just the basic "does it compile + tests | |
| # pass" gate. Linux is the cheapest GH Actions runner. | |
| # --------------------------------------------------------------------------- | |
| build-and-test: | |
| name: Build & Test | |
| needs: changes | |
| # GH-hosted: `dotnet test` only needs the SDK that `setup-dotnet@v4` installs. | |
| # The CI test slice (`TestNoUi`) excludes Playwright so no extra host packages | |
| # are needed. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Skip (doc-only change) | |
| if: needs.changes.outputs.code != 'true' | |
| run: echo "Doc-only change — required check satisfied without running build + tests." | |
| - name: Checkout | |
| if: needs.changes.outputs.code == 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # NB.GV needs full history for version-height calc | |
| - name: Initialize vendored SatisfactorySaveNet submodule | |
| if: needs.changes.outputs.code == 'true' | |
| run: git -c submodule.vendor/SatisfactorySaveNet.update=checkout submodule update --init --recursive | |
| - name: Cache NuGet packages | |
| if: needs.changes.outputs.code == 'true' | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.nuget/packages | |
| key: ubuntu-latest-nuget-${{ hashFiles('**/*.csproj', '**/global.json') }} | |
| restore-keys: ubuntu-latest-nuget- | |
| - name: Setup .NET (10 from global.json + 8 for vendor submodule) | |
| if: needs.changes.outputs.code == 'true' | |
| uses: actions/setup-dotnet@v4 | |
| env: | |
| DOTNET_INSTALL_DIR: /home/runner/.dotnet | |
| with: | |
| dotnet-version: 8.0.x | |
| global-json-file: global.json | |
| - name: Build & Test (no UI) | |
| if: needs.changes.outputs.code == 'true' | |
| # Web.UiTests is excluded in CI — Playwright's --with-deps chromium | |
| # install (sudo apt-get + ~150MB browser download) is too heavy for the | |
| # free GitHub Actions runner. Run `./build.sh Test` locally to include | |
| # the UI tests. Re-enabling on CI is a one-line change here. | |
| run: ./build.sh TestNoUi | |
| - name: Upload TRX test results | |
| if: always() && needs.changes.outputs.code == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results | |
| path: artifacts/test-results/*.trx | |
| if-no-files-found: warn | |
| # --------------------------------------------------------------------------- | |
| # Migration drift guard (ADR-0018 follow-up, issue #81). | |
| # Asserts both EF Core migration sets (SQLite + Postgres) are in sync with | |
| # the current PlanDbContext model — `has-pending-model-changes` exits non-zero | |
| # if a snapshot drifts. Runs in parallel with Build & Test for fast feedback. | |
| # --------------------------------------------------------------------------- | |
| migration-drift: | |
| name: Migration drift (SQLite + Postgres) | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| # GH-hosted: `dotnet ef migrations has-pending-model-changes` reads the | |
| # migrations snapshot offline. SQLite + Postgres providers ship in the SDK | |
| # packages, nothing else to install. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # NB.GV needs full history for version-height calc | |
| - name: Initialize vendored SatisfactorySaveNet submodule | |
| run: git -c submodule.vendor/SatisfactorySaveNet.update=checkout submodule update --init --recursive | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.nuget/packages | |
| key: ubuntu-latest-nuget-${{ hashFiles('**/*.csproj', '**/global.json') }} | |
| restore-keys: ubuntu-latest-nuget- | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| env: | |
| DOTNET_INSTALL_DIR: /home/runner/.dotnet | |
| with: | |
| dotnet-version: 8.0.x | |
| global-json-file: global.json | |
| - name: Check migrations (both providers) | |
| run: ./build.sh CheckMigrations | |
| # --------------------------------------------------------------------------- | |
| # Postgres runtime smoke (ADR-0018 follow-up, issue #81). | |
| # Spins up postgres:16 via the GitHub Actions services block and runs | |
| # `dotnet ef database update` against it — proves the Postgres migration | |
| # actually applies. Non-zero exit fails the job. | |
| # --------------------------------------------------------------------------- | |
| postgres-smoke: | |
| name: Postgres migration smoke | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| # `services: postgres:16` needs Docker on the host, which GH-hosted runners | |
| # already provide. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_PASSWORD: postgres | |
| POSTGRES_DB: plans | |
| ports: | |
| - 5432:5432 | |
| # Wait for Postgres to accept connections before the job's steps run. | |
| options: >- | |
| --health-cmd "pg_isready -U postgres" | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # NB.GV needs full history for version-height calc | |
| - name: Initialize vendored SatisfactorySaveNet submodule | |
| run: git -c submodule.vendor/SatisfactorySaveNet.update=checkout submodule update --init --recursive | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.nuget/packages | |
| key: ubuntu-latest-nuget-${{ hashFiles('**/*.csproj', '**/global.json') }} | |
| restore-keys: ubuntu-latest-nuget- | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 8.0.x | |
| global-json-file: global.json | |
| - name: Apply Postgres migrations | |
| env: | |
| ERP_PERSISTENCE_CONNECTION: Host=localhost;Port=5432;Database=plans;Username=postgres;Password=postgres | |
| run: ./build.sh MigrationsPostgresSmoke | |
| - name: List applied tables (log only) | |
| if: always() | |
| env: | |
| PGPASSWORD: postgres | |
| run: psql -h localhost -U postgres -d plans -c "\dt" || true | |
| # --------------------------------------------------------------------------- | |
| # Publish — surface TRX results to commit check + PR comment. | |
| # --------------------------------------------------------------------------- | |
| publish-test-results: | |
| name: Publish test results | |
| needs: [changes, build-and-test] | |
| runs-on: ubuntu-latest | |
| if: always() && needs.changes.outputs.code == 'true' | |
| permissions: | |
| contents: read | |
| checks: write | |
| pull-requests: write | |
| steps: | |
| - name: Download TRX artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: test-results | |
| pattern: test-results | |
| merge-multiple: false | |
| - name: Publish results to commit check | |
| uses: EnricoMi/publish-unit-test-result-action/linux@v2 | |
| with: | |
| files: test-results/**/*.trx | |
| check_name: Test results | |
| comment_mode: changes in failures | |
| # --------------------------------------------------------------------------- | |
| # Release — auto-creates a GitHub release on every successful push to main. | |
| # Version is computed by Nerdbank.GitVersioning from version.json + git | |
| # height. Skipped on PR runs (only runs after a PR merges into main). | |
| # --------------------------------------------------------------------------- | |
| release: | |
| name: Release | |
| needs: [changes, lint, build-and-test, migration-drift, postgres-smoke] | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changes.outputs.code == 'true' | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: write # create tags + releases | |
| packages: read # per-job permissions replace workflow-level, so restate | |
| actions: write # dispatch release-images.yml (gh workflow run) | |
| steps: | |
| - name: Checkout (full history for NB.GV) | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Initialize vendored SatisfactorySaveNet submodule | |
| run: git -c submodule.vendor/SatisfactorySaveNet.update=checkout submodule update --init --recursive | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 8.0.x | |
| global-json-file: global.json | |
| - name: Create GitHub release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: ./build.sh Release | |
| # --------------------------------------------------------------------- | |
| # Dispatch release-images.yml against the freshly-created tag. | |
| # | |
| # Why we need this: build.sh Release creates the GitHub release via the | |
| # API using GITHUB_TOKEN, and tags created with the default GITHUB_TOKEN | |
| # do NOT trigger downstream workflows (GitHub's anti-recursion guard). | |
| # So release-images.yml's `on: push: tags: v*` never fires — leaving | |
| # every auto-release with zero container images and zero agent binaries. | |
| # | |
| # Explicit dispatch sidesteps the guard. The dispatch counts as a | |
| # workflow_dispatch event, but `--ref <tag>` makes GITHUB_REF point at | |
| # the tag, so release-images.yml's per-trigger logic still treats it as | |
| # a tag run and attaches assets to that specific release. | |
| # --------------------------------------------------------------------- | |
| - name: Dispatch release-images for the new tag | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # The release we just created is by definition the most recent. | |
| tag="$(gh release list --limit 1 --json tagName --jq '.[0].tagName')" | |
| if [[ -z "$tag" ]]; then | |
| echo "::error::No release found after build.sh Release. Aborting." | |
| exit 1 | |
| fi | |
| echo "Dispatching release-images.yml against ${tag}" | |
| gh workflow run release-images.yml --ref "${tag}" |