Files
upaas/README.md
user 0c5c727e01
All checks were successful
Check / check (pull_request) Successful in 1m50s
feat: add GitHub and GitLab webhook support
Add auto-detection of webhook source (Gitea, GitHub, GitLab) by
examining HTTP headers (X-Gitea-Event, X-GitHub-Event, X-Gitlab-Event).

Parse push webhook payloads from all three platforms into a normalized
PushEvent type for unified processing. Each platform's payload format
is handled by dedicated parser functions with correct field mapping
and commit URL extraction.

The webhook handler now detects the source automatically — existing
Gitea webhooks continue to work unchanged, while GitHub and GitLab
webhooks are parsed with their respective payload formats.

Includes comprehensive tests for source detection, event type
extraction, payload parsing for all three platforms, commit URL
fallback logic, and integration tests via HandleWebhook.
2026-03-17 02:37:29 -07:00

217 lines
6.9 KiB
Markdown

# µPaaS by [@sneak](https://sneak.berlin)
A simple self-hosted PaaS that auto-deploys Docker containers from Git repositories via webhooks from Gitea, GitHub, or GitLab.
## Features
- Single admin user with argon2id password hashing
- Per-app SSH keypairs for read-only deploy keys
- Per-app UUID-based webhook URLs with auto-detection of Gitea, GitHub, and GitLab
- Branch filtering - only deploy on configured branch changes
- Environment variables, labels, and volume mounts per app
- Docker builds via socket access
- Notifications via ntfy and Slack-compatible webhooks
- Simple server-rendered UI with Tailwind CSS
## Non-Goals
- Multi-user support
- Complex CI pipelines
- Multiple container orchestration
- SPA/API-first design
- Support for non-push webhook events (e.g. issues, merge requests)
## Architecture
### Project Structure
```
upaas/
├── cmd/upaasd/ # Application entry point
├── internal/
│ ├── config/ # Configuration via Viper
│ ├── database/ # SQLite database with migrations
│ ├── docker/ # Docker client for builds/deploys
│ ├── globals/ # Build-time variables (version, etc.)
│ ├── handlers/ # HTTP request handlers
│ ├── healthcheck/ # Health status service
│ ├── logger/ # Structured logging (slog)
│ ├── middleware/ # HTTP middleware (auth, logging, CORS)
│ ├── models/ # Active Record style database models
│ ├── server/ # HTTP server and routes
│ ├── service/
│ │ ├── app/ # App management service
│ │ ├── auth/ # Authentication service
│ │ ├── deploy/ # Deployment orchestration
│ │ ├── notify/ # Notifications (ntfy, Slack)
│ │ └── webhook/ # Webhook processing (Gitea, GitHub, GitLab)
│ └── ssh/ # SSH key generation
├── static/ # Embedded CSS/JS assets
└── templates/ # Embedded HTML templates
```
### Dependency Injection
Uses Uber fx for dependency injection. Components are wired in this order:
1. `globals` - Build-time variables
2. `logger` - Structured logging
3. `config` - Configuration loading
4. `database` - SQLite connection + migrations
5. `healthcheck` - Health status
6. `auth` - Authentication service
7. `app` - App management
8. `docker` - Docker client
9. `notify` - Notification service
10. `deploy` - Deployment service
11. `webhook` - Webhook processing
12. `middleware` - HTTP middleware
13. `handlers` - HTTP handlers
14. `server` - HTTP server
### Request Flow
```
HTTP Request
chi Router ──► Middleware Stack ──► Handler
(Logging, Auth, CORS, etc.)
Handler Function
Service Layer (app, auth, deploy, etc.)
Models (Active Record)
Database
```
### Key Patterns
- **Closure-based handlers**: Handlers return `http.HandlerFunc` allowing one-time initialization
- **Active Record models**: Models encapsulate database operations (`Save()`, `Delete()`, `Reload()`)
- **Async deployments**: Webhook triggers deploy via goroutine with `context.WithoutCancel()`
- **Embedded assets**: Templates and static files embedded via `//go:embed`
## Development
### Prerequisites
- Go 1.25+
- golangci-lint
- Docker (for running)
### Commands
```bash
make fmt # Format code
make fmt-check # Check formatting (read-only, fails if unformatted)
make lint # Run comprehensive linting
make test # Run tests with race detection (30s timeout)
make check # Verify everything passes (fmt-check, lint, test)
make build # Build binary
make docker # Build Docker image
make hooks # Install pre-commit hook (runs make check)
```
### Commit Requirements
**All commits must pass `make check` before being committed.**
Before every commit:
1. **Format**: Run `make fmt` to format all code
2. **Lint**: Run `make lint` and fix all errors/warnings
- Do not disable linters or add nolint comments without good reason
- Fix the code, don't hide the problem
3. **Test**: Run `make test` and ensure all tests pass
- Fix failing tests by fixing the code, not by modifying tests to pass
- Add tests for new functionality
4. **Verify**: Run `make check` to confirm everything passes
```bash
# Standard workflow before commit:
make fmt
make lint # Fix any issues
make test # Fix any failures
make check # Final verification
git add .
git commit -m "Your message"
```
The Docker build runs `make check` and will fail if:
- Code is not formatted
- Linting errors exist
- Tests fail
- Code doesn't compile
This ensures the main branch always contains clean, tested, working code.
## Configuration
Environment variables:
| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | HTTP listen port | 8080 |
| `UPAAS_DATA_DIR` | Data directory for SQLite and keys | `./data` (local dev only — use absolute path for Docker) |
| `UPAAS_HOST_DATA_DIR` | Host path for DATA_DIR (when running in container) | *(none — must be set to an absolute path)* |
| `UPAAS_DOCKER_HOST` | Docker socket path | unix:///var/run/docker.sock |
| `DEBUG` | Enable debug logging | false |
| `SENTRY_DSN` | Sentry error reporting DSN | "" |
| `METRICS_USERNAME` | Basic auth for /metrics | "" |
| `METRICS_PASSWORD` | Basic auth for /metrics | "" |
## Running with Docker
```bash
docker run -d \
-p 8080:8080 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /path/on/host/upaas-data:/var/lib/upaas \
-e UPAAS_HOST_DATA_DIR=/path/on/host/upaas-data \
upaas
```
### Docker Compose
```yaml
services:
upaas:
build: .
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${HOST_DATA_DIR}:/var/lib/upaas
environment:
- UPAAS_HOST_DATA_DIR=${HOST_DATA_DIR}
# Optional: uncomment to enable debug logging
# - DEBUG=true
# Optional: Sentry error reporting
# - SENTRY_DSN=https://...
# Optional: Prometheus metrics auth
# - METRICS_USERNAME=prometheus
# - METRICS_PASSWORD=secret
```
**Important**: You **must** set `HOST_DATA_DIR` to an **absolute path** on the host before running
`docker compose up`. This value is bind-mounted into the container and passed as `UPAAS_HOST_DATA_DIR`
so that Docker bind mounts during builds resolve correctly. Relative paths (e.g. `./data`) will break
container builds because the Docker daemon resolves paths relative to the host, not the container.
Example: `HOST_DATA_DIR=/srv/upaas/data docker compose up -d`
Session secrets are automatically generated on first startup and persisted to `$UPAAS_DATA_DIR/session.key`.
## License
WTFPL