diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55affc0..72b03e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,8 +5,6 @@ on: branches: [main] pull_request: branches: [main] - # Desktop installers are expensive — run them on demand from the Actions tab. - workflow_dispatch: concurrency: group: ci-${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} @@ -29,103 +27,3 @@ jobs: - run: pnpm rebuild better-sqlite3 - run: pnpm -r typecheck - run: pnpm test - - desktop: - name: Desktop ${{ matrix.platform }} - needs: test - if: github.event_name == 'workflow_dispatch' - strategy: - fail-fast: false - matrix: - include: - - platform: ubuntu-22.04 - args: --target x86_64-unknown-linux-gnu - rust_target: x86_64-unknown-linux-gnu - artifact: desktop-linux-x86_64 - - platform: macos-latest - args: --target aarch64-apple-darwin - rust_target: aarch64-apple-darwin - artifact: desktop-macos-aarch64 - - platform: windows-latest - args: --target x86_64-pc-windows-msvc - rust_target: x86_64-pc-windows-msvc - artifact: desktop-windows-x86_64 - runs-on: ${{ matrix.platform }} - steps: - - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v6 - with: - version: 10.33.3 - - uses: actions/setup-node@v7 - with: - node-version: 22 - cache: pnpm - - name: Install Linux WebKit deps - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.rust_target }} - - uses: Swatinem/rust-cache@v2 - with: - workspaces: apps/desktop/src-tauri -> target - - run: pnpm install --frozen-lockfile - - run: pnpm rebuild better-sqlite3 - - name: Bundle API sidecar + Node - run: node apps/desktop/scripts/bundle-api-sidecar.mjs --node - env: - PRM_BUNDLE_NODE: "1" - - uses: tauri-apps/tauri-action@v0 - id: tauri - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - projectPath: apps/desktop - args: ${{ matrix.args }} - includeUpdaterJson: false - - name: Smoke API sidecar - shell: bash - run: | - set -euo pipefail - SIDE="apps/desktop/src-tauri/sidecars" - UI="apps/desktop/src-tauri/ui-dist" - if [[ "${{ runner.os }}" == "Windows" ]]; then - NODE="$SIDE/node.exe" - else - NODE="$SIDE/node" - chmod +x "$NODE" || true - fi - test -f "$NODE" - test -f "$SIDE/prm-api.mjs" - test -f "$UI/index.html" - DATA="${RUNNER_TEMP:-/tmp}/prm-sidecar-smoke" - rm -rf "$DATA" - mkdir -p "$DATA" - PRM_DATA_DIR="$DATA" PRM_PORT=8799 PRM_LAUNCH_TOKEN=ci-smoke PRM_UI_DIR="$UI" \ - "$NODE" "$SIDE/prm-api.mjs" >"$DATA/out.log" 2>&1 & - PID=$! - cleanup() { kill "$PID" 2>/dev/null || true; } - trap cleanup EXIT - for i in $(seq 1 50); do - if curl -sf "http://127.0.0.1:8799/api/health" >/dev/null; then - echo "sidecar healthy" - curl -sf "http://127.0.0.1:8799/api/workspace/status" -H "x-prm-launch-token: ci-smoke" - echo - curl -sf "http://127.0.0.1:8799/" | head -c 200 - echo - exit 0 - fi - sleep 0.2 - done - echo "sidecar failed to become healthy" >&2 - cat "$DATA/out.log" >&2 || true - exit 1 - - name: Upload desktop installers - uses: actions/upload-artifact@v7 - with: - name: ${{ matrix.artifact }} - path: apps/desktop/src-tauri/target/${{ matrix.rust_target }}/release/bundle/ - if-no-files-found: error - retention-days: 30 diff --git a/.github/workflows/desktop-installers.yml b/.github/workflows/desktop-installers.yml new file mode 100644 index 0000000..b60a2ae --- /dev/null +++ b/.github/workflows/desktop-installers.yml @@ -0,0 +1,168 @@ +name: Desktop Installers + +# Manual-only Tauri installer builds. Default matrix is Windows + Linux; +# choose "all" (or macOS alone) from the Actions UI when needed. +on: + workflow_dispatch: + inputs: + platforms: + description: Platforms to build + type: choice + options: + - windows-and-linux + - windows + - linux + - macos + - all + default: windows-and-linux + +concurrency: + group: desktop-installers-${{ github.ref }}-${{ inputs.platforms }} + cancel-in-progress: true + +jobs: + select-matrix: + name: Select platforms + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set.outputs.matrix }} + steps: + - id: set + shell: bash + run: | + set -euo pipefail + case "${{ inputs.platforms }}" in + windows-and-linux) + keys='["linux","windows"]' + ;; + windows) + keys='["windows"]' + ;; + linux) + keys='["linux"]' + ;; + macos) + keys='["macos"]' + ;; + all) + keys='["linux","macos","windows"]' + ;; + *) + echo "Unknown platforms input: ${{ inputs.platforms }}" >&2 + exit 1 + ;; + esac + + # Full matrix definitions; jq filters to the selected OS keys. + all='[ + { + "os_key": "linux", + "platform": "ubuntu-22.04", + "args": "--target x86_64-unknown-linux-gnu", + "rust_target": "x86_64-unknown-linux-gnu", + "artifact": "desktop-linux-x86_64" + }, + { + "os_key": "macos", + "platform": "macos-latest", + "args": "--target aarch64-apple-darwin", + "rust_target": "aarch64-apple-darwin", + "artifact": "desktop-macos-aarch64" + }, + { + "os_key": "windows", + "platform": "windows-latest", + "args": "--target x86_64-pc-windows-msvc", + "rust_target": "x86_64-pc-windows-msvc", + "artifact": "desktop-windows-x86_64" + } + ]' + matrix="$(jq -c --argjson keys "$keys" '{ include: [.[] | select(.os_key as $k | $keys | index($k) != null)] }' <<<"$all")" + echo "matrix=$matrix" >> "$GITHUB_OUTPUT" + echo "Selected matrix: $matrix" + + desktop: + name: Desktop ${{ matrix.platform }} + needs: select-matrix + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.select-matrix.outputs.matrix) }} + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + with: + version: 10.33.3 + - uses: actions/setup-node@v7 + with: + node-version: 22 + cache: pnpm + - name: Install Linux WebKit deps + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rust_target }} + - uses: Swatinem/rust-cache@v2 + with: + workspaces: apps/desktop/src-tauri -> target + - run: pnpm install --frozen-lockfile + - run: pnpm rebuild better-sqlite3 + - name: Bundle API sidecar + Node + run: node apps/desktop/scripts/bundle-api-sidecar.mjs --node + env: + PRM_BUNDLE_NODE: "1" + - uses: tauri-apps/tauri-action@v0 + id: tauri + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + projectPath: apps/desktop + args: ${{ matrix.args }} + includeUpdaterJson: false + - name: Smoke API sidecar + shell: bash + run: | + set -euo pipefail + SIDE="apps/desktop/src-tauri/sidecars" + UI="apps/desktop/src-tauri/ui-dist" + if [[ "${{ runner.os }}" == "Windows" ]]; then + NODE="$SIDE/node.exe" + else + NODE="$SIDE/node" + chmod +x "$NODE" || true + fi + test -f "$NODE" + test -f "$SIDE/prm-api.mjs" + test -f "$UI/index.html" + DATA="${RUNNER_TEMP:-/tmp}/prm-sidecar-smoke" + rm -rf "$DATA" + mkdir -p "$DATA" + PRM_DATA_DIR="$DATA" PRM_PORT=8799 PRM_LAUNCH_TOKEN=ci-smoke PRM_UI_DIR="$UI" \ + "$NODE" "$SIDE/prm-api.mjs" >"$DATA/out.log" 2>&1 & + PID=$! + cleanup() { kill "$PID" 2>/dev/null || true; } + trap cleanup EXIT + for i in $(seq 1 50); do + if curl -sf "http://127.0.0.1:8799/api/health" >/dev/null; then + echo "sidecar healthy" + curl -sf "http://127.0.0.1:8799/api/workspace/status" -H "x-prm-launch-token: ci-smoke" + echo + curl -sf "http://127.0.0.1:8799/" | head -c 200 + echo + exit 0 + fi + sleep 0.2 + done + echo "sidecar failed to become healthy" >&2 + cat "$DATA/out.log" >&2 || true + exit 1 + - name: Upload desktop installers + uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.artifact }} + path: apps/desktop/src-tauri/target/${{ matrix.rust_target }}/release/bundle/ + if-no-files-found: error + retention-days: 30 diff --git a/AGENTS.md b/AGENTS.md index 6c16946..e00fefe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ Performance Review Manager — a local, single-user ("solo EM") pnpm monorepo. S - `@prm/api` (`apps/api`) — Hono + better-sqlite3 backend on `http://127.0.0.1:8787`. Run with `pnpm dev:api` (uses `tsx watch`, no strict typecheck). - `@prm/ui` (`apps/ui`) — React + Vite frontend on `http://localhost:5173`, proxies `/api` → the API. Run with `pnpm dev:ui`. - `pnpm dev` runs both in parallel — this is the primary dev entrypoint. -- `@prm/desktop` (`apps/desktop`) — Tauri shell. `desktop:dev`/`desktop:build` need Rust + system WebView libraries and are not validated in the headless cloud VM. Packaged desktop CI installers run only via Actions **Run workflow** (not on every merge). +- `@prm/desktop` (`apps/desktop`) — Tauri shell. `desktop:dev`/`desktop:build` need Rust + system WebView libraries and are not validated in the headless cloud VM. Packaged desktop installers run only via Actions → **Desktop Installers** → **Run workflow** (not on every merge; defaults to Windows + Linux). ### Package manager (important, non-obvious) - This repo requires **pnpm 10.33.3**. The VM's default corepack resolves pnpm 11, which (a) rejects this committed lockfile via a `minimumReleaseAge` supply-chain policy and (b) ignores the `pnpm.onlyBuiltDependencies` field in `package.json` that is needed to build the `better-sqlite3` native module. The update script pins 10.33.3 via `corepack prepare pnpm@10.33.3 --activate`; keep using pnpm 10.x. If native `better-sqlite3` is missing, run `pnpm rebuild better-sqlite3`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7906ce2..a31d167 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ Production `pnpm build` may still have packaging rough edges; typecheck + tests ## Desktop -Desktop installers are **not** built on every merge. Use Actions → **CI** → **Run workflow**. See `apps/desktop/README.md`. +Desktop installers are **not** built on every merge. Use Actions → **Desktop Installers** → **Run workflow** (defaults to Windows + Linux). See `apps/desktop/README.md`. ## Security diff --git a/README.md b/README.md index 8489c0a..cab952e 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ pnpm desktop:dev # native window (needs WebView + Rust) pnpm desktop:build ``` -Desktop installers are built on demand: Actions → **CI** → **Run workflow** (uploads `desktop-windows-x86_64`, `desktop-macos-aarch64`, `desktop-linux-x86_64`). Merges to `main` only run unit tests + typecheck. Details: [`apps/desktop/README.md`](./apps/desktop/README.md). +Desktop installers are built on demand: Actions → **Desktop Installers** → **Run workflow** (defaults to Windows + Linux; uploads `desktop-windows-x86_64` / `desktop-linux-x86_64`, optional macOS). Merges to `main` only run unit tests + typecheck. Details: [`apps/desktop/README.md`](./apps/desktop/README.md). ### Cursor MCP (bulk load) @@ -244,7 +244,7 @@ Regenerate with `pnpm screenshots` (UI + API running; demo password `workbench`) - **Sprint pulse** for date-window coaching - **Eight accents × dark/light** appearance system (Coral, Amber, Magenta, Citrus plus Teal, Ocean, Forest, Slate) - **AI**: digests, section draft, peer synthesize, chat, with citation allowlist -- **Desktop** packaging (Tauri); installers via Actions **Run workflow** +- **Desktop** packaging (Tauri); installers via Actions → **Desktop Installers** → **Run workflow** - **Delete / recreate workspace** from Unlock or Settings ## Docs diff --git a/SECURITY.md b/SECURITY.md index f838edd..d1bd277 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,7 +27,7 @@ What PRM does **not** claim today: - Automatic refusal of Dropbox / iCloud / OneDrive data directories (warn when detected; sync folders remain risky) - Cryptographic anonymity for peer feedback - Tamper-proof legal custody of the activity log (hash-chained and HMAC-exportable, but the EM can re-sign after unlock) -- Signed auto-update for desktop installers (manual CI artifacts; verify checksums yourself) +- Signed auto-update for desktop installers (manual **Desktop Installers** artifacts; verify checksums yourself) Details: [`docs/architecture/TRUST_MODEL.md`](./docs/architecture/TRUST_MODEL.md). diff --git a/apps/desktop/README.md b/apps/desktop/README.md index cf91939..567c713 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -48,7 +48,7 @@ Settings → **Check for updates** compares the packaged version (`get_app_versi Packaged data lives in the OS app-data directory (for example `%APPDATA%\com.prm.workbench` on Windows). Sidecar stdout/stderr is appended to `desktop-api.log` there for debugging startup failures. -CI builds the desktop matrix (Ubuntu / macOS / Windows) only when you **manually run** the **CI** workflow from the Actions tab (`workflow_dispatch`). Merges and PRs still run unit tests only. Each green desktop job uploads an Actions artifact (retained 30 days): +CI (`ci.yml`) runs unit tests + typecheck only. Desktop installers are built on demand via the **Desktop Installers** workflow (`workflow_dispatch`). Default platform choice is **windows-and-linux**; the Actions UI also offers Windows-only, Linux-only, macOS, or all three. Each green job uploads an Actions artifact (retained 30 days): | Artifact name | Typical contents | |---------------|------------------| @@ -59,8 +59,9 @@ CI builds the desktop matrix (Ubuntu / macOS / Windows) only when you **manually Download from the run’s **Artifacts** section in the GitHub UI, or: ```bash +gh workflow run "Desktop Installers" -f platforms=windows-and-linux gh run download -n desktop-windows-x86_64 -# also: desktop-macos-aarch64, desktop-linux-x86_64 +# also: desktop-linux-x86_64, desktop-macos-aarch64 ``` ## Icons diff --git a/docs/CORE_LOOP_EVAL.md b/docs/CORE_LOOP_EVAL.md index 8702066..7feb19e 100644 --- a/docs/CORE_LOOP_EVAL.md +++ b/docs/CORE_LOOP_EVAL.md @@ -39,7 +39,7 @@ upload role R&R → tag people → year-round dossier | Promotion packets | Done | Next-role marks + AI case draft | | Backfill + MCP bulk load | Done | Thin = ach+fb+docs+goals < 2; MCP needs explicit unlock / `PRM_PASSWORD` | | Settings → AI | Done | Private notes / meeting / document extract egress **enforced** on cloud paths | -| Desktop CSP / launch token / API sidecar | Done | CSP set; shell IPC removed; packaged Node + `prm-api.mjs` spawned by Tauri; desktop installers via Actions **Run workflow** (not every merge) | +| Desktop CSP / launch token / API sidecar | Done | CSP set; shell IPC removed; packaged Node + `prm-api.mjs` spawned by Tauri; desktop installers via Actions → **Desktop Installers** → **Run workflow** (not every merge) | ## Remaining gaps (honest) diff --git a/docs/DELIVERY.md b/docs/DELIVERY.md index bef3fc6..3585144 100644 --- a/docs/DELIVERY.md +++ b/docs/DELIVERY.md @@ -18,7 +18,7 @@ Aligned to [TRUST_MODEL.md](./architecture/TRUST_MODEL.md). - ActivityEvent hash chain + HMAC export - Encrypted workspace backup/restore - Synthetic **8-person** demo seed (rolling relative cycle dates) -- CI: unit tests on every `main` PR; desktop installers on manual `workflow_dispatch` +- CI: unit tests on every `main` PR; desktop installers on manual **Desktop Installers** `workflow_dispatch` **Still open (do not claim done)** - OS keychain for API keys diff --git a/docs/architecture/LOCAL_STACK.md b/docs/architecture/LOCAL_STACK.md index 3d031e0..6823072 100644 --- a/docs/architecture/LOCAL_STACK.md +++ b/docs/architecture/LOCAL_STACK.md @@ -10,7 +10,7 @@ Primary target: installable app on **Linux, Windows, macOS**. No Docker/Postgres | Layer | Choice | Notes | |-------|--------|-------| -| Shell | **Tauri 2** | dmg / msi|NSIS / AppImage|deb (manual CI `workflow_dispatch`) | +| Shell | **Tauri 2** | dmg / msi|NSIS / AppImage|deb (manual **Desktop Installers** `workflow_dispatch`) | | UI | **React + TypeScript + Vite** | | | Backend (shipped) | **Node Hono sidecar** on `127.0.0.1` + session token; desktop adds **per-launch capability token** | Prefer migrating privileged ops to Tauri IPC over time | | Backend (target) | Tauri IPC commands for DB/FS/HTTP without a long-lived TCP sidecar | Aspirational | diff --git a/docs/architecture/TRUST_MODEL.md b/docs/architecture/TRUST_MODEL.md index 414239f..2c53186 100644 --- a/docs/architecture/TRUST_MODEL.md +++ b/docs/architecture/TRUST_MODEL.md @@ -67,7 +67,7 @@ No anonymity theater: imported peer text is attributed to the named peer for the ### Not shipped yet (do not market as done) - OS keychain custody for API keys (keys ride the workspace secret today) - SQLCipher / Rusqlite-in-Rust as the primary DB path (Node `better-sqlite3` + file seal is what runs) -- Signed auto-update / notarized installers (desktop artifacts are manual CI builds) +- Signed auto-update / notarized installers (desktop artifacts are manual **Desktop Installers** builds) - Background jobs when the app is closed See [LOCAL_STACK.md](./LOCAL_STACK.md) and [SYSTEM.md](./SYSTEM.md).