name: .NET Build & Test with PostgreSQL (Reusable) on: workflow_call: inputs: solution: description: "Solution/project file to build (relative to working-directory). Empty = let dotnet pick the single sln/proj in working-directory" required: false default: "" type: string dotnet-version: description: ".NET SDK version" required: false default: "10.0.x" type: string configuration: description: "Build configuration" required: false default: "Release" type: string # string, not boolean: act_runner mangles boolean defaults. The value may still arrive as '%!t(string=…)', see Build step. warnaserror: description: "Treat build warnings as errors (\"true\"/\"false\")" required: false default: "true" type: string working-directory: description: "Path where dotnet commands are executed (relative to repo root)" required: false default: "." type: string test-filter: description: "Optional dotnet test --filter expression" required: false default: "" type: string postgres-image: description: "PostgreSQL service image" required: false default: "postgres:17-alpine" type: string postgres-user: description: "PostgreSQL superuser (POSTGRES_USER of the service)" required: false default: "postgres" type: string postgres-password: description: "PostgreSQL password (throwaway CI database, not a secret)" required: false default: "postgres" type: string postgres-db: description: "Database created on service start (POSTGRES_DB). Empty = same as postgres-user" required: false default: "" type: string jobs: build-and-test: runs-on: ubuntu-latest env: DOTNET_CLI_TELEMETRY_OPTOUT: "1" DOTNET_NOLOGO: "1" SOLUTION: ${{ inputs.solution }} CONFIGURATION: ${{ inputs.configuration }} WARNASERROR: ${{ inputs.warnaserror }} TEST_FILTER: ${{ inputs.test-filter }} POSTGRES_HOST: postgres POSTGRES_PORT: "5432" POSTGRES_USER: ${{ inputs.postgres-user }} POSTGRES_PASSWORD: ${{ inputs.postgres-password }} POSTGRES_DB: ${{ inputs.postgres-db || inputs.postgres-user }} # Reached by service name on the job network (container-mode runner). # No host port mapping, so concurrent jobs or a host PostgreSQL can't collide on 5432. services: postgres: image: ${{ inputs.postgres-image }} env: POSTGRES_USER: ${{ inputs.postgres-user }} POSTGRES_PASSWORD: ${{ inputs.postgres-password }} POSTGRES_DB: ${{ inputs.postgres-db || inputs.postgres-user }} defaults: run: shell: bash working-directory: ${{ inputs.working-directory }} steps: - name: Checkout uses: actions/checkout@v6 - name: Setup .NET uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ inputs.dotnet-version }} - name: Restore run: dotnet restore ${SOLUTION:+"$SOLUTION"} - name: Build run: | ARGS=(-c "$CONFIGURATION" --no-restore) # act_runner may deliver the value mangled as '%!t(string=false)', so match by substring. case "${WARNASERROR,,}" in *false*) ;; *) ARGS+=(-warnaserror) ;; esac set -x dotnet build ${SOLUTION:+"$SOLUTION"} "${ARGS[@]}" # The image's init phase listens on the unix socket only, so an open TCP port means the final server is up. - name: Wait for PostgreSQL run: | for _ in $(seq 1 60); do if (exec 3<>"/dev/tcp/$POSTGRES_HOST/$POSTGRES_PORT") 2>/dev/null; then echo "PostgreSQL reachable at $POSTGRES_HOST:$POSTGRES_PORT" exit 0 fi sleep 1 done echo "::error::PostgreSQL not reachable at $POSTGRES_HOST:$POSTGRES_PORT after 60s" exit 1 - name: Test run: | ARGS=(-c "$CONFIGURATION" --no-build) if [ -n "$TEST_FILTER" ]; then ARGS+=(--filter "$TEST_FILTER") fi dotnet test ${SOLUTION:+"$SOLUTION"} "${ARGS[@]}"