Files
tessera-ctl/.planning/phases/06-desktop-client-ci-cd/06-03-PLAN.md
T
2026-06-25 09:25:24 +02:00

15 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
06-desktop-client-ci-cd 03 execute 1
.gitea/workflows/ci.yml
docker-compose.ci.yml
docs/ci-cd-setup.md
false
INFRA-04
service why dashboard_config
gitea Git remote + CI/CD host. Project currently has no git remote; Gitea instance access (URL, Actions enabled) is unknown per RESEARCH Open Question 3
task location
Create a Tessera repository in Gitea and add it as git remote Gitea web UI → New Repository, then `git remote add origin <url>`
task location
Enable Actions for the repository and register an act_runner Gitea → Repo Settings → Actions, and runner registration token under Admin/Repo → Actions → Runners
truths artifacts key_links
A git remote pointing at the Tessera Gitea repository exists and main is pushed
An act_runner is registered and able to execute Gitea Actions jobs in Docker
A .gitea/workflows/ci.yml pipeline runs on push to main: lint+type-check → tests → docker build+deploy
The pipeline reuses the existing apps/web and apps/api Dockerfiles and deploys via docker compose on the same server
path provides contains
.gitea/workflows/ci.yml Multi-stage CI/CD pipeline (quality → test → build-deploy) runs-on
path provides contains
docker-compose.ci.yml act_runner service definition (Docker socket mount, ephemeral) act_runner
path provides
docs/ci-cd-setup.md Setup runbook: Gitea remote, act_runner registration, secrets
from to via pattern
.gitea/workflows/ci.yml existing docker-compose.yml services web + api docker compose build + up -d on the deploy host docker compose
from to via pattern
.gitea/workflows/ci.yml root package.json turbo tasks pnpm lint / type-check / test pnpm (lint|test|type-check)
Establish the Gitea-based DevOps tooling (INFRA-04): connect the repo to a Gitea remote, register an act_runner, and create a multi-stage Gitea Actions pipeline that, on every push to main, runs Lint + TypeCheck → Vitest tests → Docker image builds → auto-deploy via docker compose on the same server (D-12, D-13). This is developer tooling only — no Git/Gitea features are exposed inside the Tessera application (D-10), and Claude pushes manually at milestones (D-11).

Purpose: Minimal-manual-effort version control + CI/CD so milestone pushes automatically lint, test, build, and redeploy the running stack.

Output: .gitea/workflows/ci.yml, an act_runner service definition, and a setup runbook.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/06-desktop-client-ci-cd/06-CONTEXT.md @.planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md @docker-compose.yml @package.json @turbo.json

<artifacts_this_phase_produces>

  • .gitea/workflows/ci.yml — CI/CD pipeline (quality / test / build-deploy jobs)
  • docker-compose.ci.yml — act_runner service (Docker socket mount, ephemeral mode)
  • docs/ci-cd-setup.md — runbook for Gitea remote + runner registration + secrets </artifacts_this_phase_produces>
