Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.git/
.github/
.vs/
**/bin/
**/obj/
**/TestResults/
coverage*/
tests/
docs/
scripts/
*.md
*.suo
*.user
*.log
src/SimPle.Api/.env
src/SimPle.Api/appsettings.Development.json
src/SimPle.Api/appsettings.Local.json
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: nuget
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 5
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 5
130 changes: 119 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,138 @@ on:
pull_request:
branches: ["main"]

# Keep the normal validation token read-only. Artifact provenance gets its write
# permissions only in the dedicated, push-only job below.
permissions:
contents: read

jobs:
backend:
name: Build, test, vulnerability scan
name: Build, test, package, and container smoke
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 12

env:
MIGRATION_TEST_CONNECTION_STRING: Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=postgres

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up .NET 8
uses: actions/setup-dotnet@v4
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: "8.0.x"

- name: Restore
run: dotnet restore
# NuGet lock files are not in the repository yet. Do not use
# --locked-mode until they are intentionally committed, or every CI
# run would fail before it can test the application.
run: dotnet restore SimPle.sln

- name: Build
run: dotnet build --no-restore --configuration Release
run: dotnet build SimPle.sln --no-restore --configuration Release

- name: Unit tests
run: dotnet test tests/SimPle.UnitTests/SimPle.UnitTests.csproj --no-build --configuration Release
run: dotnet test tests/SimPle.UnitTests/SimPle.UnitTests.csproj --no-build --configuration Release --logger "trx;LogFileName=unit.trx" --results-directory TestResults

- name: Integration and real-PostgreSQL migration tests
run: dotnet test tests/SimPle.IntegrationTests/SimPle.IntegrationTests.csproj --no-build --configuration Release --logger "trx;LogFileName=integration.trx" --results-directory TestResults

- name: Fail on high or critical NuGet vulnerabilities
shell: bash
run: |
set -o pipefail
dotnet list SimPle.sln package --vulnerable --include-transitive | tee TestResults/nuget-vulnerabilities.txt
if grep -Eq ' (High|Critical) ' TestResults/nuget-vulnerabilities.txt; then
echo "High or critical NuGet vulnerability found. Update it or add an owned, time-bounded waiver."
exit 1
fi

- name: Generate CycloneDX SBOM
run: |
dotnet tool install --tool-path .tools cyclonedx --version 6.2.0
./.tools/dotnet-CycloneDX SimPle.sln --output artifacts/sbom --json

- name: Build container image
run: docker build --tag simple-backend:${{ github.sha }} .

- name: Smoke the non-root, read-only container
shell: bash
run: |
set -euo pipefail
# Liveness deliberately proves only that the process serves HTTP. The
# integration job above owns real PostgreSQL migration verification;
# readiness is expected to stay unhealthy until a migration job runs.
docker run --detach --name simple-backend-smoke --network host \
--read-only --tmpfs /tmp:rw,nosuid,nodev,noexec \
-e 'ConnectionStrings__DefaultConnection=Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=postgres' \
-e 'Jwt__SecretKey=ci-smoke-jwt-secret-that-is-at-least-thirty-two-characters' \
-e 'LobbyCredential__Key=ci-smoke-lobby-key-that-is-at-least-thirty-two-characters' \
-e 'Recaptcha__SecretKey=ci-smoke-recaptcha-secret' \
-e 'Google__ClientId=ci-smoke.apps.googleusercontent.com' \
-e 'Email__SmtpHost=localhost' \
-e 'Email__Username=ci-smoke@example.test' \
-e 'Email__Password=ci-smoke-password' \
-e 'Email__From=ci-smoke@example.test' \
-e 'Google__ClientId=ci-smoke-google-client-id' \
-e 'Storage__Provider=S3Compatible' \
-e 'Storage__BucketName=ci-smoke-assets' \
-e 'Storage__Region=us-east-1' \
-e 'Storage__AccessKey=ci-smoke-access-key' \
-e 'Storage__SecretKey=ci-smoke-secret-key' \
-e 'Storage__ProfilePrefix=profile-assets' \
simple-backend:${{ github.sha }}
for attempt in {1..30}; do
if curl --fail --silent --show-error http://127.0.0.1:8080/health/live | grep -qx '{"status":"healthy"}'; then
docker exec simple-backend-smoke id | grep -q 'uid=1654(app)'
exit 0
fi
sleep 1
done
docker logs simple-backend-smoke
exit 1

