build-push.yaml: - Add environment-tag and version-tag optional inputs - Auto-detect environment from branch: main/master→:prod, staging→:staging, dev→:dev - Replace :latest with :prod for main branch - Support manual version tags for node-specific testing deploy-k8s.yaml: - Switch from image tag sed to deploy-timestamp annotation bump - Mutable tags (:prod/:staging) stay constant in manifests - ArgoCD detects rollout via timestamp annotation change - Preserves SHA in commit message for traceability Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
wectrl CI Pipeline Templates
Reusable Gitea Actions workflows for all wectrl services. These live in the wectrl-net/ci-templates repository and are called from each service repo.
Setup
1. Create the ci-templates repo on Gitea
Create a new repo at git.wectrl.net/wectrl-net/ci-templates and push the .gitea/workflows/ directory from this template.
2. Required secrets per service repo
Each service repo needs these secrets configured in Gitea (Settings > Actions > Secrets):
| Secret | Description |
|---|---|
REGISTRY_USER |
Gitea username for pushing images |
REGISTRY_TOKEN |
Gitea token with packages:write scope |
GIT_USER |
Gitea username for pushing to k8s cluster repo |
GIT_TOKEN |
Gitea token with repo:write scope on wectrl-k8s-cluster |
3. Add workflows to your service repo
Copy the appropriate example from examples/ into your repo's .gitea/workflows/ directory and customize the parameters.
Available Templates
Lint workflows
| Template | Language | Tool |
|---|---|---|
lint-python.yaml |
Python | Ruff |
lint-node.yaml |
Node/TS | ESLint + tsc |
lint-rust.yaml |
Rust | Clippy + rustfmt |
Test workflows
| Template | Language | Tool |
|---|---|---|
test-python.yaml |
Python | pytest |
test-node.yaml |
Node/TS | npm test (Vitest/Jest) |
test-rust.yaml |
Rust | cargo test |
Build & Deploy workflows
| Template | Purpose |
|---|---|
build-push.yaml |
Build Docker image, push to git.wectrl.net registry |
deploy-k8s.yaml |
Update image tag in wectrl-k8s-cluster repo (ArgoCD GitOps) |
Examples
Python + React fullstack (h1per-pms pattern)
# .gitea/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main, dev]
push:
branches: [main, dev]
jobs:
lint-backend:
uses: wectrl-net/ci-templates/.gitea/workflows/lint-python.yaml@main
lint-frontend:
uses: wectrl-net/ci-templates/.gitea/workflows/lint-node.yaml@main
with:
working-directory: web
test-backend:
uses: wectrl-net/ci-templates/.gitea/workflows/test-python.yaml@main
test-frontend:
uses: wectrl-net/ci-templates/.gitea/workflows/test-node.yaml@main
with:
working-directory: web
Rust service (wectrl-telemetry pattern)
# .gitea/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main, dev]
push:
branches: [main, dev]
jobs:
lint:
uses: wectrl-net/ci-templates/.gitea/workflows/lint-rust.yaml@main
test:
uses: wectrl-net/ci-templates/.gitea/workflows/test-rust.yaml@main
Pipeline flow
PR / push to dev push to main
│ │
▼ ▼
┌───────┐ ┌───────┐
│ Lint │ │ Lint │
│ Test │ │ Test │
└───────┘ └───┬───┘
│
▼
┌───────────┐
│ Build & │
│ Push Image│
└─────┬─────┘
│
▼
┌───────────┐
│ Update │
│ k8s repo │
└─────┬─────┘
│
▼
┌───────────┐
│ ArgoCD │
│ auto-sync │
└───────────┘
Service mapping
| Service | Repo | Stack | Deploy path in k8s-cluster |
|---|---|---|---|
| h1per-pms | wectrl-net/h1per-pms |
Python + React/TS | saas/h1per/backend/deployment.yaml |
| clok1-landing | wectrl-net/clok1-landing |
Node/TS | saas/clok1/app/deployment.yaml |
| solar-platform | wectrl-net/solar-platform |
TBD | platform/components/wectrl-solar-platform/api-deployment.yaml |
| solar-web | wectrl-net/solar-web |
TBD | platform/components/wectrl-solar-platform/web-deployment.yaml |
| client-portal API | wectrl-net/wectrl-client-portal |
TBD | platform/components/wectrl-client-portal/api-deployment.yaml |
| client-portal frontend | wectrl-net/wectrl-client-portal-frontend |
TBD | platform/components/wectrl-client-portal/frontend-deployment.yaml |
| wectrl-telemetry | wectrl-net/wectrl-telemetry |
Rust | TBD (needs k8s manifests) |
Customization
All templates accept inputs with sensible defaults. Override only what differs from the standard:
jobs:
lint:
uses: wectrl-net/ci-templates/.gitea/workflows/lint-python.yaml@main
with:
python-version: "3.12" # override default 3.13
working-directory: backend # if Python code is in a subdirectory
ARM64 builds and QEMU emulation
Our Hetzner cluster runs on ARM64 (CAX) nodes, so all Docker images target linux/arm64 by default. Since our Gitea runners are amd64, ARM64 images are built via QEMU user-mode emulation through Docker Buildx.
Trade-offs:
| QEMU emulation (default) | Native ARM64 runner | |
|---|---|---|
| Setup | Zero — works on any amd64 runner | Requires provisioning a self-hosted ARM64 runner |
| Build speed | 3-10x slower for compiled languages (Rust, Go, C) | Native speed |
| Reliability | Occasional QEMU edge cases with complex syscalls | No emulation issues |
| Best for | Node/Python/lightweight builds | Rust services, heavy native compilation |
For Rust services (e.g. wectrl-telemetry): QEMU emulation of ARM64 Rust compilation is significantly slower. When build times exceed ~10 minutes, switch to a native ARM64 runner:
jobs:
build:
uses: wectrl-net/ci-templates/.gitea/workflows/build-push.yaml@main
with:
image-name: git.wectrl.net/wectrl-net/wectrl-telemetry
runner: self-hosted-arm64 # bypass QEMU, use native ARM64 runner
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
The runner input on build-push.yaml defaults to ubuntu-latest (amd64 + QEMU). Set it to your ARM64 runner label when needed.
Notes
- All workflows trigger on both
mainanddevbranches (per CON-569 branching strategy) - Build & deploy only runs on push to
main(production deploy) - Dev/staging deploys can be added by extending
deploy-k8s.yamlwith a branch condition - The default platform is ARM64 (
linux/arm64) matching the Hetzner CAX cluster nodes — see ARM64 builds above - Semantic versioning tags (
v1.2.3) are supported bybuild-push.yamlvia the metadata action - Container images used by CI (e.g.
catthehacker/ubuntu) are pinned by digest to prevent supply-chain drift