docs(06-desktop-client-ci-cd): create phase plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
---
|
||||
phase: 06-desktop-client-ci-cd
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- apps/desktop/package.json
|
||||
- apps/desktop/src-tauri/Cargo.toml
|
||||
- apps/desktop/src-tauri/tauri.conf.json
|
||||
- apps/desktop/src-tauri/build.rs
|
||||
- apps/desktop/src-tauri/src/main.rs
|
||||
- apps/desktop/src-tauri/src/lib.rs
|
||||
- apps/desktop/src-tauri/capabilities/default.json
|
||||
- apps/desktop/src/setup.html
|
||||
autonomous: false
|
||||
requirements:
|
||||
- DESK-01
|
||||
- DESK-02
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Tauri toolchain prerequisites (Rust + WebKitGTK dev headers) are installed and verifiable"
|
||||
- "apps/desktop is a Tauri 2.x project registered in the pnpm workspace as @tessera/desktop"
|
||||
- "On first launch the app shows a local setup page where the user enters a server URL"
|
||||
- "The entered server URL is persisted via tauri-plugin-store and reused on subsequent launches"
|
||||
- "After a URL is configured, the WebView loads the configured Tessera server (no bundled frontend assets)"
|
||||
artifacts:
|
||||
- path: "apps/desktop/src-tauri/tauri.conf.json"
|
||||
provides: "Tauri window + bundle config, frontendDist pointing at local setup page"
|
||||
contains: "productName"
|
||||
- path: "apps/desktop/src-tauri/src/lib.rs"
|
||||
provides: "Rust entry: plugin registration, store read, navigate-to-server-URL logic"
|
||||
min_lines: 25
|
||||
- path: "apps/desktop/src/setup.html"
|
||||
provides: "First-run server URL configuration page using tauri-plugin-store"
|
||||
contains: "server_url"
|
||||
- path: "apps/desktop/src-tauri/capabilities/default.json"
|
||||
provides: "Plugin permission grants for store"
|
||||
contains: "store:default"
|
||||
key_links:
|
||||
- from: "apps/desktop/src/setup.html"
|
||||
to: "tauri-plugin-store (config.json)"
|
||||
via: "load() + store.set('server_url', url)"
|
||||
pattern: "server_url"
|
||||
- from: "apps/desktop/src-tauri/src/lib.rs"
|
||||
to: "main WebView window"
|
||||
via: "read stored server_url, navigate window to it on startup"
|
||||
pattern: "server_url"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Establish the Tauri 2.x desktop wrapper foundation as a thin URL-loading shell. This is the
|
||||
first vertical slice for DESK-01/DESK-02: install the missing toolchain, scaffold `apps/desktop`
|
||||
in the monorepo, and deliver an app that launches, prompts for a server URL on first run, persists
|
||||
it, and loads the configured Tessera web frontend in the native WebView.
|
||||
|
||||
Purpose: A user can run the desktop app and reach the live Tessera web app — the thinnest
|
||||
end-to-end desktop experience. All native polish (tray, window state, autostart, notifications,
|
||||
icon, production bundles) is layered on in Plan 06-02.
|
||||
|
||||
Output: A runnable `apps/desktop` Tauri project (`pnpm --filter=@tessera/desktop tauri dev`)
|
||||
that connects to a user-configured server URL and remembers it.
|
||||
</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
|
||||
@pnpm-workspace.yaml
|
||||
@apps/web/src/app/globals.css
|
||||
</context>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
This plan introduces the following symbols/files (consumed by Plan 06-02):
|
||||
- `apps/desktop/` — Tauri project root, pnpm package `@tessera/desktop`
|
||||
- `apps/desktop/src-tauri/src/lib.rs::run()` — Rust entry function (extended in 06-02)
|
||||
- `apps/desktop/src-tauri/tauri.conf.json` — central Tauri config (window, bundle, plugins)
|
||||
- `apps/desktop/src-tauri/capabilities/default.json` — permission grants (extended in 06-02)
|
||||
- `apps/desktop/src/setup.html` — first-run server URL page
|
||||
- tauri-plugin-store `config.json` key `server_url` — persisted server URL contract
|
||||
</artifacts_this_phase_produces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking-human">
|
||||
<name>Task 1: Install Tauri toolchain prerequisites (Rust + WebKitGTK dev headers)</name>
|
||||
<read_first>
|
||||
- .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (Environment Availability table, Pitfall 1, Installation block)
|
||||
</read_first>
|
||||
<what-built>
|
||||
RESEARCH.md confirms the machine is MISSING the Rust toolchain and `libwebkit2gtk-4.1-dev`
|
||||
(plus likely libssl-dev, libxdo-dev, build-essential). These require `sudo apt` and a rustup
|
||||
network installer — both need human authorization, so this is a blocking human-action checkpoint.
|
||||
|
||||
Execute these commands (the executor presents them; the human runs/authorizes sudo):
|
||||
- apt install: `sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev`
|
||||
- Rust: `curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh -s -- -y` then `source "$HOME/.cargo/env"`
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
Run each and confirm a real version/path is printed (not "not found"):
|
||||
1. `rustc --version` prints a version >= 1.77.2
|
||||
2. `cargo --version` prints a version
|
||||
3. `pkg-config --modversion webkit2gtk-4.1` prints a version (e.g. 2.x)
|
||||
4. `pkg-config --exists libssl && echo OK` prints OK
|
||||
</how-to-verify>
|
||||
<acceptance_criteria>
|
||||
- `rustc --version` exits 0 and prints version >= 1.77.2
|
||||
- `pkg-config --modversion webkit2gtk-4.1` exits 0 (no "not found")
|
||||
</acceptance_criteria>
|
||||
<resume-signal>Type "approved" once all four checks print versions, or describe which failed</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 2: Scaffold apps/desktop Tauri project as URL-loading wrapper</name>
|
||||
<files>apps/desktop/package.json, apps/desktop/src-tauri/Cargo.toml, apps/desktop/src-tauri/tauri.conf.json, apps/desktop/src-tauri/build.rs, apps/desktop/src-tauri/src/main.rs, apps/desktop/src-tauri/src/lib.rs, apps/desktop/src-tauri/capabilities/default.json</files>
|
||||
<read_first>
|
||||
- .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (Pattern 1 URL-Loading WebView, Recommended Project Structure, Cargo.toml Plugin Dependencies, Capabilities Configuration, Pitfall 6 monorepo scaffolding)
|
||||
- pnpm-workspace.yaml (confirms apps/* glob already covers apps/desktop)
|
||||
- apps/api/package.json (for name/version pattern: `@tessera/*`, version 0.0.1, private true)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `apps/desktop/` manually to avoid the monorepo scaffold confusion in Pitfall 6 (do NOT run
|
||||
create-tauri-app at repo root). Set package.json name to `@tessera/desktop`, version `0.0.1`, private true,
|
||||
with scripts `tauri` (runs `tauri`), `dev` (`tauri dev`), `build` (`tauri build`). Add devDependency
|
||||
`@tauri-apps/cli@2.11.3` and dependencies `@tauri-apps/api@2.11.1` plus `@tauri-apps/plugin-store@2.4.3`.
|
||||
Install via `pnpm add -D @tauri-apps/cli --filter=@tessera/desktop` and `pnpm add @tauri-apps/api @tauri-apps/plugin-store --filter=@tessera/desktop`.
|
||||
|
||||
Create `src-tauri/Cargo.toml` with package name `tessera-desktop`, edition 2021, a `[lib]` entry named
|
||||
`tessera_desktop_lib` (crate-type cdylib + staticlib + rlib), build-dependency `tauri-build = "2"`, and
|
||||
dependencies: `tauri = { version = "2", features = ["tray-icon"] }`, `tauri-plugin-store = "2"`,
|
||||
`serde = { version = "1", features = ["derive"] }`, `serde_json = "1"`. Only the store plugin is wired in
|
||||
this plan; notification/autostart/window-state are added in 06-02.
|
||||
|
||||
Create `src-tauri/build.rs` calling `tauri_build::build()`.
|
||||
|
||||
Create `src-tauri/tauri.conf.json`: `productName` "Tessera", `version` "0.0.1", `identifier`
|
||||
"de.ctl.tessera.desktop". Under `build`, set `frontendDist` to `../src` (the local setup page directory) and
|
||||
`devUrl` to `http://localhost:1420`. Under `app.windows`, one window: label "main", title "Tessera",
|
||||
width 1280, height 800, center true, decorations true, resizable true. Set `app.withGlobalTauri` true.
|
||||
Add `app.security.csp` allowing connection to the configured server (use `default-src 'self'; connect-src *`
|
||||
for the URL-configurable wrapper per D-02). Under `bundle`, set `active` true, `targets` `["appimage", "nsis"]`,
|
||||
`icon` `["icons/icon.png", "icons/icon.ico"]` (icons added in 06-02 — note this in a SUMMARY follow-up if build
|
||||
is attempted before icons exist).
|
||||
|
||||
Create `src-tauri/src/main.rs` as the generated entry: `#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]`
|
||||
then `fn main() { tessera_desktop_lib::run() }`.
|
||||
|
||||
Create `src-tauri/src/lib.rs` exposing `pub fn run()`. Register the store plugin via
|
||||
`tauri_plugin_store::Builder::new().build()`. In a `.setup(|app| { ... })` callback, open the store
|
||||
`config.json`, read key `server_url`; if present, get the `main` webview window and call `window.navigate(url)`
|
||||
to load the configured server; if absent, leave the WebView on the bundled setup page. Use
|
||||
`tauri::generate_context!()` in `.run(...)`.
|
||||
|
||||
Create `src-tauri/capabilities/default.json` with identifier "default", windows `["main"]`, permissions
|
||||
`["core:default", "store:default"]` (additional permissions added in 06-02).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f apps/desktop/src-tauri/tauri.conf.json && test -f apps/desktop/src-tauri/src/lib.rs && grep -q '@tessera/desktop' apps/desktop/package.json && grep -q 'store:default' apps/desktop/src-tauri/capabilities/default.json && cd apps/desktop/src-tauri && cargo check 2>&1 | grep -qiv 'could not find webkit'</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `apps/desktop/package.json` name is `@tessera/desktop`
|
||||
- `cargo check` inside `apps/desktop/src-tauri` completes without WebKitGTK "could not find" errors
|
||||
- `tauri.conf.json` `frontendDist` is `../src` (not a hardcoded server URL)
|
||||
- `lib.rs` reads `server_url` from store and calls `navigate` when present
|
||||
</acceptance_criteria>
|
||||
<done>cargo check passes; pnpm recognizes @tessera/desktop; config loads the local setup page as frontendDist</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 3: First-run setup page — server URL input, validation, persistence, navigate</name>
|
||||
<files>apps/desktop/src/setup.html</files>
|
||||
<read_first>
|
||||
- .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (Pattern 3 Configurable Server URL, First-Run Setup Page code example, Security Domain — V5 Input Validation, malicious URL threat)
|
||||
- apps/web/src/app/globals.css (OKLCH design tokens: --primary oklch(0.91 0.19 102), dark background oklch(0.17 0.01 260) — match setup page colors to the design system per D-06 spirit)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `apps/desktop/src/setup.html` as the local first-run page (D-02). It must:
|
||||
- Render a centered card on dark background `oklch(0.17 0.01 260)` with a "Tessera" heading and a URL input
|
||||
pre-filled with `http://localhost:3000`, plus a "Verbinden" button styled with primary `oklch(0.91 0.19 102)`.
|
||||
All visible strings in German (response_language de): heading "Tessera", label "Server-URL eingeben:",
|
||||
button "Verbinden", error text "Ungueltige URL" for invalid input.
|
||||
- On submit, validate input with the `URL` constructor (per the established Phase 5 pattern, decision [05]
|
||||
"URL constructor for client-side https-only validation"). Reject empty/malformed input; for non-https URLs
|
||||
that are not localhost, show a warning string but allow (internal LAN servers may be http) — this mitigates
|
||||
the malicious-URL Tampering threat T-06-01 (V5 input validation).
|
||||
- On valid input, `import { load } from '@tauri-apps/plugin-store'`, `load('config.json', { autoSave: true })`,
|
||||
`store.set('server_url', url)`, then `window.location.href = url` to navigate the WebView to the server.
|
||||
- Use `<script type="module">` and rely on `withGlobalTauri`/the store plugin already permitted in capabilities.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f apps/desktop/src/setup.html && grep -q "server_url" apps/desktop/src/setup.html && grep -q "new URL" apps/desktop/src/setup.html && grep -q "plugin-store" apps/desktop/src/setup.html</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- setup.html validates the URL with `new URL(...)` before saving
|
||||
- setup.html calls `store.set('server_url', ...)` and then navigates to the URL
|
||||
- Visible strings are German ("Verbinden", "Server-URL eingeben:")
|
||||
</acceptance_criteria>
|
||||
<done>Entering a valid URL persists it to config.json and navigates the WebView; invalid input is rejected with a German error</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| user input → WebView navigation | Server URL typed by user controls where the WebView connects (untrusted text crosses into navigation) |
|
||||
| local config store → app startup | Persisted `server_url` is read on launch and drives navigation |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-06-01 | Tampering | setup.html server URL input | mitigate | Validate with `new URL()` constructor; reject malformed; warn on non-https non-localhost (V5 input validation) |
|
||||
| T-06-02 | Spoofing | WebView navigation target | accept | Internal-only tool; URL is user-chosen by design (D-02). CSP `connect-src *` required for configurable server. Locking to a single domain conflicts with the configurable-URL requirement; revisit at external-sales stage |
|
||||
| T-06-03 | Information Disclosure | config.json stored URL | accept | URL is non-secret connection info stored via plugin-store (atomic, app-scoped path), not plaintext app config |
|
||||
| T-06-SC | Tampering | npm/cargo installs | mitigate | All Tauri packages [Approved] in RESEARCH.md Package Legitimacy Audit; no [ASSUMED]/[SUS] blocking packages in this plan |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `apps/desktop/src-tauri` `cargo check` passes (toolchain installed, config valid)
|
||||
- `pnpm --filter=@tessera/desktop tauri dev` launches a window showing the setup page on first run
|
||||
- Entering `http://localhost:3000` (with the dev stack running) navigates to the Tessera web app
|
||||
- Relaunching the app skips setup and loads the stored URL directly
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Tauri toolchain installed and verifiable (rustc, webkit2gtk-4.1 pkg-config)
|
||||
- `apps/desktop` exists as `@tessera/desktop` and `cargo check` passes
|
||||
- First-run setup page persists the server URL and connects the WebView to it (DESK-02)
|
||||
- No frontend assets bundled — `frontendDist` is the local setup page only (DESK-01 wrapper pattern)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/06-desktop-client-ci-cd/06-01-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,284 @@
|
||||
---
|
||||
phase: 06-desktop-client-ci-cd
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 06-01
|
||||
files_modified:
|
||||
- apps/desktop/src-tauri/src/lib.rs
|
||||
- apps/desktop/src-tauri/Cargo.toml
|
||||
- apps/desktop/src-tauri/tauri.conf.json
|
||||
- apps/desktop/src-tauri/capabilities/default.json
|
||||
- apps/desktop/src-tauri/icons/icon.png
|
||||
- apps/desktop/src-tauri/icons/icon.ico
|
||||
- apps/desktop/src-tauri/icons/128x128.png
|
||||
- apps/api/src/health/health.controller.ts
|
||||
autonomous: false
|
||||
requirements:
|
||||
- DESK-01
|
||||
- DESK-02
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Clicking the window close (X) hides the app to the system tray instead of quitting"
|
||||
- "The system tray icon shows a context menu with Oeffnen and Beenden; Beenden actually exits the app"
|
||||
- "Window size and position are saved on close and restored on next launch (first run 1280x800 centered)"
|
||||
- "An Autostart toggle exists and registers/unregisters the app with the OS, defaulting to disabled"
|
||||
- "On startup the app fetches the server /version endpoint and shows a native notification when a newer version is available"
|
||||
- "The app uses a Tessera-branded icon derived from the OKLCH design system"
|
||||
- "Production bundles build for Linux (AppImage) and Windows (NSIS)"
|
||||
artifacts:
|
||||
- path: "apps/desktop/src-tauri/src/lib.rs"
|
||||
provides: "Tray, close-to-tray, plugin registration, version-check logic"
|
||||
contains: "TrayIconBuilder"
|
||||
- path: "apps/api/src/health/health.controller.ts"
|
||||
provides: "GET /health/version endpoint returning app version"
|
||||
contains: "version"
|
||||
- path: "apps/desktop/src-tauri/icons/icon.png"
|
||||
provides: "Tessera app + tray icon"
|
||||
key_links:
|
||||
- from: "apps/desktop/src-tauri/src/lib.rs"
|
||||
to: "main window CloseRequested event"
|
||||
via: "window.hide() + api.prevent_close()"
|
||||
pattern: "prevent_close"
|
||||
- from: "apps/desktop/src-tauri/src/lib.rs"
|
||||
to: "apps/api GET /health/version"
|
||||
via: "HTTP fetch of {server_url}/health/version on startup"
|
||||
pattern: "version"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Layer all native desktop behaviors onto the working URL-loading wrapper from Plan 06-01:
|
||||
system tray with close-to-tray (D-03), window state persistence (D-05), autostart toggle (D-04),
|
||||
native notifications + startup version check (D-01, D-08), Tessera-branded icon (D-06), and
|
||||
production bundle targets AppImage + NSIS (D-07). Also add the API `/health/version` endpoint
|
||||
the version check depends on. Code signing is intentionally NOT configured (D-09 — deferred to
|
||||
external-sales stage).
|
||||
|
||||
Purpose: Completes DESK-01/DESK-02 — a user can install a real, well-behaved Tessera desktop app
|
||||
that lives in the tray, remembers its window, optionally autostarts, and warns about updates.
|
||||
|
||||
Output: A buildable Tauri app producing unsigned AppImage (Linux) and NSIS (Windows) installers
|
||||
with full native integration, plus a `/health/version` API endpoint.
|
||||
</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
|
||||
@.planning/phases/06-desktop-client-ci-cd/06-01-SUMMARY.md
|
||||
@apps/api/src/health/health.controller.ts
|
||||
@apps/web/src/app/globals.css
|
||||
</context>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
- `apps/api` `GET /health/version` → `{ version, name }` (consumed by desktop version check)
|
||||
- `apps/desktop/src-tauri/src/lib.rs::run()` — extended with tray, close-to-tray, 4 plugins, version check
|
||||
- `apps/desktop/src-tauri/icons/{icon.png,icon.ico,128x128.png}` — branded app/tray icons
|
||||
- Autostart toggle UI hook (invoked from within the loaded web app via tauri-plugin-autostart API)
|
||||
</artifacts_this_phase_produces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Add GET /health/version endpoint to the API</name>
|
||||
<files>apps/api/src/health/health.controller.ts</files>
|
||||
<read_first>
|
||||
- apps/api/src/health/health.controller.ts (existing controller, @Public decorator pattern, HealthResponse import)
|
||||
- .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (Version Check Endpoint code example)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- GET /health/version returns HTTP 200 with JSON `{ version: string, name: 'tessera' }`
|
||||
- The endpoint is public (no auth required) — reachable by the desktop shell before login
|
||||
- version resolves from `process.env.npm_package_version` falling back to '0.0.1'
|
||||
</behavior>
|
||||
<action>
|
||||
Extend the existing `HealthController` with a `@Public() @Get('version')` handler `getVersion()` returning an
|
||||
object `{ version: process.env.npm_package_version ?? '0.0.1', name: 'tessera' }`. Reuse the existing `@Public`
|
||||
decorator import already present in the file. If a `HealthResponse`-style shared type is desired, define a
|
||||
small inline return type or extend `@tessera/shared`; keep it minimal. Mark the route public so the desktop
|
||||
version check (D-08) can reach it pre-auth.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/api && pnpm type-check && grep -q "'version'" src/health/health.controller.ts && grep -q "name: 'tessera'" src/health/health.controller.ts</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `GET /health/version` handler exists, is decorated `@Public()`, returns `{ version, name: 'tessera' }`
|
||||
- `pnpm --filter=@tessera/api type-check` passes
|
||||
</acceptance_criteria>
|
||||
<done>API exposes a public /health/version returning the app version; type-check green</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 2: Tray, close-to-tray, window-state, autostart, notification, version check in Rust</name>
|
||||
<files>apps/desktop/src-tauri/Cargo.toml, apps/desktop/src-tauri/src/lib.rs, apps/desktop/src-tauri/capabilities/default.json</files>
|
||||
<read_first>
|
||||
- apps/desktop/src-tauri/src/lib.rs (the run() function from 06-01 — extend, do not rewrite the store/navigate logic)
|
||||
- apps/desktop/src-tauri/Cargo.toml (existing deps from 06-01)
|
||||
- apps/desktop/src-tauri/capabilities/default.json (existing permissions from 06-01)
|
||||
- .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (Pattern 2 Close-to-Tray full Rust example, Pitfall 2 both CloseRequested + ExitRequested, Capabilities Configuration, Version Check Rust example, A6 reqwest assumption)
|
||||
</read_first>
|
||||
<action>
|
||||
Extend `Cargo.toml` dependencies with `tauri-plugin-notification = "2"`, `tauri-plugin-autostart = "2"`,
|
||||
`tauri-plugin-window-state = "2"`, and `reqwest = { version = "0.12", features = ["json"] }` (per A6; if reqwest
|
||||
proves problematic, fall back to JS-side fetch — note the fallback in SUMMARY). Keep the existing
|
||||
`tauri = { features = ["tray-icon"] }`.
|
||||
|
||||
Extend `lib.rs::run()` registering all four plugins on the builder: `tauri_plugin_store` (already present),
|
||||
`tauri_plugin_notification::init()`, `tauri_plugin_window_state::Builder::default().build()`, and
|
||||
`tauri_plugin_autostart::init(tauri_plugin_autostart::MacosLauncher::LaunchAgent, None)`. Window-state plugin
|
||||
auto-handles D-05 save/restore; the 1280x800 centered first-run default already lives in tauri.conf.json.
|
||||
|
||||
In `.setup`, build the tray (D-03): a `MenuBuilder` with items `open` ("Oeffnen") and `quit` ("Beenden")
|
||||
separated by a separator. Create `TrayIconBuilder` with the bundled icon bytes (icon.png from Task 3),
|
||||
tooltip "Tessera", `show_menu_on_left_click(false)`. `on_menu_event`: "open" → show + set_focus the `main`
|
||||
window; "quit" → `app.exit(0)`. `on_tray_icon_event`: left-click Up → show + focus `main` window.
|
||||
|
||||
Add close-to-tray (D-03, Pitfall 2): `.on_window_event` handling `WindowEvent::CloseRequested` by calling
|
||||
`window.hide()` then `api.prevent_close()`. ALSO handle the exit path so the process stays alive when the
|
||||
last window hides — implement the `RunEvent::ExitRequested` guard (call `api.prevent_exit()`) via
|
||||
`.build(...).run(|_app, event| ...)` so the only real exit path is the tray "Beenden" → `app.exit(0)`.
|
||||
|
||||
Add the version check (D-08): after reading server_url in setup, spawn an async task that GETs
|
||||
`{server_url}/health/version`, parses JSON `version`, compares against the app's own
|
||||
`env!("CARGO_PKG_VERSION")`; if different/newer, send a native notification via the notification plugin with a
|
||||
German body (e.g. title "Tessera Update", body "Eine neue Version ist verfuegbar."). Download stays manual.
|
||||
|
||||
Extend `capabilities/default.json` permissions to add: `notification:default`,
|
||||
`notification:allow-is-permission-granted`, `notification:allow-request-permission`, `notification:allow-notify`,
|
||||
`autostart:allow-enable`, `autostart:allow-disable`, `autostart:allow-is-enabled`, `window-state:default`
|
||||
(keep existing `core:default`, `store:default`). These grant the loaded web app (D-04 autostart toggle) and
|
||||
Rust side the needed plugin access.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/desktop/src-tauri && grep -q "TrayIconBuilder" src/lib.rs && grep -q "prevent_close" src/lib.rs && grep -q "prevent_exit" src/lib.rs && grep -q "health/version" src/lib.rs && grep -q "autostart:allow-enable" capabilities/default.json && grep -q "window-state:default" capabilities/default.json && cargo check</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `cargo check` passes with all four plugins registered
|
||||
- lib.rs handles BOTH `prevent_close` (CloseRequested) AND `prevent_exit` (ExitRequested) per Pitfall 2
|
||||
- lib.rs fetches `{server_url}/health/version` and notifies on version mismatch
|
||||
- capabilities grants autostart + notification + window-state permissions
|
||||
</acceptance_criteria>
|
||||
<done>cargo check passes; tray + close-to-tray + window-state + autostart + version-check wired; D-01/03/04/05/08 covered in code</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 3: Generate Tessera-branded icon and configure unsigned AppImage + NSIS bundles</name>
|
||||
<files>apps/desktop/src-tauri/icons/icon.png, apps/desktop/src-tauri/icons/icon.ico, apps/desktop/src-tauri/icons/128x128.png, apps/desktop/src-tauri/tauri.conf.json</files>
|
||||
<read_first>
|
||||
- apps/desktop/src-tauri/tauri.conf.json (existing bundle config from 06-01 — targets already ["appimage","nsis"])
|
||||
- apps/web/src/app/globals.css (OKLCH tokens: --primary oklch(0.91 0.19 102) yellow, dark bg oklch(0.17 0.01 260) — icon palette per D-06)
|
||||
- .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (bundle targets, NSIS over MSI rationale, Pitfall A5 icon format)
|
||||
</read_first>
|
||||
<action>
|
||||
Create a Tessera app icon (D-06) derived from the OKLCH design system: a simple square mark using primary
|
||||
yellow `oklch(0.91 0.19 102)` on the dark `oklch(0.17 0.01 260)` background (e.g. a stylized "T" or tessera
|
||||
tile). Author a master 1024x1024 PNG (place a source SVG/PNG under `apps/desktop/src-tauri/icons/` or
|
||||
`scratchpad`), then generate the platform icon set with `pnpm --filter=@tessera/desktop tauri icon <master.png>`,
|
||||
which produces `icon.png`, `icon.ico`, `128x128.png`, and the platform variants Tauri expects. If `tauri icon`
|
||||
cannot run headless, fall back to producing icon.png (1024), 128x128.png, and icon.ico manually with
|
||||
ImageMagick (`convert`) and note it in SUMMARY.
|
||||
|
||||
Confirm `tauri.conf.json` `bundle.icon` references the generated `icons/icon.png` and `icons/icon.ico`, and
|
||||
`bundle.targets` is `["appimage", "nsis"]` (D-07 AppImage primary; NSIS chosen over MSI per RESEARCH so the
|
||||
Windows installer can cross-compile from Linux). Ensure the tray icon bytes referenced in lib.rs (Task 2)
|
||||
point at an existing icon file (e.g. `icons/128x128.png` or `icons/icon.png`).
|
||||
|
||||
Per D-09, do NOT configure code signing — internal use ships unsigned by design; signing is deferred until
|
||||
Tessera is sold externally. Leave all `bundle` signing/certificate fields unset so builds produce unsigned
|
||||
AppImage and NSIS artifacts.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f apps/desktop/src-tauri/icons/icon.png && test -f apps/desktop/src-tauri/icons/icon.ico && grep -q '"appimage"' apps/desktop/src-tauri/tauri.conf.json && grep -q '"nsis"' apps/desktop/src-tauri/tauri.conf.json && file apps/desktop/src-tauri/icons/icon.png | grep -qi png</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `icons/icon.png` and `icons/icon.ico` exist and are valid image files
|
||||
- `bundle.targets` includes both `appimage` and `nsis`
|
||||
- Icon palette uses the OKLCH primary/dark colors from the design system
|
||||
- No code-signing/certificate fields configured (D-09 unsigned for internal use)
|
||||
</acceptance_criteria>
|
||||
<done>Branded icon set generated; bundle configured for unsigned AppImage + NSIS</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 4: Build the desktop app and verify native behaviors</name>
|
||||
<read_first>
|
||||
- .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (Validation Architecture — desktop smoke tests for D-02/D-03/D-05; Pitfall 2 warning signs)
|
||||
</read_first>
|
||||
<what-built>
|
||||
The full native desktop wrapper: tray, close-to-tray, window-state, autostart, notifications, version check,
|
||||
branded icon, AppImage + NSIS bundles. Tauri desktop behavior cannot be unit-tested (RESEARCH Wave 0 Gaps) —
|
||||
it requires manual smoke verification of OS-level integration.
|
||||
|
||||
Before this checkpoint the executor runs (and reports output of):
|
||||
- `pnpm --filter=@tessera/api type-check` and `pnpm --filter=@tessera/web type-check` (regression)
|
||||
- `cd apps/desktop && pnpm tauri build` — produces the Linux AppImage (Windows NSIS may require a Windows
|
||||
runner / cross toolchain; if NSIS cross-build is unavailable on this Linux host, report it and treat the
|
||||
Windows target as build-config-verified only, AppImage as the proven artifact for DESK-01)
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
With the dev web stack running, launch the app (`pnpm --filter=@tessera/desktop tauri dev` or the built AppImage):
|
||||
1. First run shows the German setup page; enter `http://localhost:3000` → Tessera web app loads (DESK-02)
|
||||
2. Click the window X → window disappears but a Tessera tray icon remains; the process is still alive (D-03)
|
||||
3. Right-click tray → "Oeffnen" restores the window; "Beenden" fully exits the process (D-03)
|
||||
4. Resize/move the window, close to tray and quit, relaunch → window restores prior size/position (D-05)
|
||||
5. Confirm an Autostart toggle is reachable and defaults to off (D-04)
|
||||
6. If the server reports a different /health/version, a native update notification appears (D-08)
|
||||
7. Confirm `apps/desktop/src-tauri/target/release/bundle/appimage/*.AppImage` exists (DESK-01)
|
||||
</how-to-verify>
|
||||
<acceptance_criteria>
|
||||
- Setup → server URL → web app loads (DESK-02)
|
||||
- X hides to tray, process alive; "Beenden" exits (D-03)
|
||||
- Window state restored on relaunch (D-05)
|
||||
- An AppImage artifact is produced (DESK-01)
|
||||
</acceptance_criteria>
|
||||
<resume-signal>Type "approved" or describe which behavior failed</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| desktop shell → API /health/version | Desktop fetches version info from the configured server over HTTP |
|
||||
| loaded web app → autostart plugin | The web UI (D-04 toggle) invokes OS-level autostart registration via granted capability |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-06-04 | Spoofing | /health/version response | accept | Version string is non-sensitive; worst case is a spurious update notice. No action taken on it (manual download only, D-08) |
|
||||
| T-06-05 | Elevation | autostart capability exposed to WebView | accept | Internal-only app loading a trusted self-hosted server; autostart enable/disable is the only granted surface, default disabled (D-04) |
|
||||
| T-06-06 | Denial of Service | startup version fetch blocks UI | mitigate | Run the version check in a spawned async task; never block window display on the HTTP call |
|
||||
| T-06-10 | Spoofing | unsigned installer artifacts (D-09) | accept | Code signing deferred for internal-only distribution; revisit before external sales. Documented, time-bound risk |
|
||||
| T-06-SC | Tampering | cargo/npm installs | mitigate | All Tauri plugins [Approved] in RESEARCH Package Legitimacy Audit; reqwest is a mainstream crate, pin to 0.12 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `pnpm --filter=@tessera/api type-check` and web type-check pass (no regression)
|
||||
- `apps/desktop/src-tauri` `cargo check` passes with all plugins
|
||||
- `pnpm --filter=@tessera/desktop tauri build` produces an AppImage
|
||||
- Manual smoke test (Task 4) confirms tray, close-to-tray, window-state, version notification
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Close-to-tray, tray menu, window-state, autostart, notification + version check all functioning (D-01/03/04/05/08)
|
||||
- Branded icon present (D-06); unsigned AppImage + NSIS bundle targets configured (D-07, D-09)
|
||||
- API exposes /health/version (supports D-08)
|
||||
- DESK-01 (installable Windows/Linux wrapper) and DESK-02 (connects to web backend) satisfied
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/06-desktop-client-ci-cd/06-02-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,258 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user