cdcf527b250ba2053cad551a2ebc94cb96fa9cc3
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
6696db957d | Update TODO.md: check off deployment cancellation |