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:
- Each PR gets a unique port:
9000 + PR number(e.g., PR #5 → port 9005) - A Docker Compose stack starts with:
- A fresh PostgreSQL database
- The server built from the PR branch
- A comment is posted on the PR with the URL:
http://192.168.1.6:<port> - 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:
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.envhas the correctDATABASE_URL.
"node: not found" errors¶
- The runner's
ubuntu-latestlabel must map tocatthehacker/ubuntu:act-latest. Check the runner's.runnerfile and re-register if needed.