Skip to content

build: Fallout 10.4 across the repo group, pinned exactly (#321) #465

build: Fallout 10.4 across the repo group, pinned exactly (#321)

build: Fallout 10.4 across the repo group, pinned exactly (#321) #465

Workflow file for this run

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}"