|
1 | | -# CI/CD Deployment Setup |
| 1 | +# CI/CD Pipeline |
2 | 2 |
|
3 | | -This document describes the automated deployment setup for OpenLinear. |
4 | | - |
5 | | -## Overview |
6 | | - |
7 | | -The CI/CD pipeline automatically deploys the application to the DigitalOcean droplet whenever changes are pushed to the `main` or `dev` branches. |
8 | | - |
9 | | -## Architecture |
| 3 | +Two GitHub Actions workflows handle all automation. One deploys the API + frontend to the production droplet. The other builds desktop releases and publishes to npm. |
10 | 4 |
|
11 | 5 | ``` |
12 | | -GitHub Push → GitHub Actions Workflow → SSH to Droplet → Deploy Script |
| 6 | +main push ──→ deploy.yml ──→ checks (typecheck, build, test) ──→ SSH deploy ──→ health check |
| 7 | +tag push ──→ release.yml ──→ build desktop (Tauri + Rust) ──→ GitHub Release ──→ npm publish |
13 | 8 | ``` |
14 | 9 |
|
15 | | -## GitHub Secrets |
| 10 | +## Workflows |
16 | 11 |
|
17 | | -The following secrets must be configured in the GitHub repository: |
| 12 | +### `deploy.yml` — Deploy to Production |
18 | 13 |
|
19 | | -| Secret | Value | Description | |
20 | | -|--------|-------|-------------| |
21 | | -| `DEPLOY_HOST` | `206.189.139.212` | IP address of the DigitalOcean droplet | |
22 | | -| `DEPLOY_USER` | `root` | SSH username for the droplet | |
23 | | -| `DEPLOY_SSH_KEY` | Private key content | SSH private key for authentication | |
| 14 | +**Trigger:** Push to `main` (ignores docs, landing, desktop, packaging changes). |
24 | 15 |
|
25 | | -### SSH Key Setup |
| 16 | +**Concurrency:** Cancels in-progress deploys when a new push arrives. |
26 | 17 |
|
27 | | -The SSH key used for deployment is located at `~/.ssh/droplet_key` on the local machine and has been added to the droplet's `~/.ssh/authorized_keys`. |
| 18 | +| Job | What it does | Timeout | |
| 19 | +|-----|-------------|---------| |
| 20 | +| **checks** | Install deps, generate Prisma, push test schema, typecheck API + desktop-ui, build API, run API tests | 10 min | |
| 21 | +| **deploy** | SSH into droplet, run `scripts/deploy.sh` | 10 min | |
| 22 | +| **verify** | POST to `https://rixie.in/health`, retry 6 times | — | |
28 | 23 |
|
29 | | -**Public key fingerprint:** |
30 | | -``` |
31 | | -ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAzxe83rbgC3EG2VEIPvep7yc+9YdQcpw9+3UhLGcTxN opencode-agent |
32 | | -``` |
| 24 | +Path filters skip the workflow entirely for changes that don't affect the deployed services: |
| 25 | +- `docs/**`, `*.md`, `LICENSE` |
| 26 | +- `apps/landing/**` (deployed via Vercel separately) |
| 27 | +- `apps/desktop/**`, `apps/intro-video/**` |
| 28 | +- `packaging/**` |
| 29 | + |
| 30 | +### `release.yml` — Desktop Release + npm Publish |
33 | 31 |
|
34 | | -## Workflow |
| 32 | +**Trigger:** Tag push matching `v*` (e.g. `v0.1.14`). |
35 | 33 |
|
36 | | -The deployment workflow (`.github/workflows/deploy.yml`) consists of three jobs: |
| 34 | +**Concurrency:** Per-tag group, does not cancel (releases must complete). |
37 | 35 |
|
38 | | -### 1. Checks |
39 | | -- Runs tests and type checking |
40 | | -- Builds the API |
41 | | -- Ensures code quality before deployment |
| 36 | +| Job | What it does | Timeout | |
| 37 | +|-----|-------------|---------| |
| 38 | +| **build-release** | Build sidecar binary, build Tauri desktop (AppImage + deb), strip Wayland libs, create GitHub Release | 30 min | |
| 39 | +| **publish-npm** | Build + publish the `openlinear` npm package with provenance | 10 min | |
42 | 40 |
|
43 | | -### 2. Deploy |
44 | | -- SSHs into the droplet |
45 | | -- Runs the deploy script at `/opt/openlinear/scripts/deploy.sh` |
46 | | -- Performs diagnostics after deployment |
47 | | -- Verifies health endpoint |
| 41 | +Caching: |
| 42 | +- **Rust build cache** — `~/.cargo` registry + `apps/desktop/src-tauri/target/`, keyed on `Cargo.lock` hash. Saves ~10 min on repeat builds. |
| 43 | +- **pnpm store** — via `actions/setup-node` `cache: pnpm`. |
48 | 44 |
|
49 | | -### 3. Publish NPM Package |
50 | | -- Builds and publishes the `openlinear` npm package |
51 | | -- Uses trusted publishing with provenance |
| 45 | +## Deploy Script (`scripts/deploy.sh`) |
52 | 46 |
|
53 | | -## Deploy Script Optimizations |
| 47 | +Runs on the droplet via SSH. Implements incremental deploys: |
54 | 48 |
|
55 | | -The deploy script (`scripts/deploy.sh`) includes several optimizations: |
| 49 | +1. **Pull** — `git pull origin main --ff-only` |
| 50 | +2. **Diff detection** — compares `OLD_HEAD` vs `NEW_HEAD` to determine what changed: |
| 51 | + - `apps/api/**` → rebuild API |
| 52 | + - `apps/desktop-ui/**` or `packages/**` → rebuild frontend |
| 53 | + - `packages/db/**` → regenerate Prisma + push schema |
| 54 | +3. **Install** — only runs `pnpm install` if `package.json` or `pnpm-lock.yaml` changed |
| 55 | +4. **Build** — only rebuilds changed components |
| 56 | +5. **Restart** — PM2 `reload` for zero-downtime restart (only for changed services) |
56 | 57 |
|
57 | | -1. **Incremental builds**: Only rebuilds changed components (API, FE, DB) |
58 | | -2. **PNPM cache**: Uses persistent store for faster installs |
59 | | -3. **Zero-downtime reload**: Uses PM2 reload instead of restart |
60 | | -4. **Removed Docker worker build**: Not needed in local-only mode |
61 | | -5. **Timing**: Shows total deploy duration |
| 58 | +On manual trigger (`workflow_dispatch`) or first deploy, everything rebuilds. |
62 | 59 |
|
63 | | -## Manual Deployment |
| 60 | +## Release Process |
64 | 61 |
|
65 | | -If needed, manual deployment can be done via SSH: |
| 62 | +To cut a release: |
66 | 63 |
|
67 | 64 | ```bash |
68 | | -ssh -i ~/.ssh/droplet_key root@206.189.139.212 |
69 | | -cd /opt/openlinear |
70 | | -./scripts/deploy.sh |
| 65 | +# 1. Bump version in these files: |
| 66 | +# - apps/desktop/src-tauri/tauri.conf.json (version field) |
| 67 | +# - packages/openlinear/package.json (version field) |
| 68 | + |
| 69 | +# 2. Commit and tag |
| 70 | +git add -A && git commit -m "release: vX.Y.Z" |
| 71 | +git tag vX.Y.Z |
| 72 | +git push origin main --tags |
71 | 73 | ``` |
72 | 74 |
|
73 | | -## Troubleshooting |
| 75 | +This triggers: |
| 76 | +- `deploy.yml` — deploys the API/frontend commit to production |
| 77 | +- `release.yml` — builds desktop binaries and creates a GitHub Release |
74 | 78 |
|
75 | | -### Deploy Job Fails |
76 | | -1. Check GitHub secrets are correctly set |
77 | | -2. Verify SSH key is in droplet's `authorized_keys` |
78 | | -3. Check droplet is running: `curl https://rixie.in/health` |
79 | | - |
80 | | -### CORS Issues |
81 | | -If desktop app can't connect to API, ensure `tauri://localhost` is in CORS allowed origins: |
82 | | -```typescript |
83 | | -// apps/api/src/app.ts |
84 | | -const allowedOrigins = [ |
85 | | - process.env.CORS_ORIGIN || 'http://localhost:3000', |
86 | | - 'http://tauri.localhost', |
87 | | - 'https://tauri.localhost', |
88 | | - 'tauri://localhost', |
89 | | -]; |
90 | | -``` |
| 79 | +The npm publish step skips automatically if the version is already published. |
| 80 | + |
| 81 | +## Artifacts |
| 82 | + |
| 83 | +Each GitHub Release contains: |
| 84 | +- `openlinear-{version}-x86_64.AppImage` — Linux portable binary |
| 85 | +- `openlinear-{version}-x86_64.deb` — Debian/Ubuntu package |
| 86 | +- `openlinear-api-{version}-x86_64` — Standalone API server binary |
| 87 | + |
| 88 | +## Required GitHub Secrets |
| 89 | + |
| 90 | +| Secret | Description | |
| 91 | +|--------|-------------| |
| 92 | +| `DEPLOY_HOST` | Droplet IP address | |
| 93 | +| `DEPLOY_USER` | SSH username (root) | |
| 94 | +| `DEPLOY_SSH_KEY` | SSH private key for droplet access | |
| 95 | + |
| 96 | +npm publishing uses OIDC Trusted Publishers (no `NPM_TOKEN` needed). |
| 97 | + |
| 98 | +## Architecture Decisions |
| 99 | + |
| 100 | +**Why not deploy on `dev`?** Pushing to `dev` previously triggered production deploys. Removed to prevent accidental production deployments from development work. |
| 101 | + |
| 102 | +**Why `cancel-in-progress: true` on deploy?** If you push 3 commits in quick succession, only the latest one deploys. The earlier in-flight deploys are cancelled since they'd be immediately superseded. |
| 103 | + |
| 104 | +**Why `cancel-in-progress: false` on release?** Releases must complete — cancelling a half-uploaded GitHub Release corrupts it. |
| 105 | + |
| 106 | +**Why path filters?** Changes to docs, the landing page (Vercel), or the desktop app (release-only) don't need a production deploy. Saves CI minutes. |
| 107 | + |
| 108 | +**Why Rust caching?** The Tauri/Rust compilation is the slowest step (~15 min cold, ~3 min cached). Caching `target/` and `~/.cargo` dramatically reduces release build times. |
| 109 | + |
| 110 | +**Why is npm publish only in release.yml?** It was previously duplicated in both workflows. The deploy workflow would attempt to publish on every main push (wasteful, even with the version guard skip). npm publishes belong exclusively with tagged releases. |
| 111 | + |
| 112 | +## Troubleshooting |
91 | 113 |
|
92 | | -### OAuth Redirect Issues |
93 | | -Ensure the API has the desktop OAuth logic: |
94 | | -- Detects `?source=desktop` parameter |
95 | | -- Uses `desktop:` state prefix |
96 | | -- Redirects to `openlinear://callback` for desktop apps |
| 114 | +**Deploy fails with SSH timeout:** |
| 115 | +- Verify secrets are set: Settings → Secrets → Actions |
| 116 | +- Test manually: `ssh -i ~/.ssh/droplet_key root@<DEPLOY_HOST>` |
97 | 117 |
|
98 | | -## Verification |
| 118 | +**Health check fails after deploy:** |
| 119 | +- SSH in and check PM2: `pm2 status && pm2 logs openlinear-api --lines 50` |
| 120 | +- Check port binding: `ss -ltnp | grep 3001` |
99 | 121 |
|
100 | | -After deployment, verify: |
101 | | -- API health: `curl https://rixie.in/health` |
102 | | -- GitHub Actions: Check workflow run status |
103 | | -- PM2 status: `ssh root@206.189.139.212 "pm2 status"` |
| 122 | +**Release build fails on Rust compilation:** |
| 123 | +- Usually a cache corruption issue. Delete the `rust-release-*` cache from the Actions → Caches page and re-run. |
104 | 124 |
|
105 | | -## Recent Changes |
| 125 | +**npm publish fails with 403:** |
| 126 | +- Check that OIDC Trusted Publishers is configured for the `openlinear` package on npmjs.com under Settings → Publishing access. |
106 | 127 |
|
107 | | -- Fixed CORS to allow `tauri://localhost` for desktop app |
108 | | -- Optimized deploy script for faster deployments (~54s) |
109 | | -- Fixed GitHub Actions secrets for automatic deployment |
110 | | -- Added desktop OAuth flow with deep-link callback |
| 128 | +**Two workflows running on same commit:** |
| 129 | +- Expected when you push a tag to `main`. `deploy.yml` deploys the API. `release.yml` builds the desktop. They don't conflict — different concurrency groups. |
0 commit comments