Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .bun-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.3.14
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ jobs:
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v1
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version-file: .bun-version

- name: Install dependencies
run: bun install
Expand All @@ -35,9 +35,9 @@ jobs:
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v1
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version-file: .bun-version

- name: Install dependencies
run: bun install
Expand All @@ -54,9 +54,9 @@ jobs:
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v1
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version-file: .bun-version

- name: Install dependencies
run: bun install
Expand Down
64 changes: 64 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Deploy

on:
workflow_run:
workflows: [CI]
types: [completed]
workflow_dispatch:

concurrency:
group: deploy-production
cancel-in-progress: false

env:
DEPLOY_PATH: ${{ vars.DEPLOY_PATH || '/var/www/sparkhub' }}

jobs:
deploy:
name: Deploy to VPS
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
if: >
github.event_name == 'workflow_dispatch' ||
(
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'master'
)
steps:
- name: Checkout code at deployed revision
uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: .bun-version

- name: Build
run: |
bun install --frozen-lockfile
bun run build

- name: Prune to production dependencies
run: |
rm -rf node_modules
bun install --production --frozen-lockfile

- name: Deploy to VPS
env:
SSHPASS: ${{ secrets.SSH_PASSWORD }}
SSH_PORT: ${{ secrets.SSH_PORT || '22' }}
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq sshpass rsync
SSH_OPTS="-p ${SSH_PORT} -o StrictHostKeyChecking=accept-new"
TARGET="${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}"

export RSYNC_RSH="ssh ${SSH_OPTS}"
sshpass -e rsync -avz --delete-delay --delay-updates \
--exclude-from=deploy.exclude \
./ "${TARGET}:${DEPLOY_PATH}/"
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ bun run dev:full
## API Documentation

Swagger documentation is available at `http://localhost:3000/swagger` when the server is running.

## Deployment

Pushes to `master` run CI, then build and rsync to the VPS; the server reloads via Bun `--watch`.

See [docs/deploy.md](docs/deploy.md) for one-time server setup and GitHub secrets.
7 changes: 7 additions & 0 deletions deploy.exclude
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.git/
.github/
test/
docs/
*.sqlite
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SQLite WAL files unprotected from rsync deletion

Medium Severity

deploy.exclude only excludes *.sqlite, but rsync with --delete-delay also removes destination files that aren't present in the source and aren't covered by an exclude rule. SQLite's WAL mode produces *.sqlite-wal and *.sqlite-shm companion files alongside the main database. Because those patterns are missing from deploy.exclude, every rsync deploy will delete them from the VPS if they exist, which can corrupt the live database mid-operation.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0bf9dfa. Configure here.

src/config.ts
src/config-server.ts
142 changes: 142 additions & 0 deletions docs/deploy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Production deployment

SparkHub deploys automatically when CI passes on a push to `master`.

```text
push master → CI → GitHub Actions (install + build) → rsync to VPS → --watch reloads
```

The app is **built on GitHub Actions** (`bun install` + `bun run build`), then **rsynced in bulk** to the VPS (including `node_modules`). Server config files on the VPS are never overwritten (see `deploy.exclude`). The server runs **`bun run start:watch`** and reloads when rsync updates files.

## One-time VPS setup

Git is not required on the server for deploys.

### 1. Deploy user and directory

```bash
sudo mkdir -p /var/www/sparkhub
sudo useradd --system --home-dir /var/www/sparkhub --shell /bin/bash deploy
sudo chown -R deploy:deploy /var/www/sparkhub
```

### 2. Install Bun (as the deploy user)

```bash
sudo -iu deploy bash -lc 'curl -fsSL https://bun.sh/install | bash'
```

Bun will install to `/var/www/sparkhub/.bun/bin/bun`. Verify:

```bash
sudo -iu deploy bash -lc 'which bun && bun --version'
```

### 3. Production configuration

Create config on the server before the first deploy (these paths are excluded from rsync):

```bash
cd /var/www/sparkhub
mkdir -p src
# copy or create src/config.ts and src/config-server.ts
```

- `src/config.ts` — public domain
- `src/config-server.ts` — SQLite path, wallet seed, TLS cert paths

Ensure TLS certificate files exist at the paths referenced in `config-server.ts`.

Binding to port 443 usually requires:

```bash
sudo setcap 'cap_net_bind_service=+ep' /var/www/sparkhub/.bun/bin/bun
```

### 4. Keep the server running (pick one)

**Option A — PM2**

Run all of this as the `deploy` user (e.g. `sudo -iu deploy`):

```bash
cd /var/www/sparkhub
pm2 start /var/www/sparkhub/.bun/bin/bun --name sparkhub -- run start:watch
pm2 save
pm2 startup # run the command it prints (as root)
```

**Option B — systemd**

```bash
sudo tee /etc/systemd/system/sparkhub.service <<'EOF'
[Unit]
Description=SparkHub
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/sparkhub
Environment=HOME=/var/www/sparkhub
ExecStart=/var/www/sparkhub/.bun/bin/bun run start:watch
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now sparkhub
```

Adjust the `bun` path if needed (`sudo -iu deploy which bun`).

### 5. First deploy

Merge this repo’s CD setup to `master`, add GitHub secrets (below), and let the **Deploy** workflow run. It will rsync the built app to the server.

If the app directory is empty, create `src/config.ts` and `src/config-server.ts` first, then run **Actions → Deploy → Run workflow**.

### 6. SSH credentials for GitHub Actions

| Secret | Description |
|--------|-------------|
| `SSH_HOST` | VPS hostname or IP |
| `SSH_USER` | `deploy` |
| `SSH_PASSWORD` | Deploy user password |
| `SSH_PORT` | Optional; default `22` |

Optional repository **variables**:

| Variable | Default |
|----------|---------|
| `DEPLOY_PATH` | `/var/www/sparkhub` |

### 7. Branch protection (recommended)

On `master`, require the **CI** workflow to pass before merge.

## What gets synced

`deploy.exclude` keeps server-only files safe:

- `src/config.ts`, `src/config-server.ts`
- `.git/`, tests, docs, local SQLite files

## Day-to-day operations

- **Deploy**: push to `master` (after CI passes) or **Actions → Deploy → Run workflow**.
- **Logs**: `pm2 logs sparkhub` or `journalctl -u sparkhub -f`
- **Rollback**: in **Actions → Deploy → Run workflow**, pick the branch or tag at the revision you want to ship. The workflow checks out the dispatched ref. For a fast revert, `git revert` and push to `master`.

## Troubleshooting

| Symptom | Check |
|---------|--------|
| Deploy workflow skipped | CI must pass on a **push** to `master`. |
| Config overwritten | Paths must stay listed in `deploy.exclude`. |
| Server did not reload | Process must use `bun run start:watch`. |
| `bun: command not found` | Bun not on deploy user `PATH`. |
| Permission denied on 443 | `setcap` on `bun` or reverse proxy on 443. |
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"build": "bun build src/client/app.tsx --outdir dist --target browser --production",
"build:watch": "bun build src/client/app.tsx --outdir dist --target browser --watch",
"start": "bun run build && PORT=443 bun run src/index.ts",
"start:watch": "NODE_ENV=production PORT=443 bun run --watch src/index.ts",
"dev:full": "concurrently \"bun run dev\" \"bun run build:watch\"",
"tslint": "tsc --noEmit",
"check": "biome check src/",
Expand Down
Loading