Skip to main content

CI/CD Architecture

Hephaestus uses trunk-based continuous deployment powered by GitHub Actions. Every merge to main triggers automatic staging deployment, with production requiring manual approval.

๐Ÿ—๏ธ Architecture Overviewโ€‹

โšก Pipeline Timelineโ€‹

StageDurationNotes
Quality gates~3 minParallel with tests
Tests~3 minParallel with gates
Docker builds~3 min (cached)Registry cache, ~7 min cold
Release cut~1 minOn Version PR merge: tag + GitHub Release
Staging deploy~2 minAutomatic
Your verificationYou decideCheck staging
Production deploy~2 minAfter approval
Total~12 min + verification~8 min with full cache hits

๐Ÿš€ Release Flowโ€‹

Every merge to main runs CI and updates the accumulating Version PR (changesets). Merging that PR cuts the release โ€” tag vX.Y.Z, GitHub Release, docker tags X.Y.Z/X.Y/latest, then staging (automatic) and production (after approval). Full flow: Release Management.

๐Ÿ›ก๏ธ Quality Gatesโ€‹

Before any release, code must pass:

Gate (leg)ToolPurpose
Migration chain + drift (Database)LiquibaseFull chain applies empty โ†’ head, then schema is diffed against JPA entities
Changelog immutability (Migrations)git diffReleased changesets + master.xml are append-only
OpenAPI syncDiff checkClient โ†” Server sync
Java formattingSpotlessCode style
TypeScriptBiome + tscLint + typecheck

๐Ÿ”’ Securityโ€‹

  • CodeQL โ€“ SAST scanning via GitHub's Default Setup (automatic, zero maintenance)
  • Trivy โ€“ Scans dependencies for CVEs
  • TruffleHog โ€“ Secret detection in code and history
  • Renovate โ€“ Monitors dependencies for vulnerabilities
  • Environment protection โ€“ Production requires approval

CodeQL Default Setupโ€‹

CodeQL runs automatically via GitHub's Default Setup (enabled in repository settings), providing:

  • Scans on every push to main and protected branches
  • Scans on pull request creation and updates
  • Weekly scheduled scans for the full codebase
  • Incremental analysis (20% faster on PRs)
  • Zero maintenance โ€“ GitHub manages query updates

This is more efficient than a custom workflow and doesn't consume CI minutes.

๐Ÿ“ฆ Environmentsโ€‹

EnvironmentProtectionDeploys On
Preview (Coolify)NoneEvery PR
StagingNoneEvery tag
ProductionApproval requiredTag + approval

GitHub Environment Setupโ€‹

  1. Settings โ†’ Environments โ†’ New environment
  2. Create staging (no rules)
  3. Create production with Required reviewers

๐Ÿ”„ Preview Deploymentsโ€‹

Coolify handles PR previews:

  • Built directly on server (fast!)
  • URL: pr-{number}.preview.hephaestus.cit.tum.de
  • Auto-cleanup on PR close

โš™๏ธ Key Workflowsโ€‹

WorkflowTriggerPurpose
cicd.ymlPush to main, PRsOrchestrator: change detection + workflow dispatch
ci-quality-gates.ymlCalled by cicd.ymlCode quality, formatting, schema validation
ci-tests.ymlCalled by cicd.ymlUnit, integration, visual tests
ci-docker-build.ymlCalled by cicd.ymlDocker image builds per component
ci-security-scan.ymlCalled by cicd.ymlDependency scanning (Trivy), secret detection
verify-changesets.ymlPRsFails shipped-code PRs that carry no changeset
version-pr.ymlPush to mainMaintains the accumulating Version PR (changesets)
release.ymlOn CI/CD SuccessCuts a release when the Version PR merged: tag + GitHub Release, deploys staging, gates production
deploy-staging.ymlCalled by release.ymlDeploys to staging
deploy-prod.ymlworkflow_dispatchDeploys to production (manual trigger)

Workflow Architectureโ€‹

The cicd.yml workflow:

  1. Detects changes using dorny/paths-filter
  2. Dispatches sub-workflows with component-specific flags
  3. Aggregates results in the CI Status Gate job

๐ŸŽฏ Performance Optimizationsโ€‹

Path-Based Filteringโ€‹

CI only runs jobs for components that actually changed:

