# Development & Release Workflow

How code changes move from development to production deployments across all installation methods.

---

## Overview

There are **three ways** Changemaker Lite gets deployed:

| Method | Who uses it | Images from | Compose file |
|--------|------------|-------------|--------------|
| **Source install** | Developers, contributors | Built locally from source | `docker-compose.yml` |
| **Release install** | Production servers, evaluators | Gitea registry (pre-built) | `docker-compose.prod.yml` (ships as `docker-compose.yml` in tarball) |
| **CCP provisioned** | Fleet operators (Control Panel) | Gitea registry (pre-built) | Rendered from `templates/docker-compose.yml.hbs` |

All three methods share the same Gitea container registry at `gitea.bnkops.com/admin`.

---

## The Pipeline

```
 ┌──────────────────────────────────────────────────────────────────┐
 │  DEVELOPMENT (your machine)                                     │
 │                                                                  │
 │  Edit code → docker compose up -d → test locally                │
 │  Uses: docker-compose.yml (build: blocks + ./api:/app mounts)   │
 └──────────────────┬───────────────────────────────────────────────┘
                    │ git push
                    ▼
 ┌──────────────────────────────────────────────────────────────────┐
 │  BUILD & PUBLISH                                                │
 │                                                                  │
 │  Step 1: ./scripts/build-and-push.sh                            │
 │          Builds 5 production images, pushes to Gitea registry   │
 │          (api, admin, media-api, nginx, ccp-agent)              │
 │          tagged :SHA + :latest                                  │
 │                                                                  │
 │  Step 2: ./scripts/mirror-images.sh          (run once/rarely)  │
 │          Mirrors 36 third-party images to Gitea registry        │
 │          (postgres, redis, nocodb, jitsi, grafana, etc.)        │
 │                                                                  │
 │  Step 3: ./scripts/build-release.sh --tag vX.Y.Z --upload      │
 │          Packages runtime files into ~9MB tarball, uploads to   │
 │          Gitea Releases                                         │
 └──────────────────┬─────────────────100.90.78.47──────────────────────────────┘
                    │
        ┌───────────┴───────────┐
        ▼                       ▼
 ┌─────────────────┐   ┌──────────────────┐
 │ RELEASE INSTALL  │   │ CCP PROVISIONED  │
 │                  │   │                  │
 │ curl installer   │   │ Control Panel    │
 │ or manual tarball│   │ creates instance │
 │ → config.sh      │   │ via web UI       │
 │ → docker compose │   │ → renders config │
 │   up -d          │   │ → docker compose │
 │                  │   │   up -d          │
 └─────────────────┘   └──────────────────┘
        │                       │
        └───────────┬───────────┘
                    ▼
         All images pulled from
         gitea.bnkops.com/admin
         (zero external dependencies)
```

---

## Step-by-Step

### 1. Local Development

Standard Docker Compose workflow with hot-reload:

```bash
# Start core services
docker compose up -d v2-postgres redis api admin

# API logs (watch for errors)
docker compose logs -f api

# Run with media API
docker compose up -d media-api

# Run with monitoring stack
docker compose --profile monitoring up -d
```

**Key:** `docker-compose.yml` uses `build:` blocks to compile TypeScript from source and mounts `./api:/app` for live code changes. This is the only compose file that builds from source.

### 2. Build & Push Production Images

After code changes are tested locally:

```bash
# Build production images and push to Gitea registry
./scripts/build-and-push.sh
```

This builds **5 services** with multi-stage Dockerfiles (production target, no dev dependencies), tags each image with `:SHA` and `:latest`, and pushes to `gitea.bnkops.com/admin/changemaker-{service}`:

| Service | Dockerfile | What it produces |
|---------|-----------|-----------------|
| `api` | `api/Dockerfile` | Express + Prisma (compiled JS, no TS) |
| `admin` | `admin/Dockerfile` | Nginx serving React build output |
| `media-api` | `api/Dockerfile.media` | Fastify + FFmpeg (compiled JS) |
| `nginx` | `nginx/Dockerfile` | Nginx with `envsubst` domain templating |
| `ccp-agent` | `../changemaker-control-panel/agent/Dockerfile` | Remote management agent (sibling repo) |

```bash
# Build specific services only
./scripts/build-and-push.sh --services api,admin

# Build without pushing (verify first)
./scripts/build-and-push.sh --no-push

# Include code-server (~9GB, only when Dockerfile changes)
./scripts/build-and-push.sh --include-code-server
```

### 3. Mirror Third-Party Images (Run Once / On Version Bumps)

Copies all third-party Docker images used by the platform to the Gitea registry, so deployments never depend on Docker Hub, GHCR, LSCR, or GCR:

