Skip to content

4ug-aug/sweat-review

Repository files navigation

SWEAT Review

PR preview environments for Docker Compose projects, on your own server.

If you have a project that runs with docker compose up and you want a unique preview URL for each pull request — without adopting a PaaS, configuring webhooks, or giving a SaaS vendor access to your code — this is what SWEAT Review does. One thing, nothing else.

It polls the GitHub API for open PRs, spins up an isolated Docker Compose stack per PR on your VPS, routes traffic via Traefik subdomains, and posts the preview URL as a GitHub comment. Optionally gated by a label so only PRs you choose get deployed.

How it works

graph TD
    A["SWEAT Review<br/><i>Polls GitHub API every 30s</i>"] --> B["Clone repo & render<br/>ephemeral Compose override"]
    A --> C["Post preview URL<br/>comment on PR"]
    B --> D["docker compose<br/>up -d --build"]
    D --> T["Traefik<br/><i>Shared reverse proxy on port 80<br/>Routes by Host header</i>"]
    T --> PR1["pr-1"]
    T --> PR2["pr-2"]
    T --> PR3["pr-3"]
    T --> PR4["..."]

    style A fill:#2563eb,stroke:#1d4ed8,color:#fff
    style T fill:#7c3aed,stroke:#6d28d9,color:#fff
    style PR1 fill:#059669,stroke:#047857,color:#fff
    style PR2 fill:#059669,stroke:#047857,color:#fff
    style PR3 fill:#059669,stroke:#047857,color:#fff
    style PR4 fill:#059669,stroke:#047857,color:#fff
Loading

Each PR gets its own fully isolated stack (frontend, backend, nginx, database, workers — whatever your docker-compose.yml defines). Traffic is routed via subdomain:

http://pr{N}.{VPS_IP}.nip.io

nip.io provides wildcard DNS that maps any subdomain containing an IP back to that IP — no domain registration or DNS config needed.

The agent polls the GitHub API for open PRs every 30 seconds (configurable) and:

  1. New PR detected — clones the branch, renders a Compose override with Traefik labels, runs docker compose up, posts the preview URL on the PR
  2. PR updated (new commits pushed) — pulls latest, rebuilds changed services
  3. PR closed — tears down the stack, removes the clone, updates the PR comment

No webhook configuration needed — the agent works behind NAT, firewalls, or locally on your machine.

Prerequisites

  • Docker and Docker Compose installed
  • Python 3.12+ (or just use uv / uvx)
  • A GitHub personal access token (see Token setup below)

Quick start

1. Initialize

uvx sweat-review init

This prompts for your GitHub token, repo, and VPS IP, then writes .env, creates the Traefik reverse proxy config, sets up the Docker network, and starts Traefik — all in one step.

2. Start

uvx sweat-review start

That's it. Open a PR on your target repo and wait up to 30 seconds — you'll see a comment with the preview URL.

Configuration reference

The init command writes a .env file with the essentials. You can edit it to tune additional settings:

Variable Description Default
GITHUB_TOKEN GitHub PAT for PR comments and repo cloning (required)
GITHUB_REPO Target repository as owner/repo (required)
VPS_IP IP address for preview URLs 127.0.0.1
POLL_INTERVAL Seconds between GitHub API polls 30
CLONE_BASE_DIR Directory where PR repos are cloned <data_dir>/repos
MAX_CONCURRENT Maximum simultaneous preview environments 15
STALE_TIMEOUT_HOURS Hours before a deployment is considered stale 48
DB_PATH Path to the SQLite state database <data_dir>/sweat-review.db
COMPOSE_FILE Name of the Compose file in the target repo docker-compose.yml
TRIGGER_LABEL Only deploy PRs with this label (blank = all PRs) (none)
TARGET_ENV_FILE Path to an env file to copy into each preview environment (optional)
TEMPLATE_PATH Path to the Jinja2 override template templates/docker-compose.override.yml.j2

Environment variables for the target project

