44154a4697
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
259 lines
15 KiB
Markdown
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>
|