```bash
# Mirror all 36 images (core + platform + comms + monitoring)
./scripts/mirror-images.sh

# Mirror only essential infrastructure (postgres, redis, alpine)
./scripts/mirror-images.sh --core-only

# Preview without executing
./scripts/mirror-images.sh --dry-run
```

**When to re-run:** Only when upgrading a third-party image version. The script has explicit version pins — update the version in `mirror-images.sh`, then re-run.

Images are organized into 4 groups:

| Group | Count | Examples |
|-------|-------|---------|
| Core Infrastructure | 5 | postgres:16-alpine, redis:7-alpine, alpine:3 |
| Platform Services | 16 | nocodb, listmonk, gitea, n8n, vaultwarden, nginx, code-server |
| Communication | 8 | rocket.chat, mongo, nats, gancio, jitsi (4 containers) |
| Monitoring | 7 | prometheus, grafana, alertmanager, cadvisor, exporters, gotify |

### 4. Build Release Tarball

Packages only runtime files (~9 MB) — no source code, no node_modules:

```bash
# Build tarball
./scripts/build-release.sh --tag v2.2.0

# Build and upload to Gitea Releases
./scripts/build-release.sh --tag v2.2.0 --upload

# Preview contents without creating tarball
./scripts/build-release.sh --dry-run

# --upload refuses to overwrite an existing tag. To deliberately replace
# a release (destructive — users on that tag see no upgrade signal):
./scripts/build-release.sh --tag v2.2.0 --upload --replace
```

**Version hygiene:** bump the tag when changing release contents. Overwriting
an existing release silently breaks upgrade checks for users already on that
version — they see "no update available" even though the tarball they'd
download differs.

The tarball contains:
- `docker-compose.yml` (copy of `docker-compose.prod.yml` — image-only, no build blocks)
- `.env.example`, `config.sh` (configuration wizard)
- `scripts/` (init scripts, backup, upgrade, systemd units)
- `configs/` (prometheus, grafana, alertmanager, homepage, pangolin)
- `nginx/conf.d/` (templates for reference)
- `mkdocs/` (starter documentation)
- Empty data directories

### 5. Deploying

#### New Release Install (End Users)

```bash
# One-liner
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash

# Or manual
curl -LO https://gitea.bnkops.com/admin/changemaker.lite/releases/latest/download/changemaker-lite-latest.tar.gz
tar xzf changemaker-lite-latest.tar.gz
cd changemaker-lite
bash config.sh
docker compose up -d
```

All images (custom + third-party) pull from `gitea.bnkops.com/admin`. No external registry access needed.

#### New CCP Instance (Fleet Operators)

The Control Panel provisions instances via its web UI:

1. Operator fills in the Create Instance wizard (domain, features, email, tunnel)
2. CCP copies source files, renders templates (Handlebars), generates secrets
3. With `USE_REGISTRY_IMAGES=true` (default): pulls pre-built images from Gitea (~2 min)
4. With `USE_REGISTRY_IMAGES=false`: builds from source (~10+ min)
5. Starts infrastructure → runs migrations → starts all services

CCP registry settings (in `changemaker-control-panel/.env`):
```bash
GITEA_REGISTRY=gitea.bnkops.com/admin    # Registry URL for all images
USE_REGISTRY_IMAGES=true                   # true = pull pre-built, false = build from source
IMAGE_TAG=latest                           # Tag for custom images (api, admin, media-api)
```

### 6. Upgrading Existing Installations

#### Source Installs

```bash
./scripts/upgrade.sh                    # Standard: git pull + rebuild from source
./scripts/upgrade.sh --use-registry     # Fast: pull pre-built images instead of rebuilding
./scripts/upgrade.sh --dry-run          # Preview changes
```

#### Release Installs

```bash
./scripts/upgrade.sh                    # Auto-detects release mode, downloads latest tarball
```

Release installs are detected by the presence of a `VERSION` file and absence of `.git/`. The upgrade script automatically downloads the latest tarball from Gitea instead of running `git pull`.

---

## Image Naming Conventions

All images live under `gitea.bnkops.com/admin/`:

| Type | Naming Pattern | Example |
|------|---------------|---------|
| Custom services | `changemaker-{service}:{sha\|latest}` | `changemaker-api:latest` |
| Simple names | Same as upstream | `postgres:16-alpine`, `redis:7-alpine` |
| Namespaced → short | Org removed | `nocodb/nocodb` → `nocodb:0.301.3` |
| Conflict resolution | Explicit short name | `gotify/server` → `gotify`, `vaultwarden/server` → `vaultwarden` |
| Jitsi suite | `jitsi-{component}` | `jitsi-web:stable-9823`, `jitsi-prosody:stable-9823` |
| LinuxServer nginx | `ls-nginx` (avoids nginx conflict) | `ls-nginx:1.28.2` |