If your target project needs a .env file (e.g. database credentials, API keys), you can't commit it to the repo. Instead, store it on the machine running the agent and point to it:

# In your .env:
TARGET_ENV_FILE=/path/to/my-project.env

The agent copies this file as .env into each PR's clone directory before running docker compose up. It's refreshed on every update too, so changes to the file take effect on the next PR push.

If TARGET_ENV_FILE is not set, the agent skips this step.

GitHub token setup

The agent needs a GitHub token to poll for PRs, clone repos, and post comments. You can use either a fine-grained personal access token (recommended) or a classic token.

Fine-grained token (recommended) — go to Settings > Developer settings > Personal access tokens > Fine-grained tokens:

Permission Access Used for
Contents Read Cloning the PR branch
Pull requests Read Polling for open PRs and checking PR state
Issues Write Posting and updating preview URL comments (GitHub serves PR comments via the Issues API)

Set Repository access to "Only select repositories" and pick your target repo.

Classic token — go to Settings > Developer settings > Personal access tokens > Tokens (classic):

Select the repo scope (grants full access to private repos — less granular than fine-grained tokens).

Validate your Compose file

Before opening a PR, you can check your docker-compose.yml for preview-compatibility issues:

uvx sweat-review check

Use -f to point at a different file, or --format json for machine-readable output.

API endpoints

Method Path Description
GET /health Health check with disk info
GET /status List all tracked deployments
GET /status/{pr_number} Get a single deployment's status

Testing

Run the test suite

uv sync --all-groups
uv run pytest -v

97 tests covering state store, compose rendering, compose checker, orchestrator, poller, cleanup, resource checks, and integration.

Manual test with the sample app

A sample multi-service app is in sample-app/ for testing Traefik routing without needing a real repo:

# 1. Make sure Traefik is running (sweat-review init starts it)

# 2. Render an override for "PR 1"
uv run python -c "
from sweat_review.compose import ComposeRenderer
from pathlib import Path
r = ComposeRenderer(Path('templates/docker-compose.override.yml.j2'))
r.write_override(Path('sample-app'), pr_number=1, vps_ip='127.0.0.1')
"

# 3. Start the stack
docker compose -p pr-1 \
  -f sample-app/docker-compose.yml \
  -f sample-app/docker-compose.override.yml \
  up -d --build

# 4. Test it
curl -H "Host: pr1.127.0.0.1.nip.io" http://localhost/api/health
# Expected: {"status":"ok","service":"backend"}

# 5. Tear down
docker compose -p pr-1 down -v --remove-orphans

Target repo requirements

Your project needs a docker-compose.yml with an nginx service as the entry point. If your entry point service has a different name, edit templates/docker-compose.override.yml.j2 and replace nginx with your service name.

Project structure

├── pyproject.toml                          # Package config + CLI entry point
├── .env.example                            # Environment variable template
├── src/sweat_review/
│   ├── main.py                             # FastAPI app + CLI entry point
│   ├── config.py                           # Settings loaded from env vars
│   ├── poller.py                           # Polls GitHub API for PR changes
│   ├── orchestrator.py                     # Deploy / update / teardown logic
│   ├── compose.py                          # Jinja2 override template rendering
│   ├── github_client.py                    # GitHub API client (PRs + comments)
│   ├── state.py                            # SQLite deployment state tracking
│   ├── resources.py                        # Disk/memory resource checks
│   └── cleanup.py                          # Stale + orphan cleanup scheduler
│   └── compose_check/                      # Compose file linter (check command)
├── templates/
│   └── docker-compose.override.yml.j2      # Per-PR Compose override with Traefik labels
├── traefik/
│   └── docker-compose.yml                  # Shared Traefik reverse proxy
├── sample-app/                             # Minimal multi-service app for testing
│   ├── docker-compose.yml
│   ├── backend/                            # Flask API
│   ├── frontend/                           # Static HTML
│   └── nginx/                              # Reverse proxy
└── tests/                                  # 97 tests

About

Ephemeral Pull Request review service that spins up a new compose instance from PRs.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors