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

259 lines
15 KiB
Markdown

---
phase: 06-desktop-client-ci-cd
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
- .gitea/workflows/ci.yml
- docker-compose.ci.yml
- docs/ci-cd-setup.md
autonomous: false
requirements:
- INFRA-04
user_setup:
- service: gitea
why: "Git remote + CI/CD host. Project currently has no git remote; Gitea instance access (URL, Actions enabled) is unknown per RESEARCH Open Question 3"
dashboard_config:
- task: "Create a Tessera repository in Gitea and add it as git remote"
location: "Gitea web UI → New Repository, then `git remote add origin <url>`"
- task: "Enable Actions for the repository and register an act_runner"
location: "Gitea → Repo Settings → Actions, and runner registration token under Admin/Repo → Actions → Runners"
must_haves:
truths:
- "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"
artifacts:
- path: ".gitea/workflows/ci.yml"
provides: "Multi-stage CI/CD pipeline (quality → test → build-deploy)"
contains: "runs-on"
- path: "docker-compose.ci.yml"
provides: "act_runner service definition (Docker socket mount, ephemeral)"
contains: "act_runner"
- path: "docs/ci-cd-setup.md"
provides: "Setup runbook: Gitea remote, act_runner registration, secrets"
key_links:
- from: ".gitea/workflows/ci.yml"
to: "existing docker-compose.yml services web + api"
via: "docker compose build + up -d on the deploy host"
pattern: "docker compose"
- from: ".gitea/workflows/ci.yml"
to: "root package.json turbo tasks"
via: "pnpm lint / type-check / test"
pattern: "pnpm (lint|test|type-check)"
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<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
</context>
<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>
<tasks>
<task type="checkpoint:human-action" gate="blocking-human">
<name>Task 1: Set up Gitea remote and register act_runner</name>
<read_first>
- .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)
</read_first>
<what-built>
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.
</what-built>
<how-to-verify>
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
</how-to-verify>
<acceptance_criteria>
- `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
</acceptance_criteria>
<resume-signal>Type "approved" with the Gitea URL confirmed and runner token available, or describe the blocker</resume-signal>
</task>
<task type="auto" tdd="false">
<name>Task 2: Define act_runner service and CI/CD setup runbook</name>
<files>docker-compose.ci.yml, docs/ci-cd-setup.md</files>
<read_first>
- 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)
</read_first>
<action>
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).
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- `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
</acceptance_criteria>
<done>act_runner compose file validates; runbook documents the full Gitea + runner setup</done>
</task>
<task type="auto" tdd="false">
<name>Task 3: Create .gitea/workflows/ci.yml multi-stage pipeline</name>
<files>.gitea/workflows/ci.yml</files>
<read_first>
- 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)
</read_first>
<action>
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).
</action>
<verify>
<automated>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'))"</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>Valid multi-stage pipeline file present; runs lint+type-check → tests → local docker build+deploy on push</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 4: Trigger the pipeline with a real push and confirm it runs green</name>
<read_first>
- .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)
</read_first>
<what-built>
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.
</what-built>
<how-to-verify>
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.
</how-to-verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<resume-signal>Type "approved" once the pipeline run is green, or paste the failing job log</resume-signal>
</task>
</tasks>
<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>
<verification>
- `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
</verification>
<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>
<output>
Create `.planning/phases/06-desktop-client-ci-cd/06-03-SUMMARY.md` when done.
</output>