Skip to content

CI/CD Pipeline & Release Strategy — Laundry Management System

Document Information

Field Value
Project Laundry Management System
Version 1.0
Language English
Source Control GitHub
CI/CD Platform GitHub Actions
Branching Strategy Git Flow
Document Type CI/CD & Release Guide

Table of Contents

  1. Branching Strategy — Git Flow
  2. Workflow Overview
  3. Backend Build & Test Pipeline
  4. Frontend Build Pipeline
  5. Docker Build & Push Pipeline
  6. Tauri Desktop Build Pipeline
  7. Release Asset Assembly
  8. Auto-Update Manifest Generation
  9. Version Tagging Strategy (SemVer)
  10. Staging Deployment
  11. Production Deployment Flow
  12. Development Workflow for Contributors
  13. Secrets & Environment Variables

1. Branching Strategy — Git Flow

main              ← Production releases only
  ├── develop     ← Integration branch
  │     │
  │     ├── feature/login-screen
  │     ├── feature/carpet-receipt
  │     ├── feature/offline-sync
  │     └── ...
  ├── hotfix/1.0.1-critical-fix    ← Emergency fixes for production
  └── release/1.1.0                ← Release preparation
Branch Purpose CI Trigger
main Production-ready code On push/merge → Build Docker images + Tauri installers + Create GitHub Release
develop Integration branch On PR → Build + Test. On merge → Deploy to Staging
feature/* New feature development On PR → Build + Test only
hotfix/* Emergency production fixes On PR → Full pipeline
release/* Release candidate preparation On PR → Full pipeline

2. Workflow Overview

Developer pushes feature branch
┌──────────────────────────┐
│  GitHub Actions:         │
│  build-and-test.yml      │
│                          │
│  • dotnet build          │
│  • dotnet test (xUnit)   │
│  • npm ci + ng lint      │
│  • ng test (Karma/Jest)  │
└──────────────────────────┘
   PR opened to develop
┌──────────────────────────┐
│  Checks must pass:       │
│  ✅ All tests green      │
│  ✅ Code coverage ≥ 80%  │
│  ✅ ESLint clean         │
│  ✅ Code review approved │
└──────────────────────────┘
   Merge to develop
┌──────────────────────────┐
│  deploy-staging.yml      │
│  • Build Docker image    │
│  • Push to ghcr.io       │
│  • Deploy to staging VPS │
└──────────────────────────┘
   Merge to main (or release/*)
┌──────────────────────────────────────┐
│  release.yml                          │
│                                      │
│  • Build Docker images                │
│  • Push to ghcr.io (tagged + latest) │
│  • Build Tauri (Windows/Mac/Linux)    │
│  • Assemble Release Assets            │
│  • Generate auto-update manifest      │
│  • Create GitHub Release              │
└──────────────────────────────────────┘

3. Backend Build & Test Pipeline

# .github/workflows/build-and-test.yml
name: Build & Test

on:
  pull_request:
    branches: [develop, main]
  push:
    branches: [develop]

jobs:
  backend:
    name: Backend (.NET)
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: laundry_test
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 10.0.x

      - name: Restore
        run: dotnet restore backend/Laundry.sln

      - name: Build
        run: dotnet build backend/Laundry.sln --no-restore --configuration Release

      - name: Run Unit Tests
        run: dotnet test backend/Laundry.sln --no-build --configuration Release --collect:"XPlat Code Coverage" --logger "trx;LogFileName=test-results.trx"

      - name: Report Coverage
        uses: dorny/test-reporter@v1
        with:
          name: Backend Tests
          path: "**/test-results.trx"
          reporter: dotnet-trx

      - name: Verify Coverage Threshold
        run: |
          # Using reportgenerator to check coverage >= 80%
          dotnet tool install -g dotnet-reportgenerator-globaltool
          reportgenerator -reports:**/coverage.cobertura.xml -targetdir:coverage
          # Check threshold (implement as needed)

4. Frontend Build Pipeline

  frontend:
    name: Frontend (Angular)
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
          cache-dependency-path: frontend/laundry-app/package-lock.json

      - name: Install dependencies
        run: npm ci
        working-directory: frontend/laundry-app

      - name: Lint
        run: npm run lint
        working-directory: frontend/laundry-app

      - name: Run Unit Tests
        run: npm run test -- --watch=false --browsers=ChromeHeadless --code-coverage
        working-directory: frontend/laundry-app

      - name: Build Angular
        run: npm run build -- --configuration=production
        working-directory: frontend/laundry-app

5. Docker Build & Push Pipeline