- name: Integration tests
run: dotnet test tests/SimPle.IntegrationTests/SimPle.IntegrationTests.csproj --no-build --configuration Release
- name: Remove smoke container
if: always()
run: docker rm --force simple-backend-smoke || true

- name: NuGet vulnerability scan
run: dotnet list package --vulnerable
- name: Upload test and SBOM evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: backend-evidence-${{ github.sha }}
path: |
TestResults/
artifacts/sbom/
if-no-files-found: error
retention-days: 30

attest-sbom:
name: Attest backend SBOM
needs: backend
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
attestations: write
id-token: write
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: backend-evidence-${{ github.sha }}
path: evidence

- uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: evidence/artifacts/sbom/*.json
29 changes: 29 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CodeQL

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
schedule:
- cron: "23 4 * * 1"

permissions:
contents: read
security-events: write

jobs:
analyze:
name: Analyze C#
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
languages: csharp
build-mode: manual
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: "8.0.x"
- run: dotnet build SimPle.sln --configuration Release
- uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
53 changes: 53 additions & 0 deletions .github/workflows/publish-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Publish immutable backend image

on:
workflow_dispatch:

# Run this only for an already-green commit on main. The resulting digest and
# provenance attestation are release evidence; a mutable tag is never enough.
permissions:
contents: read
packages: write
attestations: write
id-token: write

jobs:
publish:
name: Publish and attest backend image
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
env:
IMAGE_NAME: ghcr.io/simpleplatform/simple-backend
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0

- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push immutable candidate
id: push
uses: docker/build-push-action@ee4ca427a2f43b6a16632044ca514c076267da23 # v6.19.0
with:
context: .
push: true
tags: ${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}

- name: Attest image build provenance
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-name: ${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

- name: Record digest in job summary
run: |
echo '### Immutable backend image' >> "$GITHUB_STEP_SUMMARY"
echo "\`${IMAGE_NAME}@${{ steps.push.outputs.digest }}\`" >> "$GITHUB_STEP_SUMMARY"
30 changes: 30 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# syntax=docker/dockerfile:1

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

COPY ["src/SimPle.Api/SimPle.Api.csproj", "src/SimPle.Api/"]
COPY ["src/SimPle.Application/SimPle.Application.csproj", "src/SimPle.Application/"]
COPY ["src/SimPle.Domain/SimPle.Domain.csproj", "src/SimPle.Domain/"]
COPY ["src/SimPle.Infrastructure/SimPle.Infrastructure.csproj", "src/SimPle.Infrastructure/"]
COPY ["src/SimPle.Shared/SimPle.Shared.csproj", "src/SimPle.Shared/"]
RUN dotnet restore "src/SimPle.Api/SimPle.Api.csproj"

COPY src/ ./src/
RUN dotnet publish "src/SimPle.Api/SimPle.Api.csproj" --configuration Release --no-restore --output /app/publish /p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app

# Container Apps probes this port. Secrets are injected by the platform as environment variables, never copied here.
ENV ASPNETCORE_URLS=http://+:8080 \
ASPNETCORE_ENVIRONMENT=Production \
DOTNET_EnableDiagnostics=0
EXPOSE 8080

COPY --from=build /app/publish ./

# The official ASP.NET image supplies this non-root user (UID 1654). The application does not require a writable
# filesystem, allowing the deployment manifest to set a read-only root filesystem as an additional hardening layer.
USER $APP_UID
ENTRYPOINT ["dotnet", "SimPle.Api.dll"]
Loading
Loading