Skip to content

CI/CD Workflows

All workflows live in .gitea/workflows/ and run on the Gitea Actions runner (192.168.1.6).

Overview

flowchart LR
    subgraph Triggers
        PUSH_MAIN[Push to main]
        PUSH_DEV[Push to develop]
        PUSH_ANY[Push to any branch]
        PR[Pull Request]
        TAG[Tag v*]
    end

    subgraph Workflows
        SERVER[server.yml]
        DEPLOY[deploy.yml]
        STAGING[deploy-staging.yml]
        DOCS[docs.yml]
        PR_ENV[pr-environment.yml]
        RELEASE[release.yml]
        ANDROID[android.yml]
        IOS[ios.yml]
    end

    subgraph Targets
        PROD_SERVER[Production\n192.168.1.10]
        STAGING_SERVER[Staging\n192.168.1.7]
        DOCS_SERVER[Docs\n192.168.1.8]
        DOCKER[Docker on Runner\n192.168.1.6]
        GITEA_RELEASE[Gitea Release]
    end

    PUSH_ANY -->|server/**| SERVER
    PUSH_MAIN -->|server/**| DEPLOY
    PUSH_DEV -->|server/**| STAGING
    PUSH_MAIN -->|docs/**| DOCS
    PR -->|server/**| PR_ENV
    TAG --> RELEASE
    PUSH_ANY -->|android/**| ANDROID
    PUSH_ANY -->|ios/**| IOS

    DEPLOY --> PROD_SERVER
    STAGING --> STAGING_SERVER
    DOCS --> DOCS_SERVER
    PR_ENV --> DOCKER
    RELEASE --> GITEA_RELEASE

Workflows in Detail

server.yml — Server CI

Triggers: Push or PR touching server/** on any branch.

What it does: Validates that the Rust server code is correct.

Job Steps Purpose
lint cargo fmt --check, cargo clippy Code formatting and lint rules
test Start Postgres service, run migrations, cargo test Run all unit and integration tests
build cargo build --release Verify the release binary compiles

When it fails: Your code has formatting issues, clippy warnings, test failures, or doesn't compile. Fix locally before pushing again.


deploy.yml — Deploy to Production

Triggers: Push to main touching server/**.

What it does: Builds a static musl binary and deploys it to the production server.

Step What happens
Install Rust + musl target Sets up the cross-compilation toolchain
Build static release binary cargo build --release --target x86_64-unknown-linux-musl
Run migrations via SSH Rsyncs migration files to server, runs sqlx migrate run locally on the server
Deploy binary + assets Rsyncs binary, templates, and static files
Atomic swap + restart Renames old binary, moves new one in, restarts service

Target: 192.168.1.10 (server.dannyhaslund.dk)

Secrets used: PROD_DEPLOY_KEY


deploy-staging.yml — Deploy to Staging

Triggers: Push to develop touching server/**.

What it does: Identical to deploy.yml but targets the staging server.

Target: 192.168.1.7 (test-server.dannyhaslund.dk)

Secrets used: STAGING_DEPLOY_KEY


docs.yml — Documentation CI/CD

Triggers: Push or PR touching docs/** on main.

What it does: Builds the MkDocs site and deploys it with versioning.

Step What happens
Install dependencies Sets up Python venv, installs mkdocs + plugins
Build docs (PR only) mkdocs build --strict — validates, no deploy
Deploy versioned docs (main) mike deploy creates/updates version on gh-pages branch
Rsync to docs server Copies built site to 192.168.1.8

Target: 192.168.1.8 (docs.dannyhaslund.dk)

Secrets used: DOCS_DEPLOY_KEY

Versioning: Uses mike to maintain multiple doc versions. The dev alias always points to the latest docs from main. Tagged releases can add versioned docs (e.g., v1.0).


pr-environment.yml — Ephemeral PR Environments

Triggers: Pull request opened, updated, reopened, or closed — touching server/**.

What it does: Spins up a temporary Docker environment for each PR so reviewers can test changes live.

Event Action
PR opened/updated Starts a Docker Compose stack (app + Postgres) on a unique port
PR closed/merged Tears down the Docker stack and cleans up volumes

How it works:

  1. Each PR gets a unique port: 9000 + PR number (e.g., PR #5 → port 9005)
  2. A Docker Compose stack starts with:
  3. A fresh PostgreSQL database
  4. The server built from the PR branch
  5. A comment is posted on the PR with the URL: http://192.168.1.6:<port>
  6. When the PR is closed or merged, the stack is destroyed

Target: Docker containers on the runner (192.168.1.6)

Secrets used: CI_RELEASE_TOKEN (to post PR comments)

Files: infrastructure/docker/pr-environment.yml, infrastructure/docker/Dockerfile.server


release.yml — Create Release

Triggers: Push a tag matching v* (e.g., v0.1.0).

What it does: Generates a changelog and creates a Gitea release with the server binary attached.

Step What happens
Install Rust + git-cliff Sets up build tools and changelog generator
Generate release notes git cliff --latest parses conventional commits
Build static binary Cross-compiles for musl (same as deploy)
Create Gitea release Uses the Gitea API to create a release with notes + binary

How to trigger:

git checkout main
git tag -a v0.1.0 -m "v0.1.0"
git push origin v0.1.0

Secrets used: CI_RELEASE_TOKEN


android.yml — Android CI

Triggers: Push or PR touching android/**.

What it does: Validates Android code.

Job Steps Purpose
lint ./gradlew ktlintCheck Code style
test ./gradlew test Unit tests
build ./gradlew assembleDebug Build debug APK

Not active yet — requires Android project setup in android/.


ios.yml — iOS CI

Triggers: Push or PR touching ios/**.

What it does: Validates iOS code.

Job Steps Purpose
lint swiftlint lint --strict Code style
test xcodebuild test Unit tests
build xcodebuild build Build archive

Not active yet — requires a macOS runner registered with Gitea and an Xcode project in ios/.


Required Gitea Secrets

Secret Used By Purpose
PROD_DEPLOY_KEY deploy.yml SSH key to access 192.168.1.10
STAGING_DEPLOY_KEY deploy-staging.yml SSH key to access 192.168.1.7
DOCS_DEPLOY_KEY docs.yml SSH key to access 192.168.1.8
CI_RELEASE_TOKEN release.yml, pr-environment.yml Gitea API token for releases + PR comments

Troubleshooting

Workflow didn't trigger

  • Check the path filter — the workflow only runs if changes are in the matching directory (e.g., server/**).
  • "Re-run all jobs" replays the old workflow definition. If you changed the workflow file, you need a new push to use the updated version.

Build fails with "not found"

  • The binary was built with glibc (Ubuntu) but the server runs musl (Alpine). Ensure the workflow uses --target x86_64-unknown-linux-musl.

Migrations fail with "Connection refused"

  • Migrations run via SSH on the target server, not from the CI runner. Ensure PostgreSQL is running on the target and /etc/all_in_one.env has the correct DATABASE_URL.

"node: not found" errors

  • The runner's ubuntu-latest label must map to catthehacker/ubuntu:act-latest. Check the runner's .runner file and re-register if needed.