# .github/workflows/release.yml (triggered on merge to main)
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  docker:
    name: Build & Push Docker Images
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Set version
        id: version
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build Angular (for embedding in API)
        uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci && npm run build -- --configuration=production
        working-directory: frontend/laundry-app

      - name: Build API Image
        uses: docker/build-push-action@v6
        with:
          context: .
          file: backend/Laundry.Api/Dockerfile
          push: true
          tags: |
            ghcr.io/${{ github.repository_owner }}/laundry-api:${{ steps.version.outputs.VERSION }}
            ghcr.io/${{ github.repository_owner }}/laundry-api:latest
          labels: |
            org.opencontainers.image.version=${{ steps.version.outputs.VERSION }}
            org.opencontainers.image.revision=${{ github.sha }}

      - name: Export Docker images for offline deployment
        run: |
          mkdir -p release-assets
          docker pull ghcr.io/${{ github.repository_owner }}/laundry-api:${{ steps.version.outputs.VERSION }}
          docker pull postgres:16-alpine
          docker pull traefik:v3
          docker pull datalust/seq:2024.1
          docker save \
            ghcr.io/${{ github.repository_owner }}/laundry-api:${{ steps.version.outputs.VERSION }} \
            postgres:16-alpine \
            traefik:v3 \
            datalust/seq:2024.1 \
            -o release-assets/laundry-images_v${{ steps.version.outputs.VERSION }}.tar

      - name: Upload Docker image archive
        uses: actions/upload-artifact@v4
        with:
          name: docker-images
          path: release-assets/laundry-images_v${{ steps.version.outputs.VERSION }}.tar

