CI/CD Pipeline & Release Strategy — Laundry Management System
| 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
- Branching Strategy — Git Flow
- Workflow Overview
- Backend Build & Test Pipeline
- Frontend Build Pipeline
- Docker Build & Push Pipeline
- Tauri Desktop Build Pipeline
- Release Asset Assembly
- Auto-Update Manifest Generation
- Version Tagging Strategy (SemVer)
- Staging Deployment
- Production Deployment Flow
- Development Workflow for Contributors
- 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 |