--- 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 `" - 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)" --- 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. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.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 - `.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 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 `. 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 ## 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) | - `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 - 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) Create `.planning/phases/06-desktop-client-ci-cd/06-03-SUMMARY.md` when done.