Skip to content

Commit 566ed64

Browse files
committed
ci: optimize CI/CD pipeline for production
- deploy.yml: remove dev trigger, eliminate duplicate npm publish job, add path filters to skip doc/landing/desktop changes, enable cancel-in-progress for faster iteration - release.yml: add Rust build cache (saves ~10min), per-tag concurrency, upgrade Node to 22, use frozen-lockfile - build-sidecar.sh: fix hardcoded Prisma@5.22.0 path with dynamic version-agnostic resolution - docs/CICD.md: complete rewrite with workflow details, deploy script internals, release process, and troubleshooting guide
1 parent 3cb6aa6 commit 566ed64

File tree

4 files changed

+158
-121
lines changed

4 files changed

+158
-121
lines changed

.github/workflows/deploy.yml

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,31 @@ name: Deploy to Production
22

33
on:
44
push:
5-
branches: [main, dev]
5+
branches: [main]
6+
paths-ignore:
7+
- 'docs/**'
8+
- '*.md'
9+
- 'LICENSE'
10+
- 'packaging/**'
11+
- 'apps/landing/**'
12+
- 'apps/intro-video/**'
13+
- 'apps/desktop/**'
14+
- '.gitignore'
15+
- '.opencodeignore'
616
workflow_dispatch:
717

818
concurrency:
919
group: deploy-production
10-
cancel-in-progress: false
20+
cancel-in-progress: true
1121

1222
jobs:
1323
checks:
1424
name: Checks
1525
runs-on: ubuntu-latest
26+
timeout-minutes: 10
1627
services:
1728
postgres:
18-
image: postgres:16
29+
image: postgres:16-alpine
1930
env:
2031
POSTGRES_USER: openlinear
2132
POSTGRES_PASSWORD: openlinear
@@ -29,6 +40,7 @@ jobs:
2940
--health-retries 5
3041
env:
3142
DATABASE_URL: postgresql://openlinear:openlinear@localhost:5432/openlinear_test
43+
TURBO_TELEMETRY_DISABLED: 1
3244
steps:
3345
- uses: actions/checkout@v4
3446
with:
@@ -49,20 +61,22 @@ jobs:
4961
- name: Push schema to test database
5062
run: pnpm --filter @openlinear/db db:push
5163

52-
- name: Typecheck
53-
run: pnpm --filter @openlinear/api typecheck
64+
- name: Typecheck (API + Desktop UI)
65+
run: pnpm --filter @openlinear/api typecheck && pnpm --filter @openlinear/desktop-ui lint
5466

5567
- name: Build API
5668
run: pnpm --filter @openlinear/api build
5769

5870
- name: Test
5971
run: pnpm --filter @openlinear/api test
72+
timeout-minutes: 5
6073

