3 Commits

Author SHA1 Message Date
user
35af9c99d5 Add µPaaS deployment setup for fsn1app1
All checks were successful
check / check (push) Successful in 58s
- Add Docker HEALTHCHECK instruction probing /.well-known/healthcheck.json
  (30s interval, 5s timeout, 10s start period, 3 retries) for µPaaS
  container health verification
- Create deploy/README.md with full µPaaS app configuration reference
  (app name, repo URL, branch, env vars, volumes, ports, production config)
- Add Deployment section to README.md linking to deploy docs
- Add deploy/ to .dockerignore (docs not needed in build context)
2026-03-17 02:17:15 -07:00
2e934c8894 fix: QA audit fixes for 1.0/MVP readiness (#25)
All checks were successful
check / check (push) Successful in 5s
closes #24

## QA Audit Fixes

This PR addresses issues found during the 1.0/MVP QA audit.

### Changes

1. **TODO.md: Mark AVIF encoding as done** — AVIF encoding is fully implemented via govips in `processor.go` but was still listed as a TODO item.

2. **scripts/manual-test.sh: Fix form field names** — The manual test script was using wrong field names:
   - Login form: was sending `password=...`, should be `key=...` (matching the HTML form's `name="key"`)
   - Generator form: was sending `source_url`, `fit_mode` — should be `url`, `fit` (matching the handler's `r.FormValue()` calls)
   - This means **the manual test script never actually worked** — login always failed silently because the `key` field was empty.

### Full QA Audit Results

The comprehensive QA audit report has been posted as a comment on [issue #24](#24).

Co-authored-by: user <user@Mac.lan guest wan>
Reviewed-on: #25
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-15 17:58:13 +01:00
2f15340f26 Split Dockerfile: pre-built golangci-lint stage for faster CI (#23)
All checks were successful
check / check (push) Successful in 5s
## Summary

Splits the Dockerfile into a dedicated lint stage using the pre-built `golangci/golangci-lint:v2.10.1-alpine` Docker image, replacing the manual binary download with curl/sha256 verification.

## Changes

- **Lint stage** (`AS lint`): Uses `golangci/golangci-lint:v2.10.1-alpine` pinned by sha256. Runs `make fmt-check` + `make lint`. Includes CGO deps (`build-base`, `vips-dev`, `libheif-dev`, `pkgconfig`) needed for type-checking govips imports.
- **Build stage** (`AS builder`): Depends on lint stage via `COPY --from=lint /src/go.sum /dev/null`. Runs `make test` + builds the binary. Removes `curl` (no longer needed) and the manual golangci-lint download block.
- **Runtime stage**: Unchanged.

## Benefits

- Eliminates slow multi-arch binary download + sha256 verification step
- Lint and build stages can potentially run in parallel with BuildKit
- Better Docker layer caching — lint deps cached separately from build deps
- All images remain pinned by sha256 with version+date comments

## Verification

- `docker build .` passes: fmt-check , lint (0 issues) , all tests pass , binary builds 

Closes [#18](#18)

<!-- session: agent:sdlc-manager:subagent:7aac9c54-81c8-4494-94ab-0843f97a1e62 -->

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #23
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-02 21:09:51 +01:00
6 changed files with 175 additions and 77 deletions

View File

@@ -6,3 +6,4 @@
node_modules node_modules
bin/ bin/
data/ data/
deploy/

View File

@@ -1,7 +1,29 @@
# Lint stage
# golangci/golangci-lint:v2.10.1-alpine, 2026-02-17
FROM golangci/golangci-lint:v2.10.1-alpine@sha256:33bc6b6156d4c7da87175f187090019769903d04dd408833b83083ed214b0ddf AS lint
RUN apk add --no-cache make build-base vips-dev libheif-dev pkgconfig
WORKDIR /src
# Copy go mod files first for better layer caching
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Run formatting check and linter
RUN make fmt-check
RUN make lint
# Build stage # Build stage
# golang:1.25.4-alpine, 2026-02-25 # golang:1.25.4-alpine, 2026-02-25
FROM golang:1.25.4-alpine@sha256:d3f0cf7723f3429e3f9ed846243970b20a2de7bae6a5b66fc5914e228d831bbb AS builder FROM golang:1.25.4-alpine@sha256:d3f0cf7723f3429e3f9ed846243970b20a2de7bae6a5b66fc5914e228d831bbb AS builder
# Depend on lint stage passing
COPY --from=lint /src/go.sum /dev/null
ARG VERSION=dev ARG VERSION=dev
# Install build dependencies for CGO image libraries # Install build dependencies for CGO image libraries
@@ -9,25 +31,7 @@ RUN apk add --no-cache \
build-base \ build-base \
vips-dev \ vips-dev \
libheif-dev \ libheif-dev \
pkgconfig \ pkgconfig
curl
# golangci-lint v2.10.1, 2026-02-25
# SHA-256 checksums per architecture (amd64 / arm64)
RUN set -e; \
ARCH="$(uname -m)"; \
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then \
GOARCH="arm64"; \
HASH="6652b42ae02915eb2f9cb2a2e0cac99514c8eded8388d88ae3e06e1a52c00de8"; \
else \
GOARCH="amd64"; \
HASH="dfa775874cf0561b404a02a8f4481fc69b28091da95aa697259820d429b09c99"; \
fi; \
curl -sSfL "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-linux-${GOARCH}.tar.gz" -o /tmp/golangci-lint.tar.gz && \
echo "${HASH} /tmp/golangci-lint.tar.gz" | sha256sum -c - && \
tar -xzf /tmp/golangci-lint.tar.gz -C /tmp && \
mv "/tmp/golangci-lint-2.10.1-linux-${GOARCH}/golangci-lint" /usr/local/bin/ && \
rm -rf /tmp/golangci-lint*
WORKDIR /src WORKDIR /src
@@ -38,8 +42,8 @@ RUN GOTOOLCHAIN=auto go mod download
# Copy source code # Copy source code
COPY . . COPY . .
# Run all checks (fmt-check, lint, test) # Run tests
RUN make check RUN make test
# Build with CGO enabled # Build with CGO enabled
RUN CGO_ENABLED=1 GOTOOLCHAIN=auto go build -ldflags "-X main.Version=${VERSION}" -o /pixad ./cmd/pixad RUN CGO_ENABLED=1 GOTOOLCHAIN=auto go build -ldflags "-X main.Version=${VERSION}" -o /pixad ./cmd/pixad
@@ -71,4 +75,7 @@ WORKDIR /var/lib/pixa
EXPOSE 8080 EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -q --spider http://localhost:8080/.well-known/healthcheck.json
ENTRYPOINT ["/usr/local/bin/pixad", "--config", "/etc/pixa/config.yml"] ENTRYPOINT ["/usr/local/bin/pixad", "--config", "/etc/pixa/config.yml"]

View File

@@ -125,6 +125,17 @@ See `config.example.yml` for all options with defaults.
- **Metrics**: Prometheus - **Metrics**: Prometheus
- **Logging**: stdlib slog - **Logging**: stdlib slog
## Deployment
Pixa is deployed via
[µPaaS](https://git.eeqj.de/sneak/upaas) on `fsn1app1`
(paas.datavi.be). Pushes to `main` trigger automatic builds and
deployments. The Dockerfile includes a `HEALTHCHECK` that probes
`/.well-known/healthcheck.json`.
See [deploy/README.md](deploy/README.md) for the full µPaaS app
configuration, volume mounts, and production setup instructions.
## TODO ## TODO
See [TODO.md](TODO.md) for the full prioritized task list. See [TODO.md](TODO.md) for the full prioritized task list.

101
TODO.md
View File

@@ -1,64 +1,65 @@
# Status # Pixa 1.0 TODO
pre-1.0. No git tags. Full policy file set is present, but make check Remaining tasks sorted by priority for a working 1.0 release.
fails on main with 10 gosec lint findings, so main is not green.
# Next Step ## P0: Critical for 1.0
Fix the 10 gosec findings so make check passes on main. Fix each issue ### Image Processing
properly (no blanket nolint suppressions) and confirm lint, tests, and - [x] Add WebP encoding support (currently returns error)
CI are green. This restores the main-always-green policy. - [x] Add AVIF encoding support (implemented via govips)
# Completed Steps ### Manual Testing (verify auth/encrypted URLs work)
- [ ] Manual test: visit `/`, see login form
- [ ] Manual test: enter wrong key, see error
- [ ] Manual test: enter correct signing key, see generator form
- [ ] Manual test: generate encrypted URL, verify it works
- [ ] Manual test: wait for expiration or use short TTL, verify expired URL returns 410
- [ ] Manual test: logout, verify redirected to login
- 2026-02-25: Docker arm64 build fixed via architecture detection for ### Cache Management
golangci-lint download (#16) - [ ] Implement cache size management/eviction (prevent disk from filling up)
- 2026-02-25: full repo policy compliance pass (#14): LICENSE,
REPO_POLICIES.md, .editorconfig, Gitea Actions CI workflow, Docker
images pinned by hash, README restructured to policy sections, hooks
Makefile target, golangci-lint errors resolved
- 2026-02-20: stale local dev config files removed (#12)
- 2026-02-09: correctness fixes: negative cache checked in Service.Get()
before upstream fetch (#8); Stats() column scanning and HitRate
computation corrected (#9)
- 2026-02-08: AllowHTTP propagated to SourceURL() scheme selection (#6)
- WebP encoding support added (P0 item, checked off in TODO)
- 2026-01-08: initial implementation with Makefile and TODO checklist
# Future Steps ### Configuration
- [ ] Validate configuration on startup (fail fast on bad config)
Compliance: ## P1: Important for Production
- After the gosec fix lands, verify CI runs make check green on main ### Security
- [ ] Implement blocked networks configuration (extend SSRF protection)
- [ ] Add rate limiting global concurrent fetches (prevent resource exhaustion)
P0, critical for 1.0 (from existing TODO.md): ### Image Processing
- [ ] Implement EXIF/metadata stripping (privacy)
- Add AVIF encoding support (currently returns error) ## P2: Nice to Have
- Manual test pass for auth and encrypted URLs:
- visit /, see login form
- wrong key shows error; correct signing key shows generator form
- generated encrypted URL works
- expired URL (short TTL) returns 410
- logout redirects to login
- Implement cache size management and eviction (prevent disk fill)
- Validate configuration on startup (fail fast on bad config)
P1, important for production: ### Security
- [ ] Implement referer blacklist
- [ ] Add rate limiting per-IP
- [ ] Add rate limiting per-origin
- Blocked networks configuration (extend SSRF protection) ### HTTP Response Handling
- Rate limit global concurrent fetches (resource exhaustion) - [ ] Implement Last-Modified headers
- EXIF/metadata stripping (privacy) - [ ] Implement Vary header for content negotiation
- [ ] Implement X-Request-ID propagation
P2, nice to have: ### Additional Endpoints
- [ ] Implement auto-format selection (format=auto based on Accept header)
- Referer blacklist; per-IP and per-origin rate limiting ### Configuration
- Last-Modified headers; Vary header; X-Request-ID propagation - [ ] Add all configuration options from README
- Auto-format selection (format=auto based on Accept header) - [ ] Implement environment variable overrides
- All configuration options from README; env var overrides; YAML config - [ ] Implement YAML config file support
file support
- Sentry error reporting (optional); comprehensive request logging; ### Operational
Prometheus metrics - [ ] Implement Sentry error reporting (optional)
- Integration tests for the image proxy flow; load tests for the - [ ] Add comprehensive request logging
1-5k req/s target - [ ] Add performance metrics (Prometheus)
- Docs: configuration options, API endpoints, deployment guide, example - [ ] Write integration tests for image proxy flow
nginx/caddy reverse proxy config - [ ] Write load tests to verify 1-5k req/s target
### Documentation
- [ ] Document configuration options
- [ ] Document API endpoints
- [ ] Document deployment guide
- [ ] Add example nginx/caddy reverse proxy config

78
deploy/README.md Normal file
View File

@@ -0,0 +1,78 @@
# Pixa Deployment via µPaaS
Pixa is deployed on `fsn1app1` via
[µPaaS](https://git.eeqj.de/sneak/upaas) (paas.datavi.be).
## µPaaS App Configuration
Create the app in the µPaaS web UI with these settings:
| Setting | Value |
| --- | --- |
| **App name** | `pixa` |
| **Repo URL** | `git@git.eeqj.de:sneak/pixa.git` |
| **Branch** | `main` |
| **Dockerfile path** | `Dockerfile` |
### Environment Variables
| Variable | Description | Required |
| --- | --- | --- |
| `PORT` | HTTP listen port (default: 8080) | No |
Configuration is provided via the config file baked into the Docker
image at `/etc/pixa/config.yml`. To override it, mount a custom
config file as a volume (see below).
### Volumes
| Host Path | Container Path | Description |
| --- | --- | --- |
| `/srv/pixa/data` | `/var/lib/pixa` | SQLite database and image cache |
| `/srv/pixa/config.yml` | `/etc/pixa/config.yml` | Production config (signing key, whitelist, etc.) |
### Ports
| Host Port | Container Port | Protocol |
| --- | --- | --- |
| (assigned) | 8080 | TCP |
### Docker Network
Attach to the shared reverse-proxy network if using Caddy/Traefik
for TLS termination.
## Production Configuration
Copy `config.example.yml` from the repo root and customize for
production:
```yaml
port: 8080
debug: false
maintenance_mode: false
state_dir: /var/lib/pixa
signing_key: "<generate with: openssl rand -base64 32>"
whitelist_hosts:
- s3.sneak.cloud
- static.sneak.cloud
- sneak.berlin
allow_http: false
```
**Important:** Generate a unique `signing_key` for production. Never
use the default placeholder value.
## Health Check
The Dockerfile includes a `HEALTHCHECK` instruction that probes
`/.well-known/healthcheck.json` every 30 seconds. µPaaS verifies
container health 60 seconds after deployment.
## Deployment Flow
1. Push to `main` triggers the Gitea webhook
2. µPaaS clones the repo and runs `docker build .`
3. The Dockerfile runs `make check` (format, lint, test) during build
4. On success, µPaaS stops the old container and starts the new one
5. After 60 seconds, µPaaS checks container health

View File

@@ -48,7 +48,7 @@ fi
# Test 3: Wrong password shows error # Test 3: Wrong password shows error
echo "--- Test 3: Login with wrong password ---" echo "--- Test 3: Login with wrong password ---"
WRONG_LOGIN=$(curl -sf -X POST "$BASE_URL/" -d "password=wrong-key" -c "$COOKIE_JAR") WRONG_LOGIN=$(curl -sf -X POST "$BASE_URL/" -d "key=wrong-key" -c "$COOKIE_JAR")
if echo "$WRONG_LOGIN" | grep -qi "invalid\|error\|incorrect\|wrong"; then if echo "$WRONG_LOGIN" | grep -qi "invalid\|error\|incorrect\|wrong"; then
pass "Wrong password shows error message" pass "Wrong password shows error message"
else else
@@ -57,7 +57,7 @@ fi
# Test 4: Correct password redirects to generator # Test 4: Correct password redirects to generator
echo "--- Test 4: Login with correct signing key ---" echo "--- Test 4: Login with correct signing key ---"
curl -sf -X POST "$BASE_URL/" -d "password=$SIGNING_KEY" -c "$COOKIE_JAR" -b "$COOKIE_JAR" -L -o /dev/null curl -sf -X POST "$BASE_URL/" -d "key=$SIGNING_KEY" -c "$COOKIE_JAR" -b "$COOKIE_JAR" -L -o /dev/null
GENERATOR_PAGE=$(curl -sf "$BASE_URL/" -b "$COOKIE_JAR") GENERATOR_PAGE=$(curl -sf "$BASE_URL/" -b "$COOKIE_JAR")
if echo "$GENERATOR_PAGE" | grep -qi "generate\|url\|source\|logout"; then if echo "$GENERATOR_PAGE" | grep -qi "generate\|url\|source\|logout"; then
pass "Correct password shows generator page" pass "Correct password shows generator page"
@@ -68,12 +68,12 @@ fi
# Test 5: Generate encrypted URL # Test 5: Generate encrypted URL
echo "--- Test 5: Generate encrypted URL ---" echo "--- Test 5: Generate encrypted URL ---"
GEN_RESULT=$(curl -sf -X POST "$BASE_URL/generate" -b "$COOKIE_JAR" \ GEN_RESULT=$(curl -sf -X POST "$BASE_URL/generate" -b "$COOKIE_JAR" \
-d "source_url=$TEST_IMAGE_URL" \ -d "url=$TEST_IMAGE_URL" \
-d "width=800" \ -d "width=800" \
-d "height=600" \ -d "height=600" \
-d "format=jpeg" \ -d "format=jpeg" \
-d "quality=85" \ -d "quality=85" \
-d "fit_mode=cover" \ -d "fit=cover" \
-d "ttl=3600") -d "ttl=3600")
if echo "$GEN_RESULT" | grep -q "/v1/e/"; then if echo "$GEN_RESULT" | grep -q "/v1/e/"; then
pass "Encrypted URL generated" pass "Encrypted URL generated"
@@ -121,10 +121,10 @@ fi
# Test 9: Generate short-TTL URL and verify expiration # Test 9: Generate short-TTL URL and verify expiration
echo "--- Test 9: Expired URL returns 410 ---" echo "--- Test 9: Expired URL returns 410 ---"
# Login again # Login again
curl -sf -X POST "$BASE_URL/" -d "password=$SIGNING_KEY" -c "$COOKIE_JAR" -b "$COOKIE_JAR" -L -o /dev/null curl -sf -X POST "$BASE_URL/" -d "key=$SIGNING_KEY" -c "$COOKIE_JAR" -b "$COOKIE_JAR" -L -o /dev/null
# Generate URL with 1 second TTL # Generate URL with 1 second TTL
GEN_RESULT=$(curl -sf -X POST "$BASE_URL/generate" -b "$COOKIE_JAR" \ GEN_RESULT=$(curl -sf -X POST "$BASE_URL/generate" -b "$COOKIE_JAR" \
-d "source_url=$TEST_IMAGE_URL" \ -d "url=$TEST_IMAGE_URL" \
-d "width=100" \ -d "width=100" \
-d "height=100" \ -d "height=100" \
-d "format=jpeg" \ -d "format=jpeg" \