6. Tauri Desktop Build Pipeline

  tauri:
    name: Build Tauri Desktop App
    needs: [docker]
    runs-on: ${{ matrix.platform }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - platform: windows-latest
            target: x86_64-pc-windows-msvc
            artifact: msi
          - platform: macos-latest
            target: aarch64-apple-darwin
            artifact: dmg
          - platform: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            artifact: deb

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Install dependencies
        run: npm ci
        working-directory: frontend/laundry-app

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Install Linux dependencies
        if: runner.os == 'Linux'
        run: |
          sudo apt-get update
          sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf

      - name: Build Tauri
        uses: tauri-apps/tauri-action@v0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          projectPath: frontend/laundry-app
          args: --target ${{ matrix.target }}

      - name: Upload Tauri installer
        uses: actions/upload-artifact@v4
        with:
          name: tauri-${{ matrix.platform }}
          path: frontend/laundry-app/src-tauri/target/${{ matrix.target }}/release/bundle/

7. Release Asset Assembly

  release-assets:
    name: Assemble GitHub Release
    needs: [docker, tauri]
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - uses: actions/checkout@v4

      - name: Download all artifacts
        uses: actions/download-artifact@v4

      - name: Assemble release package
        run: |
          mkdir -p release

          # Docker compose + .env template
          cp deploy/docker-compose.yml release/
          cp deploy/.env.template release/

          # Docker images
          cp docker-images/laundry-images_v*.tar release/

          # Tauri installers
          cp tauri-windows-latest/msi/*.msi release/
          cp tauri-macos-latest/dmg/*.dmg release/
          cp tauri-ubuntu-latest/deb/*.deb release/

          # README
          cp deploy/RELEASE_NOTES.md release/

          # Package
          cd release
          tar -czf ../laundry-release_v${{ github.ref_name }}.tar.gz .

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          name: Release ${{ github.ref_name }}
          body_path: deploy/RELEASE_NOTES.md
          files: |
            release-assets/laundry-release_*.tar.gz
            release-assets/docker-images/laundry-images_*.tar
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

8. Auto-Update Manifest Generation

For Type 1 (online) deployments, Tauri's auto-updater checks a latest.json file hosted on GitHub Releases.

  update-manifest:
    name: Generate Auto-Update Manifest
    needs: [tauri]
    runs-on: ubuntu-latest

    steps:
      - name: Generate latest.json
        run: |
          VERSION="${{ github.ref_name }}"
          cat > latest.json << EOF
          {
            "version": "${VERSION#v}",
            "notes": "See release notes at https://github.com/${{ github.repository }}/releases/tag/${VERSION}",
            "pub_date": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
            "platforms": {
              "windows-x86_64": {
                "signature": "",
                "url": "https://github.com/${{ github.repository }}/releases/download/${VERSION}/LaundrySystem_${VERSION#v}_x64_en-US.msi"
              },
              "darwin-aarch64": {
                "signature": "",
                "url": "https://github.com/${{ github.repository }}/releases/download/${VERSION}/LaundrySystem_${VERSION#v}_aarch64.dmg"
              },
              "linux-x86_64": {
                "signature": "",
                "url": "https://github.com/${{ github.repository }}/releases/download/${VERSION}/laundry-system_${VERSION#v}_amd64.deb"
              }
            }
          }
          EOF

      - name: Upload manifest
        uses: softprops/action-gh-release@v2
        with:
          name: Release ${{ github.ref_name }}
          files: latest.json
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

9. Version Tagging Strategy (SemVer)

Format v<MAJOR>.<MINOR>.<PATCH>
v1.0.0 Initial stable release
v1.0.1 Bug fix (backward-compatible)
v1.1.0 New feature (backward-compatible)
v2.0.0 Breaking changes (schema change, API removal)
# Creating a release:
git checkout main
git pull origin main
git tag -a v1.0.1 -m "Release v1.0.1: Fix invoice numbering bug"
git push origin v1.0.1
# → Triggers the release.yml workflow

10. Staging Deployment

# .github/workflows/deploy-staging.yml
name: Deploy to Staging

on:
  push:
    branches: [develop]

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to staging VPS via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.STAGING_HOST }}
          username: deploy
          key: ${{ secrets.STAGING_SSH_KEY }}
          script: |
            cd /opt/laundry-staging
            docker compose pull
            docker compose up -d
            docker compose ps

11. Production Deployment Flow

Production is NEVER auto-deployed from CI/CD. The CI/CD pipeline produces artifacts (Docker images, Tauri installers). The vendor manually deploys to customer servers.

Customer Type Deployment Method
Type 1 (Online Server) Vendor SSHs into the customer's server. Runs docker compose pull && docker compose up -d. Or provides instructions to the customer's IT person.
Type 2/3 (Offline Branch) Vendor downloads the Release Asset .tar.gz. Copies to USB. Visits the branch. Runs docker load < images.tar && docker compose up -d.

No customer server credentials are stored in GitHub.


12. Development Workflow for Contributors

Daily Development

# Clone the repo
git clone git@github.com:your-org/laundry.git
cd laundry

# Create feature branch
git checkout -b feature/carpet-receipt develop

# Start backend (with Docker PostgreSQL)
cd backend/Laundry.Api
docker compose -f docker-compose.dev.yml up -d postgres seq
dotnet run

# Start frontend + Tauri dev mode
cd frontend/laundry-app
npm install
npx tauri dev     # Opens Tauri window with hot-reload

Pull Request Checklist

  • [ ] All backend tests pass (dotnet test)
  • [ ] Backend code coverage ≥ 80%
  • [ ] All frontend tests pass (npm test)
  • [ ] ESLint clean (npm run lint)
  • [ ] Angular production build succeeds (npm run build)
  • [ ] Tauri dev build works (npx tauri dev)
  • [ ] EF Core migration added if schema changed
  • [ ] Database migration tested against fresh PostgreSQL container
  • [ ] PR description includes what was changed and why

13. Secrets & Environment Variables

GitHub Repository Secrets

Secret Purpose
GITHUB_TOKEN Auto-provided. For pushing to ghcr.io, creating releases.
STAGING_HOST Staging server IP/hostname
STAGING_SSH_KEY SSH private key for staging deployment
TAURI_PRIVATE_KEY For code signing Tauri installers (Windows code signing cert)
APPLE_CERTIFICATE For macOS code signing
APPLE_CERTIFICATE_PASSWORD macOS signing cert password

Per-Customer .env (NEVER in GitHub)

Variable How Generated
DB_PASSWORD 32-char random — generated by installation wizard
BRANCH_ID UUID — generated by installation wizard
CLIENT_TOKEN_SECRET 64-char random — generated by installation wizard
license.dat Encrypted file — provided by vendor per customer

Appendix A: Dockerfile (API)

# backend/Laundry.Api/Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY backend/ .
RUN dotnet restore Laundry.Api/Laundry.Api.csproj
RUN dotnet publish Laundry.Api/Laundry.Api.csproj -c Release -o /app/publish --no-restore

# Copy Angular build output
COPY frontend/laundry-app/dist/laundry-app/browser/ /app/publish/wwwroot/

FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app/publish .

ENV ASPNETCORE_URLS=http://+:5000
EXPOSE 5000

ENTRYPOINT ["dotnet", "Laundry.Api.dll"]

Appendix B: Full Release Checklist

Before creating a new GitHub Release:

  • [ ] All tests pass on main
  • [ ] EF Core migrations are backward-compatible
  • [ ] Release notes written (deploy/RELEASE_NOTES.md)
  • [ ] Version bumped in appsettings.json and tauri.conf.json
  • [ ] Docker image builds locally without errors
  • [ ] Tauri builds on all 3 platforms locally
  • [ ] Staging deployment verified
  • [ ] Rollback procedure documented in release notes

Revision History

Date Version Author Changes
2026-05-10 1.0 System Architect Initial CI/CD & release pipeline specification