diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a6f729d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,287 @@ +name: Release + +on: + # Cut a GitHub Release with desktop installers (Linux / macOS / Windows). + workflow_dispatch: + inputs: + version: + description: "Semver without leading v (e.g. 0.1.0-alpha.1)" + required: true + type: string + default: "0.1.0-alpha.1" + prerelease: + description: "Mark the GitHub Release as a pre-release" + required: true + type: boolean + default: true + push: + tags: + - "v*" + +permissions: + contents: write + +concurrency: + group: release-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + meta: + name: Resolve version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.out.outputs.version }} + tag: ${{ steps.out.outputs.tag }} + prerelease: ${{ steps.out.outputs.prerelease }} + steps: + - id: out + shell: bash + run: | + set -euo pipefail + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + VERSION="${{ inputs.version }}" + VERSION="${VERSION#v}" + PRERELEASE="${{ inputs.prerelease }}" + else + TAG_NAME="${GITHUB_REF_NAME}" + VERSION="${TAG_NAME#v}" + if [[ "$VERSION" == *"-"* ]]; then + PRERELEASE=true + else + PRERELEASE=false + fi + fi + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-].+)?$ ]]; then + echo "Invalid version: $VERSION" >&2 + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" + echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT" + echo "Resolved tag=v$VERSION prerelease=$PRERELEASE" + + test: + name: Test (Ubuntu) + needs: meta + runs-on: ubuntu-latest + 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 + - run: pnpm install --frozen-lockfile + - run: pnpm rebuild better-sqlite3 + - run: pnpm -r typecheck + - run: pnpm test + + desktop: + name: Desktop ${{ matrix.platform }} + needs: [meta, test] + 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: Align package versions to release + shell: bash + run: | + set -euo pipefail + VERSION="${{ needs.meta.outputs.version }}" + python3 - <<'PY' "$VERSION" + import json, pathlib, re, sys + version = sys.argv[1] + roots = [ + pathlib.Path("package.json"), + pathlib.Path("apps/api/package.json"), + pathlib.Path("apps/ui/package.json"), + pathlib.Path("apps/desktop/package.json"), + pathlib.Path("packages/db/package.json"), + pathlib.Path("packages/mcp/package.json"), + pathlib.Path("packages/shared/package.json"), + ] + for path in roots: + data = json.loads(path.read_text()) + data["version"] = version + path.write_text(json.dumps(data, indent=2) + "\n") + conf = pathlib.Path("apps/desktop/src-tauri/tauri.conf.json") + conf_data = json.loads(conf.read_text()) + conf_data["version"] = version + conf.write_text(json.dumps(conf_data, indent=2) + "\n") + cargo = pathlib.Path("apps/desktop/src-tauri/Cargo.toml") + text = cargo.read_text() + text, n = re.subn(r'(?m)^version = "[^"]+"', f'version = "{version}"', text, count=1) + if n != 1: + raise SystemExit("failed to patch Cargo.toml version") + cargo.write_text(text) + print(f"set version {version}") + PY + - 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 + exit 0 + fi + sleep 0.2 + done + echo "sidecar failed to become healthy" >&2 + cat "$DATA/out.log" >&2 || true + exit 1 + - name: Stage installer artifacts + shell: bash + run: | + set -euo pipefail + SRC="apps/desktop/src-tauri/target/${{ matrix.rust_target }}/release/bundle" + DST="${RUNNER_TEMP}/release-assets" + mkdir -p "$DST" + # Prefer distributable installers; skip raw .app directories. + find "$SRC" -type f \( \ + -name '*.deb' -o -name '*.AppImage' -o -name '*.rpm' -o \ + -name '*.dmg' -o -name '*.msi' -o -name '*.exe' \ + \) -print -exec cp -v {} "$DST/" \; + ls -la "$DST" + test -n "$(ls -A "$DST")" + - name: Upload desktop installers + uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.artifact }} + path: ${{ runner.temp }}/release-assets/* + if-no-files-found: error + retention-days: 30 + + publish: + name: Publish GitHub Release + needs: [meta, desktop] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: Download installer artifacts + uses: actions/download-artifact@v7 + with: + path: release-assets + pattern: desktop-* + merge-multiple: true + - name: List assets + run: find release-assets -type f | sort + - name: Ensure release tag exists + if: github.event_name == 'workflow_dispatch' + env: + TAG: ${{ needs.meta.outputs.tag }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "Tag $TAG already exists" + else + git tag -a "$TAG" -m "Release $TAG" + git push origin "$TAG" + fi + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.meta.outputs.tag }} + name: Performance Review Manager ${{ needs.meta.outputs.tag }} + prerelease: ${{ needs.meta.outputs.prerelease == 'true' }} + generate_release_notes: true + body: | + ## Alpha release + + Local workbench for evidence-backed performance reviews and promotion cases. + + ### Desktop installers + + | Platform | Artifact | + |----------|----------| + | Linux x86_64 | `.deb` / `.AppImage` | + | macOS Apple Silicon | `.dmg` | + | Windows x86_64 | `.msi` / NSIS `.exe` | + + ### Notes + + - First-run: create an empty workspace or demo team; lock/encrypt between sessions. + - Demo password is for exploration only — do not store real employee data behind it. + - Signed auto-update is not enabled yet; use **Settings → Check for desktop updates** or download from this release. + + ### Verify + + Compare checksums after download if your browser provides them, then install and unlock a fresh workspace. + files: | + release-assets/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index e00fefe..dc0f838 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 installers run only via Actions → **Desktop Installers** → **Run workflow** (not on every merge; defaults to Windows + Linux). +- `@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 via Actions → **Desktop Installers** → **Run workflow** (not on every merge; defaults to Windows + Linux). Formal GitHub Releases (with installers attached) use Actions → **Release** → **Run workflow**. ### 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/README.md b/README.md index cab952e..4806331 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,12 @@ pnpm desktop:dev # native window (needs WebView + Rust) pnpm desktop:build ``` -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). +Desktop installers: + +- **Artifacts only:** Actions → **Desktop Installers** → **Run workflow** (defaults to Windows + Linux; uploads `desktop-windows-x86_64` / `desktop-linux-x86_64`, optional macOS) +- **GitHub Release:** Actions → **Release** → **Run workflow** with version `0.1.0-alpha.1` (or push tag `v0.1.0-alpha.1`) + +Merges to `main` only run unit tests + typecheck. Details: [`apps/desktop/README.md`](./apps/desktop/README.md). ### Cursor MCP (bulk load) diff --git a/apps/api/package.json b/apps/api/package.json index cbbdb5b..b09face 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@prm/api", - "version": "0.1.0", + "version": "0.1.0-alpha.1", "private": true, "type": "module", "scripts": { diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 567c713..609114a 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -48,6 +48,8 @@ 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. +### Desktop Installers (artifacts only) + 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 | @@ -56,14 +58,22 @@ CI (`ci.yml`) runs unit tests + typecheck only. Desktop installers are built on | `desktop-macos-aarch64` | `.dmg` / `.app` under `bundle/` | | `desktop-windows-x86_64` | `.msi` / NSIS `.exe` under `bundle/` | -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-linux-x86_64, desktop-macos-aarch64 ``` +### GitHub Releases (alpha / tagged) + +Use the **Release** workflow (Actions → **Release** → **Run workflow**) to build installers and publish a GitHub Release: + +- Input `version`: semver without `v` (default `0.1.0-alpha.1`) +- Input `prerelease`: leave on for alphas +- Or push a tag `v*` (hyphenated versions such as `v0.1.0-alpha.1` are treated as pre-releases) + +Assets attached to the release are the platform installers (`.deb` / `.AppImage`, `.dmg`, `.msi` / `.exe`). + ## Icons Source mark: `apps/desktop/app-icon.png` (1024²). Regenerate platform icons with: diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 89f8ce5..74c5633 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@prm/desktop", - "version": "0.1.0", + "version": "0.1.0-alpha.1", "private": true, "type": "module", "scripts": { diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 7d8d4da..65fbd94 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prm-desktop" -version = "0.1.0" +version = "0.1.0-alpha.1" description = "Performance Review Manager desktop shell" authors = ["Damon van der Linde"] edition = "2021" diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 6060884..bebf9e2 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Performance Review Manager", - "version": "0.1.0", + "version": "0.1.0-alpha.1", "identifier": "com.prm.workbench", "build": { "beforeDevCommand": "pnpm prepare:dev", diff --git a/apps/ui/package.json b/apps/ui/package.json index eabe5c2..cd6383f 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -1,6 +1,6 @@ { "name": "@prm/ui", - "version": "0.1.0", + "version": "0.1.0-alpha.1", "private": true, "type": "module", "scripts": { diff --git a/apps/ui/src/pages/SettingsPage.tsx b/apps/ui/src/pages/SettingsPage.tsx index 75eb8ad..64392d8 100644 --- a/apps/ui/src/pages/SettingsPage.tsx +++ b/apps/ui/src/pages/SettingsPage.tsx @@ -611,7 +611,7 @@ export function SettingsPage({ className="btn secondary" onClick={async () => { try { - let localVersion = "0.1.0"; + let localVersion = "0.1.0-alpha.1"; const invoke = (window as unknown as { __TAURI__?: { core?: { invoke?: (cmd: string) => Promise } }; }).__TAURI__?.core?.invoke; diff --git a/docs/DELIVERY.md b/docs/DELIVERY.md index 3585144..1f03163 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 **Desktop Installers** `workflow_dispatch` +- CI: unit tests on every `main` PR; desktop installers on manual **Desktop Installers** `workflow_dispatch`; **Release** workflow publishes GitHub Release assets **Still open (do not claim done)** - OS keychain for API keys diff --git a/package.json b/package.json index ab86b23..9f8ff82 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "performance-review-manager", "private": true, - "version": "0.1.0", + "version": "0.1.0-alpha.1", "license": "MIT", "description": "Local solo EM workbench for evidence-backed performance reviews", "scripts": { diff --git a/packages/db/package.json b/packages/db/package.json index 1cb1be3..53d4f12 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@prm/db", - "version": "0.1.0", + "version": "0.1.0-alpha.1", "private": true, "type": "module", "main": "./src/index.ts", diff --git a/packages/mcp/package.json b/packages/mcp/package.json index e6ef2d1..d5fdcae 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@prm/mcp", - "version": "0.1.0", + "version": "0.1.0-alpha.1", "private": true, "type": "module", "bin": { diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 108b228..0f0b959 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -17,7 +17,7 @@ async function main() { const client = new PrmClient(); const server = new McpServer({ name: "prm", - version: "0.1.0", + version: "0.1.0-alpha.1", }); registerPrmTools(server, client); diff --git a/packages/shared/package.json b/packages/shared/package.json index 89648bd..dbf8650 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@prm/shared", - "version": "0.1.0", + "version": "0.1.0-alpha.1", "private": true, "type": "module", "main": "./src/index.ts",