CI/CD
Audience
PLANA staff shipping code, debugging a failed pipeline, configuring a new repository.
PLANA runs CI/CD on self-hosted Forgejo Actions. Every repository has a .forgejo/workflows/pipeline.yml describing build, test, and deploy. Builds run on in-cluster runners; deploys go through Flux GitOps (for most services) or a direct kubectl set image step (for the few services that haven't migrated yet).
The runners
| Runner | Where | What |
|---|---|---|
forgejo-runner-main | forgejo-runner namespace | Main org-scoped runner |
forgejo-runner-vantage | Same namespace | Vantage-org-scoped runner (separate Forgejo org) |
Both run as DaemonSets / Deployments inside the cluster. They poll Forgejo for queued jobs, execute, and report back.
Shared workflows in ci-templates
Common workflow templates live in git.planapulse.com/plana-pulse/ci-templates:
| Template | Purpose |
|---|---|
build-and-deploy-static.yml | Static site build + push to nginx-served image |
build-and-deploy-service.yml | Generic Node/Python service build + deploy |
oca-sync.yml | Weekly OCA submodule refresh |
rebuild-templates.yml | Tenant template DB rebuild on Odoo branch push |
Repos consume these by referencing the template path in their pipeline.yml.
A typical pipeline
For pulse-account-api (a Node.js service):
on:
push:
branches: [main]
jobs:
build:
runs-on: docker
container:
image: git.planapulse.com/plana-pulse/kaniko:debug
steps:
- name: Build and push image
run: |
/kaniko/executor \
--context=. \
--dockerfile=./Dockerfile \
--destination=git.planapulse.com/plana-pulse/pulse-account-api:${{ github.sha }} \
--destination=git.planapulse.com/plana-pulse/pulse-account-api:latest
deploy:
needs: build
runs-on: docker
container:
image: alpine/k8s:1.29.14
steps:
- name: Rollout
run: |
kubectl -n pulse-account set image deploy/pulse-account-api \
pulse-account-api=git.planapulse.com/plana-pulse/pulse-account-api:${{ github.sha }}
kubectl -n pulse-account rollout status deploy/pulse-account-apiThe docs-portal pipeline is the same pattern; see infra/k8s/docs/ for its specific deploy step.
GitOps deploys (preferred)
For services managed by Flux:
- CI builds the image
- CI commits the new image tag to
infra/k8s/<service>/deployment.yaml - Flux reconciles within ~60 seconds
- New pods roll out
This is the preferred path because:
- The state of the cluster is fully described in git
- Rollback = revert a commit
- Drift detection works (
kubectl set imagewould race with Flux)
A few services (docs-portal, pulse-website) still use the direct kubectl set image pattern — migrating them is a noted follow-up.
Image registry
All images live in Forgejo's built-in registry at git.planapulse.com. No Docker Hub, no GHCR, no ECR.
Pull secrets:
| Namespace | Secret |
|---|---|
| Per-service namespace | forgejo-registry (copied at namespace setup) |
The pull secret is in SOPS under forgejo.registry_pull_secret. When adding a new namespace, copy the secret in as part of the namespace bootstrap.
Build caching
Kaniko caches layers in Forgejo's registry under git.planapulse.com/cache/<repo>. Subsequent builds with unchanged Dockerfile early steps reuse the cached layer.
For Node/Python builds, npm ci / pip install benefit most. First build of a service is ~5 minutes; subsequent builds with cache hits are ~30 seconds.
CI secrets
Per-repo secrets in Forgejo's UI:
| Secret | Purpose |
|---|---|
REGISTRY_TOKEN | Push to Forgejo registry |
KUBECONFIG_B64 | kubectl access (services using direct set-image) |
OCA_SYNC_SSH_KEY | OCA-sync workflow push |
SOPS_AGE_KEY | (Carefully scoped) for jobs that need to decrypt SOPS |
Most CI does not need to decrypt SOPS. The age key is provided only to the few jobs that must (e.g. running an integration test against a real DB).
OCA weekly sync
.forgejo/workflows/oca-sync.yml runs Saturdays at 02:00 UTC:
- Iterates
SUPPORTED_VERSIONS = "17.0 18.0 19.0" - For each, runs
scripts/update-oca.shto refresh OCA submodules - Commits + pushes if changed (each push triggers
pipeline.ymlwhich rebuilds the image)
Configured via the per-repo SSH deploy key oca-sync-bot-write.
Where to read more
- Flux GitOps — the deploy half of CI/CD
- Annual Odoo port — uses the OCA-sync workflow
- Forgejo — the registry + CI runner
- Source:
git.planapulse.com/plana-pulse/ci-templates