Task 1: Set up Gitea remote and register act_runner - .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (Open Question 3 Gitea Instance Access, Environment Availability — Gitea/act_runner "Unknown", Pitfall 5 Docker socket security) - docker-compose.yml (existing services + networks to understand deploy target) The project has NO git remote configured and Gitea instance availability is unknown (RESEARCH Open Question 3). Creating a Gitea repo, adding the remote, enabling Actions, and obtaining a runner registration token require Gitea web-UI access and credentials only the human has — hence a blocking human-action checkpoint.
The executor presents these steps; the human performs them and supplies values back:
1. Confirm the Gitea base URL (e.g. https://gitea.example). If Gitea is not running, decide whether to add it
   to docker-compose or use an existing instance.
2. Create a repository (e.g. `tessera`) and run `git remote add origin <gitea-repo-url>`.
3. In Gitea, enable Actions for the repo (Settings → Actions) and generate an act_runner registration token
   (Admin/Repo → Actions → Runners → Create registration token).
4. Provide the runner registration token + Gitea instance URL so Task 2 can configure the runner, and confirm
   the deploy host has the project checked out at the docker-compose path for `docker compose` deploys.
1. `git remote -v` shows an `origin` pointing at the Gitea repo 2. The human confirms Actions is enabled for the repo in Gitea settings 3. A runner registration token + Gitea URL are available for Task 2 - `git remote -v` lists a Gitea `origin` remote - Gitea Actions is enabled for the repository (human-confirmed) - Runner registration token and Gitea URL captured for runner setup Type "approved" with the Gitea URL confirmed and runner token available, or describe the blocker Task 2: Define act_runner service and CI/CD setup runbook docker-compose.ci.yml, docs/ci-cd-setup.md - docker-compose.yml (network names frontend-net/backend-net/data-net, service patterns, healthcheck style) - .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (act_runner setup, Pitfall 5 socket security + ephemeral runners, anti-pattern: mount host docker.sock not Docker-in-Docker, anti-pattern: skip registry build locally) Create `docker-compose.ci.yml` (separate compose file so CI infra is opt-in, not part of the app stack) defining an `act_runner` service using the official `gitea/act_runner:latest` image. Configure via env: `GITEA_INSTANCE_URL` (from Task 1), `GITEA_RUNNER_REGISTRATION_TOKEN` (from Task 1, referenced from a `.env`/secret — never hardcoded), `GITEA_RUNNER_NAME` "tessera-runner", and `GITEA_RUNNER_EPHEMERAL=1` (Pitfall 5: ephemeral runner revokes credentials after each job). Mount the host Docker socket `/var/run/docker.sock:/var/run/docker.sock` (RESEARCH anti-pattern: mount the socket, do NOT run Docker-in-Docker) and a named volume for runner config/data. Add restart policy `unless-stopped`.
Create `docs/ci-cd-setup.md` runbook documenting: (1) the Gitea repo + remote setup from Task 1, (2) how to
start the runner `docker compose -f docker-compose.ci.yml up -d`, (3) required secrets/vars
(GITEA_INSTANCE_URL, GITEA_RUNNER_REGISTRATION_TOKEN, and any deploy secrets used by ci.yml such as the deploy
path), (4) the Docker socket security note + ephemeral runner rationale (Pitfall 5), (5) that registry push is
intentionally skipped — images build locally on the same server (D-13, RESEARCH anti-pattern).
test -f docker-compose.ci.yml && grep -q "act_runner" docker-compose.ci.yml && grep -q "GITEA_RUNNER_EPHEMERAL" docker-compose.ci.yml && grep -q "/var/run/docker.sock" docker-compose.ci.yml && test -f docs/ci-cd-setup.md && docker compose -f docker-compose.ci.yml config >/dev/null - `docker compose -f docker-compose.ci.yml config` validates (exit 0) - act_runner mounts the host Docker socket and sets GITEA_RUNNER_EPHEMERAL=1 - Registration token is referenced from env/secret, never hardcoded - docs/ci-cd-setup.md documents remote setup, runner start, secrets, and socket security note act_runner compose file validates; runbook documents the full Gitea + runner setup Task 3: Create .gitea/workflows/ci.yml multi-stage pipeline .gitea/workflows/ci.yml - package.json (root turbo scripts: lint, test, type-check — pipeline calls these via pnpm) - turbo.json (lint/test/type-check tasks exist) - docker-compose.yml (service names `web` and `api`, the build+deploy targets) - apps/web/Dockerfile, apps/api/Dockerfile (reused image builds — do not author new ones) - .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (Pattern 4 Multi-Stage Pipeline YAML, Pitfall 4 docker/build-push-action JWT error → use plain docker commands, anti-pattern skip registry, Security Domain CI secrets) Create `.gitea/workflows/ci.yml` (GitHub Actions-compatible syntax, D-12). Name "Tessera CI/CD", trigger on `push` to branch `main`. Three sequential jobs with `needs` chaining (D-12 staged quality gates):
1. `quality` (runs-on ubuntu-latest): `actions/checkout@v4`, `pnpm/action-setup@v4` (version 9, matching
   packageManager pnpm@9.15.0), `actions/setup-node@v4` (node-version 24, cache pnpm), `pnpm install
   --frozen-lockfile`, `pnpm lint` (Biome via turbo), `pnpm type-check`.
2. `test` (needs: quality): same checkout/pnpm/node setup, `pnpm install --frozen-lockfile`, `pnpm test`
   (Vitest via turbo — `apps/web/vitest.config.ts` exists).
3. `build-deploy` (needs: test): checkout, then build + deploy with PLAIN docker compose commands (Pitfall 4 —
   do NOT use docker/build-push-action which fails on Gitea's non-JWT token; Pattern 4 + RESEARCH anti-pattern):
   `docker compose build web api` then `docker compose up -d web api`. Since the deploy target is the same
   server as the runner (D-13), this builds images locally and restarts the services with no registry push.

Never echo secrets in steps. If the runbook (Task 2) defines deploy secrets/vars (e.g. a deploy path), read
them via Gitea Actions `${{ secrets.* }}` / `${{ vars.* }}`, not inline literals (Security Domain: CI secrets
leakage). Keep the workflow minimal and readable per CLAUDE.md (Claude builds maintainable infra).
test -f .gitea/workflows/ci.yml && grep -q "on:" .gitea/workflows/ci.yml && grep -q "pnpm lint" .gitea/workflows/ci.yml && grep -q "pnpm test" .gitea/workflows/ci.yml && grep -q "docker compose build" .gitea/workflows/ci.yml && ! grep -q "build-push-action" .gitea/workflows/ci.yml && python3 -c "import yaml,sys; yaml.safe_load(open('.gitea/workflows/ci.yml'))" - ci.yml is valid YAML and triggers on push to main - Three chained jobs: quality (lint+type-check) → test (vitest) → build-deploy (docker compose) - Uses plain `docker compose build`/`up -d` — NOT docker/build-push-action (Pitfall 4) - No hardcoded secrets; secrets/vars referenced via `${{ }}` expressions Valid multi-stage pipeline file present; runs lint+type-check → tests → local docker build+deploy on push Task 4: Trigger the pipeline with a real push and confirm it runs green - .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (Validation Architecture — INFRA-04 integration test "Push to Gitea, verify Actions run") - docs/ci-cd-setup.md (the runbook produced in Task 2) The complete CI/CD track: Gitea remote (Task 1), act_runner (Task 2), and the ci.yml pipeline (Task 3). INFRA-04 can only be proven by an actual push that triggers Gitea Actions — this requires the live Gitea instance + running runner, so it is a blocking human-verify checkpoint.
Before this checkpoint the executor ensures the runner is up (`docker compose -f docker-compose.ci.yml up -d`)
and the workflow + compose files are committed to the branch that will be pushed.
1. Push the current main to the Gitea remote (`git push origin main`) — Claude does this manually per D-11. 2. Open the Gitea repo → Actions tab and confirm a "Tessera CI/CD" run started for the push. 3. Confirm the three jobs run in order and the `quality` and `test` jobs pass (lint, type-check, vitest). 4. Confirm `build-deploy` builds the web + api images and restarts them (`docker compose ps` shows them up). 5. Confirm no secrets appear in the job logs. - A Gitea Actions run is triggered by the push (INFRA-04) - quality + test jobs pass; build-deploy rebuilds and restarts web + api - No secret values leaked in pipeline logs Type "approved" once the pipeline run is green, or paste the failing job log

<threat_model>

Trust Boundaries

Boundary Description
Gitea push → act_runner execution Pushed workflow code executes on the runner with Docker socket access
act_runner → host Docker daemon Mounted /var/run/docker.sock grants the runner control over host containers
pipeline → secrets store Deploy/registration secrets pass through Gitea Actions context

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-06-07 Elevation act_runner Docker socket mount accept Internal-only CI, only Claude pushes (D-11); GITEA_RUNNER_EPHEMERAL=1 revokes creds per job (Pitfall 5)
T-06-08 Information Disclosure secrets in pipeline logs mitigate Reference secrets via ${{ secrets.* }}/${{ vars.* }}; never echo; registration token from env not hardcoded (Security Domain)
T-06-09 Tampering malicious workflow modification accept Single trusted committer (Claude), push to main only; no external contributors (D-10/D-11)
T-06-SC Tampering act_runner image mitigate Use official gitea/act_runner image; CI actions pinned to major (checkout@v4, setup-node@v4)
</threat_model>
- `docker compose -f docker-compose.ci.yml config` validates the runner service - `.gitea/workflows/ci.yml` is valid YAML with three chained jobs and no build-push-action - A real push (Task 4) triggers a green Gitea Actions run that rebuilds and redeploys web + api

<success_criteria>

  • Gitea remote configured and main pushed (INFRA-04)
  • act_runner registered and executing jobs
  • Multi-stage pipeline (lint+type-check → tests → docker build+deploy) runs on push (D-12, D-13)
  • No Git/Gitea functionality added to the Tessera app itself (D-10) </success_criteria>
Create `.planning/phases/06-desktop-client-ci-cd/06-03-SUMMARY.md` when done.