6174
deploy:
6275
name: Deploy
6376
needs: checks
6477
runs-on: ubuntu-latest
6578
environment: production
79+
timeout-minutes: 10
6680
steps:
6781
- name: Deploy to droplet
6882
uses: appleboy/ssh-action@v1
@@ -101,39 +115,11 @@ jobs:
101115
for i in 1 2 3 4 5 6; do
102116
HTTP_STATUS=$(curl -s -o /dev/null -w '%{http_code}' https://rixie.in/health)
103117
if [ "$HTTP_STATUS" = "200" ]; then
104-
echo "Deployment verified — health check returned $HTTP_STATUS"
118+
echo "Deployment verified (HTTP $HTTP_STATUS)"
105119
exit 0
106120
fi
107121
echo "Attempt $i: got $HTTP_STATUS, retrying in 5s..."
108122
sleep 5
109123
done
110-
echo "Health check failed after 6 attempts"
124+
echo "Health check failed after 6 attempts"
111125
exit 1
112-
113-
publish-npm:
114-
name: Publish NPM Package
115-
needs: checks
116-
runs-on: ubuntu-latest
117-
permissions:
118-
id-token: write
119-
contents: read
120-
steps:
121-
- uses: actions/checkout@v4
122-
- uses: pnpm/action-setup@v4
123-
- uses: actions/setup-node@v4
124-
with:
125-
node-version: 22
126-
cache: pnpm
127-
- run: pnpm install --frozen-lockfile
128-
- name: Upgrade npm for OIDC Trusted Publishers support
129-
run: npm install -g npm@latest
130-
- name: Publish @openlinear/core (openlinear)
131-
run: |
132-
cd packages/openlinear
133-
LOCAL_VERSION=$(node -p "require('./package.json').version")
134-
REMOTE_VERSION=$(npm view openlinear version 2>/dev/null || echo "0.0.0")
135-
if [ "$LOCAL_VERSION" = "$REMOTE_VERSION" ]; then
136-
echo "Version $LOCAL_VERSION already published, skipping"
137-
exit 0
138-
fi
139-
npm publish --access public --provenance

.github/workflows/release.yml

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: release
1+
name: Release
22

33
on:
44
push:
@@ -9,9 +9,16 @@ permissions:
99
contents: write
1010
packages: write
1111

12+
concurrency:
13+
group: release-${{ github.ref_name }}
14+
cancel-in-progress: false
15+
1216
jobs:
1317
build-release:
1418
runs-on: ubuntu-22.04
19+
timeout-minutes: 30
20+
env:
21+
TURBO_TELEMETRY_DISABLED: 1
1522
steps:
1623
- name: Checkout
1724
uses: actions/checkout@v4
@@ -22,12 +29,24 @@ jobs:
2229
- name: Setup Node
2330
uses: actions/setup-node@v4
2431
with:
25-
node-version: 20
32+
node-version: 22
2633
cache: pnpm
2734

2835
- name: Setup Rust
2936
uses: dtolnay/rust-toolchain@stable
3037

38+
- name: Cache Rust build artifacts
39+
uses: actions/cache@v4
40+
with:
41+
path: |
42+
~/.cargo/registry/index/
43+
~/.cargo/registry/cache/
44+
~/.cargo/git/db/
45+
apps/desktop/src-tauri/target/
46+
key: rust-release-${{ runner.os }}-${{ hashFiles('apps/desktop/src-tauri/Cargo.lock') }}
47+
restore-keys: |
48+
rust-release-${{ runner.os }}-
49+
3150
- name: Install Linux dependencies
3251
run: |
3352
sudo apt-get update
@@ -40,7 +59,7 @@ jobs:
4059
libfuse2
4160
4261
- name: Install dependencies
43-
run: pnpm install
62+
run: pnpm install --frozen-lockfile
4463

4564
- name: Generate Prisma client
4665
run: pnpm --filter @openlinear/db db:generate
@@ -80,12 +99,14 @@ jobs:
8099
- name: Create GitHub release
81100
uses: softprops/action-gh-release@v2
82101
with:
102+
generate_release_notes: true
83103
files: dist/release/*
84104

85105
publish-npm:
86106
name: Publish NPM Package
87107
needs: build-release
88108
runs-on: ubuntu-latest
109+
timeout-minutes: 10
89110
permissions:
90111
id-token: write
91112
contents: read

docs/CICD.md

Lines changed: 99 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,129 @@
1-
# CI/CD Deployment Setup
1+
# CI/CD Pipeline
22

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.
104

115
```
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
138
```
149

15-
## GitHub Secrets
10+
## Workflows
1611

17-
The following secrets must be configured in the GitHub repository:
12+
### `deploy.yml` — Deploy to Production
1813

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).
2415

25-
### SSH Key Setup
16+
**Concurrency:** Cancels in-progress deploys when a new push arrives.
2617

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 ||
2823

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
3331

34-
## Workflow
32+
**Trigger:** Tag push matching `v*` (e.g. `v0.1.14`).
3533

36-
The deployment workflow (`.github/workflows/deploy.yml`) consists of three jobs:
34+
**Concurrency:** Per-tag group, does not cancel (releases must complete).
3735

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 |
4240

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`.
4844

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`)
5246

53-
## Deploy Script Optimizations
47+
Runs on the droplet via SSH. Implements incremental deploys:
5448

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)
5657

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.
6259

63-
## Manual Deployment
60+
## Release Process
6461

65-
If needed, manual deployment can be done via SSH:
62+
To cut a release:
6663

6764
```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
7173
```
7274

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
7478

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
91113

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>`
97117

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`
99121

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.
104124

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.
106127

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

Comments
 (0)