--- id: ci-cd title: CI/CD sidebar_label: CI/CD --- Two Gitea Actions workflows: one releases agents, one builds images. ## Agent releases Triggered by an `agent/v*` tag. ```bash git tag agent/v1.0.0 && git push origin agent/v1.0.0 ``` Builds `linux/amd64`, `linux/arm64` and `windows/amd64`, writes `checksums.txt` and creates a Gitea release. A second job on Windows packages the WiX MSI. The install and update scripts read the newest `agent/v*` release from the Gitea API, so tagging is what makes a new agent available to every install. ## Image builds Triggered on every push to `main`. Builds and pushes seven images: `server`, `web`, `site`, `sitesvc`, `admin`, `adminsite` and `docsite`. :::warning Despite the name, this workflow does not deploy There is no SSH step. Rolling images out is a manual step on the host: ```bash cd /opt/vantage && \ docker compose -f docker-compose.yml -f docker-compose.site.yml pull && \ docker compose -f docker-compose.yml -f docker-compose.site.yml up -d --remove-orphans ``` ::: ### Each image rebuilds only when its own inputs change A `git diff` against the previous head decides. That is why the checkout uses `fetch-depth: 0` — a shallow clone has one commit and nothing to diff against. | Image | Rebuilds when | | --- | --- | | `server` | `server/`, `shared/`, `proto/`, `go.work` | | `admin` | `admin/`, `shared/`, `go.work` | | `sitesvc` | `sitesvc/`, `shared/`, `go.work` | | `web` · `site` · `adminsite` · `docsite` | their own directory only | `shared/` fans out to all three Go images because each of their Dockerfiles copies it from a root context. **If a fourth service ever imports `shared/`, it must be added to that list or it will ship stale.** Everything rebuilds when there is no trustworthy base commit to diff against: a manual `workflow_dispatch`, a new branch, or a force-push whose old head is gone. Changing the workflow file itself also rebuilds everything, since a build argument is baked into each image. ### The gap: repository variables :::danger Editing a repository variable pushes no commit, so nothing rebuilds Values like `API_URL`, `ADMIN_API_URL`, `HQ_URL`, `ADMIN_ENV`, `PADDLE_ENV`, `PADDLE_CLIENT_TOKEN`, `DOCS_URL` and `DOCS_BASE_URL` are baked into images at build time. After editing one, run the workflow manually — that is what `workflow_dispatch` is for. The symptom is a frontend that keeps using the old value with no error anywhere, which is a long afternoon if you do not know about this. ::: The same applies to base images: a service nobody touches stops being rebuilt on newer base layers. A periodic manual run covers it. ## Secrets and variables | Name | Type | Purpose | | --- | --- | --- | | `RELEASE_TOKEN` | Secret | Gitea API token, `write:release` | | `REGISTRY_USER` / `REGISTRY_PASSWORD` | Secret | Registry push credentials | | `PADDLE_API_KEY` | Secret | Read by admin at runtime | | `PADDLE_WEBHOOK_SECRET` | Secret | Webhook signature verification | | `GITEA_HOST` / `DOCKER_HOST` | Variable | Hosts used in tags and URLs | | `API_URL` | Variable | Baked into `web` | | `HQ_URL` | Variable | Baked into `web`; empty on self-hosted | | `SITE_API_URL` / `SITE_CONTACT_EMAIL` | Variable | Baked into `site` | | `ADMIN_API_URL` | Variable | Baked into `adminsite` **and** `site` | | `ADMIN_ENV` | Variable | Environment badge in the portal | | `PADDLE_ENV` | Variable | Baked into `adminsite`, read by `admin`. Must match on both sides | | `PADDLE_CLIENT_TOKEN` | Variable | Browser Paddle token for checkout | | `DOCS_URL` / `DOCS_BASE_URL` | Variable | Baked into `docsite` | Anything marked "browser-reachable" must be an origin a browser can actually resolve — not an internal service name. Get it wrong and every request fails at runtime with a not-connected panel rather than at build time. ## The documentation site `docsite/` builds to static files and is served by nginx under `/docs` on the marketing host, routed by its own proxy location. `DOCS_BASE_URL` has to agree with three things at once: that proxy location, the directory the runtime image serves from, and the value baked into the build. When they disagree the page loads and every stylesheet and script 404s.