---

## Two Compose Files

| File | Purpose | Build? | Source mounts? | Image source |
|------|---------|--------|---------------|-------------|
| `docker-compose.yml` | Development | Yes (`build:` blocks) | Yes (`./api:/app`) | Built locally |
| `docker-compose.prod.yml` | Production | No | No | `${GITEA_REGISTRY:-gitea.bnkops.com/admin}/...` |

Release tarballs ship `docker-compose.prod.yml` renamed as `docker-compose.yml`.

The CCP template (`templates/docker-compose.yml.hbs`) generates a compose file that works like `docker-compose.prod.yml` when `USE_REGISTRY_IMAGES=true`, or like `docker-compose.yml` when `false`.

---

## Quick Reference

```bash
# ── Development ──
docker compose up -d v2-postgres redis api admin     # Start dev stack
docker compose logs -f api                            # Watch API logs
docker compose exec api npx prisma migrate dev        # Create migration

# ── Build & Publish ──
./scripts/build-and-push.sh                           # Build + push 5 images
./scripts/mirror-images.sh                            # Mirror 36 third-party images
git tag --sort=-v:refname | head -3                   # Check latest version tags
./scripts/build-release.sh --tag vX.Y.Z --upload      # Package + upload release

# ── Deploy ──
curl -fsSL .../install.sh | bash                      # New install (release)
./scripts/upgrade.sh                                  # Upgrade existing install
./scripts/upgrade.sh --use-registry                   # Fast upgrade (registry images)

# ── Verify ──
curl -s http://localhost:4000/api/health              # API health check
docker compose ps                                     # Container status
```

---

## Gitea API Tokens

There are **two separate Gitea tokens** with different purposes. Using the wrong one is a common mistake:

| Variable | Target | Used by | Create at |
|----------|--------|---------|-----------|
| `GITEA_REGISTRY_API_TOKEN` | Remote registry (`gitea.bnkops.com`) | `build-release.sh --upload`, release API calls | `https://gitea.bnkops.com/user/settings/applications` |
| `GITEA_API_TOKEN` | Local Gitea instance | Docs comments, user provisioning, SSO | `http://localhost:3030/user/settings/applications` |

**Key:** Release uploads and the Gitea Releases API require `GITEA_REGISTRY_API_TOKEN`. If you get `"user does not exist"` from the API, you're using the wrong token.

---

## Checklist: Cutting a New Release

1. [ ] All code changes committed and pushed to `main` branch
2. [ ] `docker compose up -d` works locally (smoke test)
3. [ ] **Determine version tag:**
   ```bash
   # Check the latest existing tag to pick the next version
   git tag --sort=-v:refname | head -5
   # Check commits since the last tag
   git log $(git tag --sort=-v:refname | head -1)..HEAD --oneline
   ```
4. [ ] `./scripts/build-and-push.sh` — builds and pushes 5 production images
5. [ ] `./scripts/mirror-images.sh` — only if third-party versions changed
6. [ ] `./scripts/build-release.sh --tag vX.Y.Z --upload` — packages and uploads tarball
7. [ ] **Add release notes** (via Gitea web UI or API):
   ```bash
   # Update release body via API (use GITEA_REGISTRY_API_TOKEN, not GITEA_API_TOKEN)
   GITEA_TOKEN=$(grep -oP 'GITEA_REGISTRY_API_TOKEN=\K.*' .env)
   # Find release ID
   curl -s "https://gitea.bnkops.com/api/v1/repos/admin/changemaker.lite/releases?limit=1" \
     -H "Authorization: token $GITEA_TOKEN" | python3 -c "import sys,json; r=json.load(sys.stdin)[0]; print(f'ID: {r[\"id\"]}, Tag: {r[\"tag_name\"]}')"
   # Update with release notes (write JSON body to /tmp/release-notes.json first)
   curl -s -X PATCH "https://gitea.bnkops.com/api/v1/repos/admin/changemaker.lite/releases/RELEASE_ID" \
     -H "Authorization: token $GITEA_TOKEN" \
     -H "Content-Type: application/json" \
     -d @/tmp/release-notes.json
   ```
8. [ ] Test clean install: `tar xzf ... && cd changemaker-lite && bash config.sh && docker compose up -d`
9. [ ] Test upgrade: `./scripts/upgrade.sh` on an existing installation
10. [ ] Verify: `curl http://localhost:4000/api/health` returns `{"status":"ok"}`