ComponentTriggers On
Webappwebapp/**, package.json, pnpm-lock.yaml, pnpm-workspace.yaml, .npmrc, .node-version
Application Serverserver/**, scripts/db-utils.sh (includes webhook receiver โ€” ADR 0008)
CI Config.github/workflows/**, .github/actions/** โ†’ runs all jobs

Benefits:

  • Webapp-only changes skip Java tests (~3 min saved)
  • Docs-only changes skip all CI jobs (~7 min saved)
  • CI config changes run everything (safety net)

Docker Layer Cachingโ€‹

Docker builds use registry-based caching to store intermediate layers in ghcr.io:

ComponentFirst BuildCached BuildCache Size
Application Server~7 min~30 sec~500 MB
Webapp~5 min~30 sec~200 MB

How it works:

  • cache-from: Pulls cached layers from registry (main branch + current branch)
  • cache-to: Pushes new layers with mode=max (all intermediate layers)
  • Separate cache tags per platform: image:cache-linux-amd64, image:cache-linux-arm64
  • Native builds: amd64 on x86 runners, arm64 on ARM runners (no QEMU emulation)

Benefits over GitHub Actions Cache:

  • No 10GB size limit (registry is unlimited)
  • No eviction after 7 days
  • Works across branches (PRs benefit from main's cache)
  • Persistent (survives cache clearing)

Without Docker Hub authentication, base image metadata resolution can be rate-limited, adding 2+ minutes to every Docker build. To avoid this:

  1. Create a Docker Hub account (free tier is sufficient)
  2. Generate an access token at hub.docker.com/settings/security
  3. Add repository variable and secret:
    • Variable: DOCKERHUB_USERNAME = your Docker Hub username
    • Secret: DOCKERHUB_TOKEN = your access token

The Docker build workflow will automatically use these credentials if present, falling back to anonymous (slower) access if not configured.

Parallel Executionโ€‹

  • 6 test types run in parallel (3 app-server, 3 webapp). Webhook reception is part of the app-server test surface since ADR 0008.
  • 5 quality-gate legs run in parallel (App Server, Webapp, OpenAPI, Database, Migrations), plus a legacy-cleanup guard
  • 4 Docker builds ร— 2 architectures (amd64 + arm64)
  • fail-fast: false ensures all jobs complete for full feedback

Concurrency Controlโ€‹

  • Outdated PR runs cancelled automatically
  • Release runs never cancelled

๐Ÿ› ๏ธ Running CI Locallyโ€‹

Before pushing, run the same checks that CI runs:

# Format and check all services
pnpm run format && pnpm run check

Per-Service Commandsโ€‹

# Webapp
pnpm run check:webapp # Full check (format + lint + typecheck)
pnpm run test:webapp # Unit tests

# Application Server (Java) โ€” includes the integration.core.webhook receiver
pnpm run format:java:check # Check formatting
cd server && ./mvnw test -Dgroups="unit" # Unit tests

Common Issuesโ€‹

IssueSolution
Formatting errorsRun pnpm run format
TypeScript errorsRun pnpm run typecheck:webapp to see details
Test failuresCheck the specific test output for details
OpenAPI out of syncRun pnpm run generate:api
Database schema driftRun pnpm run db:draft-changelog

๐Ÿ“Š CI Featuresโ€‹

Test Resultsโ€‹

All test suites generate JUnit XML reports that are displayed in the Test Results tab of each workflow run:

  • Application Server: Unit, integration, and architecture tests (incl. the in-process Pi mentor agent and the webhook receiver per ADR 0008)
  • Webapp: Unit tests and Storybook interaction tests

Job Summaryโ€‹

Each CI run generates a rich Job Summary in the Actions UI with:

  • Overall status with emoji indicators
  • Results table for each workflow (quality gates, tests, security, Docker)
  • Components changed table (from path filtering)
  • Failure-specific troubleshooting guides with fix commands
  • Performance metrics showing skipped workflows

Workflow Timelineโ€‹

The CI Status Gate job generates a visual Mermaid timeline showing:

  • Job execution order and duration
  • Parallel job execution
  • Runner wait times
  • Critical path identification

This helps identify bottlenecks and optimization opportunities.

๐Ÿ†• Adding a New Serviceโ€‹

When adding a new service to the monorepo, update CI configuration in this order:

Step 1: Path Detection (cicd.yml)โ€‹

Add a path filter and output for the new service:

# In detect-changes job outputs:
outputs:
new-service: ${{ steps.filter.outputs.new-service }}

# In paths-filter step:
filters: |
new-service:
- 'server/new-service/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'

Update the any-code aggregate output to include the new service.

Step 2: Quality Gates (ci-quality-gates.yml)โ€‹

  1. Add to matrix:
matrix:
check: [
# ... existing checks
new-service-quality,
]
  1. Add case statement in "Determine if check should run":
"new-service-quality")
echo "run=${{ inputs.new_service_changed }}" >> $GITHUB_OUTPUT
;;
  1. Add quality check step with the appropriate linting/type checking commands.

Step 3: Tests (ci-tests.yml)โ€‹

  1. Add to matrix:
matrix:
test-type: [
# ... existing tests
new-service-unit,
new-service-integration, # if applicable
]
  1. Add case statement in "Determine if test should run":
"new-service-unit"|"new-service-integration")
echo "run=${{ inputs.new_service_changed }}" >> $GITHUB_OUTPUT
;;
  1. Add test execution step with the test commands.

  2. Add test result upload for JUnit reporting.

Step 4: Docker Build (ci-docker-build.yml)โ€‹

Add a new build job:

new-service-build:
name: "Docker: new-service"
if: inputs.should_skip != 'true' && inputs.new_service_changed == 'true'
uses: ls1intum/.github/.github/workflows/build-and-push-docker-image.yml@main
with:
image-name: "ls1intum/hephaestus/new-service"
docker-file: "./server/new-service/Dockerfile"
docker-context: "./server/new-service"
# ... rest of config

Step 5: Caching (setup-caches/action.yml)โ€‹

Add the new service's cache-types to the appropriate conditions:

# For Node.js services:
- name: Cache Node.js dependencies
if: contains(fromJSON('["...", "new-service-quality", "new-service-unit"]'), inputs.cache-type)

# For Java services:
- name: Cache Maven dependencies
if: contains(fromJSON('["...", "new-service-unit"]'), inputs.cache-type)

Step 6: Update Workflow Inputsโ€‹

In cicd.yml, add the new input to workflow calls:

with:
new_service_changed: ${{ (needs.detect-changes.outputs.new-service == 'true' || ...) && 'true' || 'false' }}

Verification Checklistโ€‹

After adding a new service, verify:

  • Path filter correctly detects changes to new service
  • Quality gates run only when new service changes
  • Tests run only when new service changes
  • Docker build runs only when new service changes
  • CI config changes trigger all jobs (safety net)
  • JUnit reports appear in Test Results tab