Skip to content
Merged
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
23 changes: 23 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Keep the build context small and deterministic.
.git
.github
**/node_modules
frontend/.next
frontend/out
frontend/.turbo
**/__pycache__
*.pyc
.venv
venv
data
*.db
.env
.env.*
!.env.example
test-results
playwright-report
tests/e2e
spec
harness
*.md
!README.md
44 changes: 22 additions & 22 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
# Workshop Helmsman — environment template
#
# Copy this file to `.env` and fill in values. Nothing is strictly required
# for local dev (SQLite is the default), but the OpenRouter key enables AI assist.

# --- Server ---
# BIND_HOST=0.0.0.0
# BIND_PORT=8001
# Workshop Helmsman v0.2 — copy to .env and fill in. `.env` is gitignored; never commit it.

# --- Database ---
# Local dev (default): sqlite:///./data/helmsman.db
# Production (PostgreSQL): postgresql+psycopg://user:pass@host:5432/dbname
DATABASE_URL=sqlite:///./data/helmsman.db
# SQLAlchemy URL. Default is SQLite in data/. PostgreSQL-ready: postgresql+psycopg2://...
DATABASE_URL=sqlite:///data/helmsman.db

# --- Workshop defaults ---
# DEFAULT_WORKSHOP_TTL_HOURS=8
# --- Server ---
PORT=8001

# --- LLM (optional) ---
# OpenRouter API key — enables facilitator help-reply suggestions.
# Get one at https://openrouter.ai/keys
# If absent or empty, AI assist is silently disabled (no errors, no fallback).
OPENROUTER_API_KEY=
# --- Deploy (docker-compose + Caddy TLS; see deploy/RUNBOOK.md) ---
# Public hostname served over HTTPS. DNS A record must point at the VM.
HELMSMAN_DOMAIN=workshops.smalltech.in
# Email Let's Encrypt uses for cert expiry notices.
ACME_EMAIL=contact@smalltech.in
# Absolute base URL for generated share links (set when running behind a proxy).
# Leave empty to derive from the request.
HELMSMAN_BASE_URL=
HELMSMAN_LOG_LEVEL=INFO

# OpenRouter primary model (default: nvidia/llama-3.3-nemotron-super-49b-v1)
# OPENROUTER_PRIMARY_MODEL=nvidia/llama-3.3-nemotron-super-49b-v1
# --- Facilitator access (REQUIRED) ---
# The instance access key entered on the Admin Home page. Choose any strong string (>=16 chars).
HELMSMAN_ADMIN_KEY=

# OpenRouter fallback model (default: openai/gpt-4o-mini)
# OPENROUTER_FALLBACK_MODEL=openai/gpt-4o-mini
# --- AI help-desk (Phase 4; optional) ---
# Absent => the product runs fully air-gapped with zero errors and AI surfaces show a labelled off state.
OPENROUTER_API_KEY=
HELMSMAN_AI_MODEL=anthropic/claude-sonnet-4-6
HELMSMAN_AI_CONFIDENCE=0.75
87 changes: 87 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: CI

# Runs on every PR and on pushes to the integration branches.
# Backend unit+integration tests and the frontend production build always run.
# Playwright e2e runs against a live single-origin server (built frontend served
# by FastAPI) — the exact shape the app ships in.

on:
pull_request:
push:
branches: [feature/rebuild-v0.2, main]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
- run: uv sync --frozen
- name: Migrations apply cleanly
run: uv run alembic upgrade head
- name: Unit + integration tests
run: uv run pytest tests/unit tests/integration -q

frontend-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 11
- uses: actions/setup-node@v4
with:
node-version-file: frontend/.nvmrc
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
working-directory: frontend
- run: pnpm build
working-directory: frontend

e2e:
runs-on: ubuntu-latest
needs: [backend, frontend-build]
env:
HELMSMAN_ADMIN_KEY: ci-admin-key-0123456789
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
- run: uv sync --frozen
- uses: pnpm/action-setup@v4
with:
version: 11
- uses: actions/setup-node@v4
with:
node-version-file: frontend/.nvmrc
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
working-directory: frontend
- name: Build frontend (served by the API at /app)
run: pnpm build
working-directory: frontend
- name: Install Playwright browser
run: pnpm exec playwright install --with-deps chromium
- name: Migrate + boot server
run: |
uv run alembic upgrade head
uv run python -m src &
for i in $(seq 1 20); do curl -fsS http://localhost:8001/api/health && break || sleep 1; done
- name: Playwright e2e
run: BASE_URL=http://localhost:8001 pnpm exec playwright test --reporter=line
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report
91 changes: 91 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Deploy

# Build the production image, push it to GitHub Container Registry, then deploy
# on the GCP VM over SSH by pulling the new image and re-upping compose.
#
# Trigger: push to main (i.e. a merged PR), or manual dispatch.
#
# Required repository secrets (Settings -> Secrets and variables -> Actions):
# GHCR_PAT optional; GITHUB_TOKEN is used by default for GHCR
# DEPLOY_SSH_KEY private key whose public half is in the VM's authorized_keys
# DEPLOY_HOST VM public IP or hostname
# DEPLOY_USER SSH user on the VM (e.g. deploy)
# The VM holds its own /opt/helmsman/.env (secrets never leave the VM).

on:
push:
branches: [main]
workflow_dispatch:

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

env:
IMAGE: ghcr.io/${{ github.repository }}

jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image_ref: ${{ steps.meta.outputs.image_ref }}
steps:
- uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
run: echo "image_ref=${IMAGE,,}:${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v3
- name: Build + push image
uses: docker/build-push-action@v6
with:
context: .
file: deploy/Dockerfile
push: true
tags: |
${{ steps.meta.outputs.image_ref }}
${{ env.IMAGE }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max

deploy:
runs-on: ubuntu-latest
needs: build-and-push
steps:
- uses: actions/checkout@v4
- name: Copy compose assets to the VM
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
source: "deploy/docker-compose.prod.yml,deploy/Caddyfile"
target: "/opt/helmsman"
strip_components: 1
- name: Pull + roll the new image
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
set -euo pipefail
cd /opt/helmsman
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
export HELMSMAN_IMAGE="${{ needs.build-and-push.outputs.image_ref }}"
docker compose -f docker-compose.prod.yml pull app
docker compose -f docker-compose.prod.yml up -d
docker image prune -f
# Wait for health before declaring success.
for i in $(seq 1 20); do
curl -fsS http://localhost:8001/api/health && exit 0 || sleep 3
done
echo "health check failed after deploy" >&2
exit 1
33 changes: 27 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
# secrets
.env

# python
__pycache__/
*.pyc
*.pyo
.venv/
venv/
.pytest_cache/

# runtime data (SQLite lives here)
data/*
!data/.gitkeep
*.db
*.db-journal
*.db-wal
*.db-shm
*.log
*.pyc
*.pyo

# node / frontend
node_modules/
frontend/.next/
frontend/out/

# playwright
test-results/
playwright-report/

# misc
.DS_Store
.env
/tmp/
__pycache__/
data/
venv/
15 changes: 0 additions & 15 deletions Dockerfile

This file was deleted.

Loading
Loading