Commit Graph
13 Commits
Author SHA1 Message Date
clawbotandsneak 7a34fc999c Update golangci-lint to v2.12.2 with canonical config (#187)
Check / check (push) Successful in 4s
Bumps golangci-lint from v2.10.1 to v2.12.2 everywhere it is pinned and installs the canonical `.golangci.yml`, then fixes every finding the new linter surfaces so `make check` is green.

## Version pins

- `Dockerfile` lint stage: `golangci/golangci-lint:v2.12.2` (Debian-based), tag plus digest pin
- `script/bootstrap`: `GOLANGCI_LINT_VERSION=2.12.2` with updated `linux-amd64`/`linux-arm64` release-archive sha256 pins

## Config

`.golangci.yml` replaced with the canonical config. Material change: the old file declared `version: "2"` but kept settings under the legacy top-level `linters-settings` key, which golangci-lint v2 ignores — so the intended thresholds (`lll` 88, `funlen` 80/50, `cyclop` 15, `dupl` 100) were not being applied. The canonical file moves them under `linters.settings` and drops `issues.exclude-use-default`.

## Lint fixes (216 findings)

- `lll` (96): wrapped lines to the 88-column limit
- `noctx` (46): `httptest.NewRequestWithContext` with `t.Context()` throughout the tests
- `goconst` (24): shared constants for template/JSON keys in `internal/handlers` and repeated test literals
- `gosec` (23): app-page redirects now go through a `redirectToApp` helper that path-escapes the app ID (G710 open redirect); `http.ServeFile` of the internally derived deployment log path annotated like the adjacent `os.Stat` (G703)
- `dupl` (22): extracted a generic `findAllByAppID` in `internal/models`, a `deleteAppResource` helper in `internal/handlers`, a shared `parsePush` in `internal/service/webhook`, and table-driven/helper-based dedup in tests
- `nolintlint` (5): removed `//nolint:funlen` directives made obsolete by the new limits (plus one more that became obsolete after refactoring)
- `nilerr` (3, surfaced during fixing): resource-delete lookups now propagate the find error to the caller

No behavior changes intended; all tests pass and `make check` is green.

Note: golangci-lint v2.12 warns that `gomodguard` is deprecated in favor of `gomodguard_v2` — a future canonical-config update should address this centrally.
Co-authored-by: sneak <sneak@sneak.berlin>
Reviewed-on: #187
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-08-07 22:21:42 +02:00
d5c3d4d55f Move schema_migrations table creation into 000.sql with INTEGER version column (#172)
Check / check (push) Successful in 5s
Closes #171

Refactors the migration system to follow the [pixa pattern](sneak/pixa#36):

## Changes

### New file: `internal/database/migrations/000.sql`
- Bootstrap migration that creates `schema_migrations` with `version INTEGER PRIMARY KEY`
- Self-contained: includes both `CREATE TABLE IF NOT EXISTS` and `INSERT OR IGNORE INTO schema_migrations (version) VALUES (0)`

### Refactored: `internal/database/migrations.go`
- **Go code no longer creates the migrations table inline** — it reads and executes `000.sql` as a bootstrap step before the normal migration loop
- **`ParseMigrationVersion(filename)`** — exported function that extracts integer version from filenames like `001_initial.sql` → `1`
- **`ApplyMigrations(ctx, db, log)`** — exported function for tests to apply schema without full fx lifecycle
- **`bootstrapMigrationsTable`** — checks if table exists; if missing, runs `000.sql`; if present, checks for legacy format
- **`convertLegacyMigrations`** — one-time conversion for existing databases that stored versions as TEXT filenames (e.g. `"001_initial.sql"`) to INTEGER versions (e.g. `1`)
- **Transaction-wrapped migration application** — each migration runs in a transaction for atomicity
- Sentinel error `ErrInvalidMigrationFilename` for static error wrapping

### New file: `internal/database/migrations_test.go`
- `TestParseMigrationVersion` — valid/invalid filename parsing
- `TestApplyMigrationsFreshDatabase` — verifies bootstrap creates table, all migrations apply, application tables exist
- `TestApplyMigrationsIdempotent` — double-apply is a no-op
- `TestApplyMigrationsLegacyConversion` — simulates old TEXT-based table with filename entries, verifies conversion to integer format

## Backward Compatibility

Existing databases with the old TEXT-based `schema_migrations` table are automatically converted on first run. The conversion:
1. Detects legacy entries (versions containing `_`)
2. Parses integer version from each legacy filename
3. Drops the old table
4. Creates the new INTEGER-based table via `000.sql`
5. Re-inserts all parsed integer versions

All existing migrations (001-007) continue to work unchanged.

Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: clawbot <clawbot@sneak.berlin>
Reviewed-on: #172
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-30 21:40:16 +02:00
clawbotanduser 57ec4331ef feat: add GitHub and GitLab webhook support (#170)
Check / check (push) Successful in 3m17s
## 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: #170
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-22 00:46:10 +01:00
clawbotanduser 67361419f5 feat: CPU/memory resource limits per app (#165)
Check / check (push) Successful in 5s
## Summary

Adds configurable Docker CPU and memory resource constraints per app, closes #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 #72

Co-authored-by: user <user@Mac.lan guest wan>
Reviewed-on: #165
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-20 06:44:48 +01:00
fd110e69db feat: monolithic env var editing with bulk save (#158)
Check / check (push) Successful in 6s
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 #156
closes #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: #158
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-11 12:06:56 +01:00
clawbotandclawbot e1dc865226 feat: add webhook event history UI page (#164)
Check / check (push) Successful in 4s
## 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](#85)

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #164
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-10 18:53:58 +01:00
49ff625ac4 fix: add missing Makefile targets (docker, hooks) and test timeout (#159)
Check / check (push) Successful in 4s
## Changes

- Add `docker` target (`docker build .`)
- Add `hooks` target (installs pre-commit hook running `make check`)
- Add 30-second timeout to `test` target (`-timeout 30s`)
- Update `.PHONY` to include new targets
- Update README to document all Makefile targets (`fmt-check`, `docker`, `hooks`)
- Run `make fmt` to fix JS formatting via prettier

`docker build .` passes 

closes #136, closes #137

<!-- session: agent:sdlc-manager:subagent:44375174-444b-43bf-a341-2def7ebb9fdf -->

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #159
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-10 01:09:15 +01:00
clawbotandclawbot ab63670043 fix: pass notification settings from create form to service (#160)
Check / check (push) Successful in 3m49s
## 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 #157

<!-- session: agent:sdlc-manager:subagent:1fb3582d-1eff-4309-b166-df5046a1b885 -->

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #160
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-10 01:01:32 +01:00
clawbotandclawbot 1cd433b069 chore: add REPO_POLICIES compliance files (#155)
Check / check (push) Successful in 6s
Add `.gitignore`, `.editorconfig`, `REPO_POLICIES.md`, and `.dockerignore` to bring the repository into compliance with REPO_POLICIES standards.

### Changes

- **`.gitignore`** ([#132](#132)): Standard template from `sneak/prompts` plus Go-specific entries (bin/, *.exe, *.test, etc.)
- **`.editorconfig`** ([#133](#133)): root=true, UTF-8, LF line endings, trim trailing whitespace, final newline. Go and Makefile use tabs, everything else 2 spaces.
- **`REPO_POLICIES.md`** ([#134](#134)): Copied as-is from `sneak/prompts` (last_modified: 2026-02-22)
- **`.dockerignore`** ([#135](#135)): Excludes `.git`, `bin/`, `.editorconfig`, `.vscode/`, `.idea/`, `*.test`, `LICENSE`, and documentation files. Keeps all files needed by the Dockerfile (source code, go.mod, go.sum, Makefile, .golangci.yml, static/, templates/).

`docker build .` passes with these changes.

closes #132, closes #133, closes #134, closes #135

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #155
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-03 18:07:44 +01:00
clawbotandclawbot 94639a47e9 fix: add COPY --from=lint to builder stage to force lint execution (#154)
Check / check (push) Successful in 1m30s
BuildKit skips unreferenced stages silently. The lint stage (added in PR [#152](#152)) was never referenced by the builder stage via `COPY --from`, so it was being skipped entirely during `docker build .`. Linting was not actually running in CI.

This adds `COPY --from=lint /src/go.sum /dev/null` to the builder stage, creating a stage dependency that forces the lint stage to complete before the build proceeds.

Verified: `docker build .` now runs the lint stage (fmt-check + lint) and passes.

closes #153

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #154
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 23:46:52 +01:00
12446f9f79 fix: change module path to sneak.berlin/go/upaas (closes #143) (#149)
Check / check (push) Successful in 1m16s
Changes the Go module path from `git.eeqj.de/sneak/upaas` to `sneak.berlin/go/upaas`.

All import paths in Go files updated accordingly. `go mod tidy` and `make check` pass cleanly.

fixes #143

Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #149
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 23:22:18 +01:00
877fb2c0c5 Split Dockerfile into lint + build stages for faster CI feedback (#152)
Check / check (push) Successful in 1m4s
## Summary

Splits the Dockerfile into separate lint and build stages to provide faster CI feedback on formatting and lint issues.

### Changes

**Dockerfile:**
- **Lint stage** (`golangci/golangci-lint:v2.10.1`, pinned by sha256): Runs `make fmt-check` and `make lint` using the official golangci-lint image which has the linter pre-installed. No more downloading golangci-lint on every build.
- **Build stage** (`golang:1.25-alpine`, pinned by sha256): Runs `make test` and `make build`. Same alpine image as before.
- **Runtime stage**: Unchanged.

**Makefile:**
- Added `fmt-check` target for standalone gofmt checking.
- Refactored `check` target to use `fmt-check`, `lint`, `test` as dependencies instead of inline commands. Still works identically for local use.

### Benefits
- Lint failures surface immediately without waiting for golangci-lint download
- Uses official pre-built golangci-lint image instead of manual binary download
- Cleaner separation of concerns between lint and build stages
- `make check` still runs everything sequentially for local development

closes #151

Co-authored-by: clawbot <clawbot@eeqj.de>
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #152
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 21:19:21 +01:00
clawbot 6696db957d Update TODO.md: check off deployment cancellation 2026-02-16 09:12:08 +01:00