Skip to content

Release pipeline: versioned builds, Docker image, and Proxmox deployment #40

@rado0x54

Description

@rado0x54

Summary

Set up GitHub Actions release pipelines that produce versioned, ready-to-run artifacts for ShellWatch. The goal is a single Docker image for most users, a standalone tarball, and a separate release path for the Go agent client.

Artifacts

1. Docker image (primary distribution)

Registry: GitHub Container Registry (ghcr.io/rado0x54/shellwatch)

Tag strategy:

Trigger Image tags
Push to `develop` `:develop`, `:sha-`
Push to `main` `:stable`, `:sha-`
Push tag `v1.2.3` `:1.2.3`, `:1.2`, `:latest` (retag from SHA, no rebuild)

Architecture: `linux/amd64` only (for now).

Dockerfile — multi-stage build

Three stages to keep the runtime image minimal:

Stage 1 — `deps` (node:24-bookworm)

  • Install build tools: `python3`, `make`, `g++`, `libssl-dev`
  • Copy `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`
  • Run `pnpm install --frozen-lockfile` (compiles native addons: better-sqlite3, ssh2, cbor-extract, cpu-features)

Stage 2 — `build` (same base)

  • Copy source (`src/`, `client/`, `tsconfig.json`)
  • Run `pnpm build` (tsc + SvelteKit static adapter → `dist/`)
  • Run `pnpm prune --prod` to drop devDependencies

Stage 3 — `runtime` (node:24-bookworm-slim)

  • Copy from build: `dist/`, `node_modules/`, `drizzle/`, `package.json`
  • Create non-root user (`shellwatch`)
  • Create volume mount points: `/app/data`, `/app/keys`
  • Expose port 3000
  • Entrypoint: `node dist/index.js`

Both `deps` and `runtime` stages must use the same Debian release (bookworm) for native addon binary compatibility.

Known considerations:

  • ssh2 GitHub fork: `pnpm install` clones `rado0x54/ssh2#feat/webauthn-sk-ecdsa` during build. If this fork is ever made private, the Docker build will need a GitHub token via `--mount=type=secret`.
  • Drizzle migrations: `dist/db/migrate.js` resolves `../../drizzle` relative to itself, so the `drizzle/` directory must be copied to `/app/drizzle/` in the runtime image.
  • Static files: Fastify serves from `/dist/client`, which works with WORKDIR `/app`.

.dockerignore

Exclude: `node_modules`, `dist`, `data`, `keys`, `logs`, `coverage`, `.git`, `.github`, `.md`, `.env`, `config.yaml`, `.husky`, `agent-client`

Volumes:

  • `/app/data` — SQLite database (persisted)
  • `/app/keys` — SSH private keys (mounted from host or secret)
  • `/app/config.yaml` — bind mount

Runtime usage:
```bash
docker run -d
-v ./config.yaml:/app/config.yaml:ro
-v ./data:/app/data
-v ./keys:/app/keys:ro
-p 3000:3000
ghcr.io/rado0x54/shellwatch:latest
```

Users must mount a `config.yaml` — the image does not ship one. The app exits on startup without it (existing behavior).

2. docker-compose.yml

Provided in the repo for quick starts:

```yaml
services:
shellwatch:
image: ghcr.io/rado0x54/shellwatch:latest
ports:
- "3000:3000"
volumes:
- ./data:/app/data
- ./keys:/app/keys
- ./config.yaml:/app/config.yaml:ro
environment:
- HOST=0.0.0.0
restart: unless-stopped
```

3. Standalone tarball

For users who don't want Docker:

  • `shellwatch--linux-x64.tar.gz`
  • Contains: `dist/`, `drizzle/`, `package.json`, `pnpm-lock.yaml`, `config.sample.yaml`
  • User runs: `pnpm install --prod && node dist/index.js`

Release pipelines (GitHub Actions)

CI reuse

Add `workflow_call:` to `.github/workflows/ci.yml`'s `on:` block (one-line, non-breaking addition) so the release workflow can call CI as a prerequisite.

ShellWatch release workflow (`.github/workflows/release.yml`)

Triggers:
```yaml
on:
push:
branches: [develop, main]
tags: ["v*"]
```

Jobs:

  1. `ci` — calls `.github/workflows/ci.yml` (reusable workflow). All lint/typecheck/test jobs must pass.
  2. `docker` — `needs: ci`, two paths:
    • Branch push (develop/main): Full Docker build+push using `docker/build-push-action` with GitHub Actions layer cache (`type=gha`). Tags: `:develop`/`:stable` + `:sha-`.
    • Tag push (v):* No rebuild — retag the existing `:sha-` image as `:X.Y.Z`, `:X.Y`, `:latest` using `crane` (`imjasonh/setup-crane`).
  3. `release` (tag pushes only) — create GitHub Release:
    • Attach standalone tarball
    • SHA256 checksums
    • Auto-generated changelog via `generate_release_notes: true`
    • Commit updated `CHANGELOG.md` back to `main`

Agent release workflow (`.github/workflows/agent-release.yml`)

Triggers:
```yaml
on:
push:
tags: ["agent/v*"]
```

Jobs:

  1. Go cross-compile — build `agent-client/` for linux/darwin × amd64/arm64
  2. GitHub Release — create release with:
    • Attached cross-platform binaries
    • SHA256 checksums
    • Auto-generated changelog via `generate_release_notes: true`
    • Commit updated `agent-client/CHANGELOG.md` back to `main`

Changelog

Two separate changelogs, both auto-generated and committed back by their respective release workflows:

  • `CHANGELOG.md` — ShellWatch app (updated on `v*` tags, committed to `main`)
  • `agent-client/CHANGELOG.md` — Go agent (updated on `agent/v*` tags, committed to `main`)

Uses GitHub's automatically generated release notes.

Versioning

Semver. Tag-driven — no version in package.json (or synced via release workflow).

Deliverables

  • `Dockerfile` (multi-stage: deps → build → runtime, Node 24)
  • `.dockerignore`
  • `.github/workflows/ci.yml` — add `workflow_call` trigger
  • `.github/workflows/release.yml` — build + push on branch, retag on tag, GitHub Release + changelog
  • `.github/workflows/agent-release.yml` — Go cross-compile, GitHub Release + changelog
  • `docker-compose.yml`
  • `config.sample.yaml` — already exists, verify it's suitable for Docker usage
  • Deployment docs in `docs/deployment.md` (Docker and standalone tarball)

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions