Skip to content

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

RunnerWhereWhat
forgejo-runner-mainforgejo-runner namespaceMain org-scoped runner
forgejo-runner-vantageSame namespaceVantage-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:

TemplatePurpose
build-and-deploy-static.ymlStatic site build + push to nginx-served image
build-and-deploy-service.ymlGeneric Node/Python service build + deploy
oca-sync.ymlWeekly OCA submodule refresh
rebuild-templates.ymlTenant 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):

yaml
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-api

The docs-portal pipeline is the same pattern; see infra/k8s/docs/ for its specific deploy step.

GitOps deploys (preferred)

For services managed by Flux:

  1. CI builds the image
  2. CI commits the new image tag to infra/k8s/<service>/deployment.yaml
  3. Flux reconciles within ~60 seconds
  4. 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 image would 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:

NamespaceSecret
Per-service namespaceforgejo-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:

SecretPurpose
REGISTRY_TOKENPush to Forgejo registry
KUBECONFIG_B64kubectl access (services using direct set-image)
OCA_SYNC_SSH_KEYOCA-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.sh to refresh OCA submodules
  • Commits + pushes if changed (each push triggers pipeline.yml which 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

© PLANA Digital Ltd.