## Summary
Adds GitHub and GitLab push webhook support alongside the existing Gitea support.
closes#68
## What Changed
### Auto-detection of webhook source
The webhook handler now auto-detects which platform sent the webhook by examining HTTP headers:
- **Gitea**: `X-Gitea-Event`
- **GitHub**: `X-GitHub-Event`
- **GitLab**: `X-Gitlab-Event`
Existing Gitea webhooks continue to work unchanged. Unknown sources fall back to Gitea format for backward compatibility.
### Normalized push event
All three payload formats are parsed into a unified `PushEvent` struct containing:
- Source platform, ref, branch, commit SHA
- Repository name, clone URL, HTML URL
- Commit URL (with per-platform fallback logic)
- Pusher username/name
### New files
- **`internal/service/webhook/payloads.go`**: Source-specific payload structs (`GiteaPushPayload`, `GitHubPushPayload`, `GitLabPushPayload`), `ParsePushPayload()` dispatcher, per-platform parsers, branch extraction, and commit URL extraction functions.
### Modified files
- **`internal/service/webhook/types.go`**: Added `Source` type (gitea/github/gitlab/unknown), `DetectWebhookSource()`, `DetectEventType()`, and `PushEvent` normalized type. Moved `GiteaPushPayload` to payloads.go.
- **`internal/service/webhook/webhook.go`**: `HandleWebhook` now accepts a `Source` parameter and uses `ParsePushPayload()` for unified parsing instead of directly unmarshaling Gitea payloads.
- **`internal/handlers/webhook.go`**: Calls `DetectWebhookSource()` and `DetectEventType()` to auto-detect the platform before delegating to the webhook service.
- **`internal/service/webhook/webhook_test.go`**: Comprehensive tests for source detection, event type extraction, payload parsing (all 3 platforms), commit URL fallback paths, and integration tests through `HandleWebhook` for GitHub and GitLab sources.
- **`README.md`**: Updated description, features, non-goals, and architecture to reflect multi-platform webhook support.
## Test coverage
Webhook package: **96.9%** statement coverage. Tests cover:
- `DetectWebhookSource` with all header combinations and precedence
- `DetectEventType` for each platform
- `ParsePushPayload` for Gitea, GitHub, GitLab, unknown source, invalid JSON, empty payloads
- Commit URL extraction fallback paths for GitHub and GitLab
- Direct struct deserialization for all three payload types
- Full `HandleWebhook` integration tests with GitHub and GitLab sources
Co-authored-by: user <user@Mac.lan guest wan>
Reviewed-on: sneak/upaas#170
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
## Summary
Adds configurable Docker CPU and memory resource constraints per app, closes sneak/upaas#72.
## Changes
### Database
- Migration `007_add_resource_limits.sql`: adds `cpu_limit` (REAL, nullable) and `memory_limit` (INTEGER in bytes, nullable) columns to the `apps` table
### Model (`internal/models/app.go`)
- Added `CPULimit` (`sql.NullFloat64`) and `MemoryLimit` (`sql.NullInt64`) fields to `App` struct
- Updated insert, update, scan, and column list to include the new fields
### Docker Client (`internal/docker/client.go`)
- Added `CPULimit` (float64, CPU cores) and `MemoryLimit` (int64, bytes) to `CreateContainerOptions`
- Added `cpuLimitToNanoCPUs()` conversion helper and `buildResources()` to construct `container.Resources`
- Extracted `buildEnvSlice()` and `buildMounts()` helpers from `CreateContainer` for cleaner code
- Resource limits are passed to Docker's `HostConfig.Resources` (NanoCPUs / Memory)
### Deploy Service (`internal/service/deploy/deploy.go`)
- `buildContainerOptions` reads `CPULimit` and `MemoryLimit` from the app and passes them to `CreateContainerOptions`
### Handlers (`internal/handlers/app.go`)
- `HandleAppUpdate` reads and validates `cpu_limit` and `memory_limit` form fields
- Added `parseOptionalFloat64()` for CPU limit parsing (positive float or empty)
- Added `parseOptionalMemoryBytes()` for memory parsing with unit suffixes (k/m/g) or plain bytes
- Added `optionalNullString()` and `applyResourceLimits()` helpers to keep cyclomatic complexity in check
### Templates
- `app_edit.html`: Added "Resource Limits" section with CPU limit (cores) and memory limit (with unit suffix) fields
- `templates.go`: Added `formatMemoryBytes` template function for display (converts bytes → human-readable like `256m`, `1g`)
### Tests
- `internal/docker/resource_limits_test.go`: Tests for `cpuLimitToNanoCPUs` conversion
- `internal/handlers/resource_limits_test.go`: Tests for `parseOptionalFloat64` and `parseOptionalMemoryBytes` (happy paths, edge cases, validation)
- `internal/models/models_test.go`: Tests for App model resource limit persistence (save/load, null defaults, clearing)
- `internal/service/deploy/deploy_container_test.go`: Tests for container options with/without resource limits
- `templates/templates_test.go`: Tests for `formatMemoryBytes` formatting
### README
- Added "CPU and memory resource limits per app" to Features list
## Behavior
- **CPU limit**: Specified in cores (e.g. `0.5` = half a core, `2` = two cores). Converted to Docker NanoCPUs internally.
- **Memory limit**: Accepts plain bytes or suffixed values (`256m`, `1g`, `512k`). Stored as bytes in the database.
- Both fields are **optional** — empty/unset means unlimited (no Docker constraint applied).
- Limits are applied on every container creation: new deploys, rollbacks, and restarts that recreate the container.
closes sneak/upaas#72
Co-authored-by: user <user@Mac.lan guest wan>
Reviewed-on: sneak/upaas#165
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
This PR fixes env var handling by consolidating individual add/edit/delete operations into a single monolithic bulk save.
## Changes
- **Template**: Restored original table-based UI with key/value rows, edit/delete buttons, and add form. Uses Alpine.js to manage the env var list client-side. On form submit, all env vars are collected into a hidden textarea and POSTed as a single bulk request.
- **Handler**: `HandleEnvVarSave` atomically replaces all env vars (DELETE all + INSERT full set).
- **Routes**: Single `POST /apps/{id}/env` endpoint replaces individual env var CRUD routes.
- **Models**: Added `DeleteEnvVarsByAppID` and `FindEnvVarsByAppID` for bulk operations.
closes sneak/upaas#156
closes sneak/upaas#163
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Co-authored-by: user <user@Mac.lan guest wan>
Reviewed-on: sneak/upaas#158
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
## Summary
Adds a per-app webhook event history page at `/apps/{id}/webhooks` showing received webhook events with match/no-match status.
## Changes
- **New template** `webhook_events.html` — displays webhook events in a table with time, event type, branch, commit SHA (linked when URL available), and match status badges
- **New handler** `HandleAppWebhookEvents()` in `webhook_events.go` — fetches app and its webhook events (limit 100)
- **New route** `GET /apps/{id}/webhooks` — registered in protected routes group
- **Template registration** — added `webhook_events.html` to the template cache in `templates.go`
- **Model enhancement** — added `ShortCommit()` method to `WebhookEvent` for truncated SHA display
- **App detail link** — added "Event History" link in the Webhook URL card on the app detail page
## UI
Follows the existing UI patterns (Tailwind CSS classes, Alpine.js `relativeTime`, badge styles, empty state, back-navigation). The page mirrors the deployments history page layout.
closes [#85](sneak/upaas#85)
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: sneak/upaas#164
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
## Summary
`HandleAppCreate` was not reading `docker_network`, `ntfy_topic`, or `slack_webhook` form values from the create app form. These fields were silently dropped during app creation, even though:
- `app_new.html` had the form fields
- `CreateAppInput` had the corresponding struct fields
- `CreateApp` already handled them correctly
The edit/update flow was unaffected — the bug was exclusively in the create path.
## Changes
- Read `docker_network`, `ntfy_topic`, `slack_webhook` form values in `HandleAppCreate`
- Pass them to `CreateAppInput`
- Include them in template re-render data (preserves values on validation errors)
closes sneak/upaas#157
<!-- session: agent:sdlc-manager:subagent:1fb3582d-1eff-4309-b166-df5046a1b885 -->
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: sneak/upaas#160
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
Remove POST /apps, DELETE /apps/{id}, and POST /apps/{id}/deploy from
the API v1 route group. These endpoints used cookie-based session auth
without CSRF protection, creating a CSRF vulnerability.
Read-only endpoints (GET /apps, GET /apps/{id}, GET /apps/{id}/deployments),
login, and whoami are retained.
Removed handlers: HandleAPICreateApp, HandleAPIDeleteApp,
HandleAPITriggerDeploy, along with apiCreateRequest struct and
validateCreateRequest function.
Updated tests to use service layer directly for app creation in
remaining read-only endpoint tests.
Add SanitizeLogs() that strips ANSI escape sequences and non-printable
control characters (preserving newlines, carriage returns, and tabs)
from all container and deployment log output paths:
- HandleAppLogs (text/plain response)
- HandleDeploymentLogsAPI (JSON response)
- HandleContainerLogsAPI (JSON response)
Container log output is attacker-controlled data. Content-Type alone
is insufficient — the data itself must be sanitized before serving.
Includes comprehensive test coverage for the sanitization function.
- Changed SCP regex to only accept 'git' as the username
- Added path traversal check: reject URLs containing '..'
- Added test cases for non-git users and path traversal
- Add .gitea/workflows/check.yml running make check on PRs and pushes to main
- Fix .golangci.yml for golangci-lint v2 config format (was using v1 keys)
- Migrate linters-settings to linters.settings, remove deprecated exclude-use-default
- Exclude gosec false positives (G117, G703, G704, G705) with documented rationale
- Increase lll line-length from 88 to 120 (88 was too restrictive for idiomatic Go)
- Increase dupl threshold from 100 to 150 (similar CRUD handlers are intentional)
- Fix funcorder: move RemoveImage before unexported methods in docker/client.go
- Fix wsl_v5: add required blank line in deploy.go
- Fix revive unused-parameter in export_test.go
- Fix gosec G306: tighten test file permissions to 0600
- Add html.EscapeString for log output, filepath.Clean for log path
- Remove stale //nolint:funlen directives no longer needed with v2 config
- Remove API token system entirely (model, migration, middleware)
- Add migration 007 to drop api_tokens table
- Add POST /api/v1/login endpoint for JSON credential auth
- API routes now use session cookies (same as web UI)
- Remove /api/v1/tokens endpoint
- HandleAPIWhoAmI uses session auth instead of token context
- APISessionAuth middleware returns JSON 401 instead of redirect
- Update all API tests to use cookie-based authentication
Addresses review comment on PR #74.
Add inline edit functionality for environment variables, labels, and
volume mounts on the app detail page. Each entity row now has an Edit
button that reveals an inline form using Alpine.js.
- POST /apps/{id}/env-vars/{varID}/edit
- POST /apps/{id}/labels/{labelID}/edit
- POST /apps/{id}/volumes/{volumeID}/edit
- Path validation for volume host and container paths
- Warning banner about container restart after env var changes
- Tests for ValidateVolumePath
fixes#67
- Add API token model with SHA-256 hashed tokens
- Add migration 006_add_api_tokens.sql
- Add Bearer token auth middleware
- Add API endpoints under /api/v1/:
- GET /whoami
- POST /tokens (create new API token)
- GET /apps (list all apps)
- POST /apps (create app)
- GET /apps/{id} (get app)
- DELETE /apps/{id} (delete app)
- POST /apps/{id}/deploy (trigger deployment)
- GET /apps/{id}/deployments (list deployments)
- Add comprehensive tests for all API endpoints
- All tests pass, zero lint issues
- Add previous_image_id column to apps table (migration 006)
- Save current image as previous before deploying new one
- POST /apps/{id}/rollback endpoint with handler
- Rollback stops current container, starts previous image
- Creates deployment record for rollback operations
- Rollback button in app detail UI (only when previous image exists)
- Add btn-warning CSS class for rollback button styling
fixes#71
Add POST /apps/{id}/deployments/cancel endpoint that allows users to
cancel in-progress deployments via the web UI.
Changes:
- Add CancelDeploy() and HasActiveDeploy() public methods to deploy service
- Add HandleCancelDeploy handler
- Wire route in routes.go
- Add cancel button to app detail template (shown during active deployments)
- Add handler tests for cancel endpoint
fixes#66
When a webhook-triggered deploy starts for an app that already has a deploy
in progress, the existing deploy is now cancelled via context cancellation
before the new deploy begins. This prevents silently lost webhook deploys.
Changes:
- Add per-app active deploy tracking with cancel func and done channel
- Deploy() accepts cancelExisting param: true for webhook, false for manual
- Cancelled deployments are marked with new 'cancelled' status
- Add ErrDeployCancelled sentinel error
- Add DeploymentStatusCancelled model constant
- Add comprehensive tests for cancellation mechanics
Validate app names in both HandleAppCreate and HandleAppUpdate using
a regex pattern matching the client-side HTML pattern: lowercase
alphanumeric and hyphens, 2-63 chars, must start and end with
alphanumeric character.
This prevents Docker API errors, path traversal, and log injection
from crafted POST requests bypassing browser validation.
Add renderTemplate helper method on Handlers that renders templates to a
bytes.Buffer first, then writes to the ResponseWriter only on success.
This prevents partial/corrupt HTML when template execution fails partway
through.
Applied to all template rendering call sites in:
- setup.go (HandleSetupGET, renderSetupError)
- auth.go (HandleLoginGET, HandleLoginPOST error paths)
- dashboard.go (HandleDashboard)
- app.go (HandleAppNew, HandleAppCreate, HandleAppDetail, HandleAppEdit,
HandleAppUpdate, HandleAppDeployments)
- Add sanitizeTail() helper that validates tail is numeric and positive
- Clamp values to max 500
- Default to 500 when empty, non-numeric, zero, or negative
- Add comprehensive test cases
Verify that the resource's AppID matches the URL path app ID before
allowing deletion. Without this check, any authenticated user could
delete resources belonging to any app by providing the target resource's
ID in the URL regardless of the app ID in the path (IDOR vulnerability).
Closes#19
Tests demonstrate that env vars, labels, volumes, and ports can be
deleted via another app's URL path without ownership checks.
All 4 tests fail, confirming the vulnerability described in #19.
Add gorilla/csrf middleware to protect all HTML-serving routes against
cross-site request forgery attacks. The webhook endpoint is excluded
since it uses secret-based authentication.
Changes:
- Add gorilla/csrf v1.7.3 dependency
- Add CSRF() middleware method using session secret as key
- Apply CSRF middleware to all HTML route groups in routes.go
- Pass CSRF token to all templates via addGlobals helper
- Add {{ .CSRFField }} / {{ $.CSRFField }} hidden inputs to all forms
Closes#11
- Write deployment logs to files when deployment finishes (success or failure)
- Log files stored in DataDir/logs/<hostname>/<appname>/<appname>_<sha>_<timestamp>.log.txt
- Capture commit SHA for manual deploys by parsing git rev-parse HEAD after clone
- Add download endpoint for log files at /apps/{id}/deployments/{deploymentID}/download
- Add download link in deployment history view for finished deployments
- Add deploy stats (last deploy time, total count) to dashboard
- Add recent-deployments API endpoint for real-time updates
- Add live build logs to deployments history page
- Fix git clone regression (preserve entrypoint for simple clones)
- Refactor JavaScript into shared app.js with page init functions
- Deploy button disables immediately on click
- Auto-refresh deployment list and logs during builds
- Format JavaScript with Prettier (4-space indent)