diff options
Diffstat (limited to 'AGENTS.md')
| -rw-r--r-- | AGENTS.md | 53 |
1 files changed, 30 insertions, 23 deletions
@@ -22,9 +22,11 @@ This project follows a **minimal philosophy**: software should be simple, minima witryna serve # Start the deployment server witryna validate # Validate config and print summary witryna run <site> [-v] # One-off build (synchronous) -witryna status [-s <site>] [--json] # Deployment status +witryna status [site] [--json] # Deployment status +witryna switch <site> <build> # Switch active build (rollback) +witryna cleanup [site] [--keep N] # Remove old builds/logs (manual) # Config discovery: ./witryna.toml → $XDG_CONFIG_HOME/witryna/witryna.toml → /etc/witryna/witryna.toml -# Override with: witryna --config /path/to/witryna.toml <command> +# Override with: witryna <command> --config /path/to/witryna.toml ``` ### Development @@ -37,10 +39,8 @@ just test # Run unit tests just test-integration # Run integration tests (Tier 1 + Tier 2) just test-integration-serial # Integration tests with --test-threads=1 (for SIGHUP) just test-all # All lints + unit tests + integration tests +just lint-picky # Pedantic + nursery clippy lints (warnings only, not gated) just pre-commit # Mirrors lefthook pre-commit checks -just man-1 # View witryna(1) man page (needs cargo build first) -just man-5 # View witryna.toml(5) man page - # Cargo (direct) cargo build # Build the project cargo run # Run the application @@ -51,7 +51,7 @@ cargo check # Type-check without building ### Core Components -1. **HTTP Server (axum)**: Listens on localhost, handles webhook POST requests +1. **HTTP Server (tiny_http)**: Listens on localhost, handles webhook POST requests 2. **Site Manager**: Manages site configurations from `witryna.toml` 3. **Build Executor**: Runs containerized builds via Podman/Docker 4. **Asset Publisher**: Atomic symlink switching for zero-downtime deployments @@ -68,7 +68,8 @@ cargo check # Type-check without building ├── clones/{site-name}/ # Git repository clones ├── builds/{site-name}/ │ ├── {timestamp}/ # Timestamped build outputs -│ └── current -> {latest} # Symlink to current build +│ ├── current -> {latest} # Symlink to current build +│ └── state.json # Current build state (building/success/failed) └── cache/{site-name}/ # Persistent build caches /var/log/witryna/ @@ -83,7 +84,6 @@ cargo check # Type-check without building - `202 Accepted` - Build triggered (immediate or queued) - `401 Unauthorized` - Invalid token (only when `webhook_token` is configured; `{"error": "unauthorized"}`) - `404 Not Found` - Unknown site (`{"error": "not_found"}`) - - `429 Too Many Requests` - Rate limit exceeded (`{"error": "rate_limit_exceeded"}`) ### System Diagram @@ -118,6 +118,7 @@ Upon receiving a valid webhook request, Witryna executes asynchronously: 1. **Acquire Lock / Queue:** Per-site non-blocking lock. If a build is in progress, the request is queued (depth-1, latest-wins). Queued rebuilds run after the current build completes. 2. **Determine Paths:** Construct clone/build paths from `base_dir` and `site_name`. +2b. **Write Build State:** Write `state.json` with `"building"` status (enables `witryna status` to show in-progress builds). 3. **Fetch Source Code:** `git clone` if first time, `git pull` otherwise. 3b. **Initialize Submodules:** If `.gitmodules` exists, run `git submodule sync --recursive` (pull only) then `git submodule update --init --recursive [--depth N]`. 4. **Parse Repository Config:** Read build config (`.witryna.yaml` / `witryna.yaml` / custom `config_file`) or use `witryna.toml` overrides. @@ -136,7 +137,8 @@ Upon receiving a valid webhook request, Witryna executes asynchronously: node:20-alpine sh -c "npm install && npm run build" ``` 6. **Publish Assets:** Copy built `public` dir to timestamped directory, atomically switch symlink via `ln -sfn`. -6b. **Post-Deploy Hook (Optional):** Run `post_deploy` command with `WITRYNA_SITE`, `WITRYNA_BUILD_DIR`, `WITRYNA_BUILD_TIMESTAMP` env vars. 30s timeout, non-fatal on failure. +6b. **Post-Deploy Hook (Optional):** Run `post_deploy` command with `WITRYNA_SITE`, `WITRYNA_BUILD_DIR`, `WITRYNA_PUBLIC_DIR`, `WITRYNA_BUILD_TIMESTAMP`, `WITRYNA_BUILD_STATUS` env vars. Hooks run on both success and failure. 30s timeout, non-fatal on failure. +6c. **Update Build State:** Write `state.json` with final status (`"success"`, `"failed"`, or `"hook failed"`). 7. **Release Lock:** Release the per-site lock. 8. **Log Outcome:** Log success or failure. @@ -186,7 +188,7 @@ cargo test --features integration overrides # Build config override tests #### Test Tiers -- **Tier 1 (no container runtime needed):** health, auth (401), 404, concurrent build (409), rate limit (429), edge cases, SIGHUP +- **Tier 1 (no container runtime needed):** health, auth (401), 404, concurrent build (409), edge cases, SIGHUP - **Tier 2 (requires podman or docker):** deploy, logs, cleanup, overrides, polling Tests that require git or a container runtime automatically skip with an explicit message (e.g., `SKIPPED: no container runtime (podman/docker) found`) when the dependency is missing. @@ -212,7 +214,6 @@ tests/integration/ not_found.rs # 404: unknown site deploy.rs # Full build pipeline (Tier 2) concurrent.rs # 409 via DashSet injection (Tier 1) - rate_limit.rs # 429 with isolated server (Tier 1) logs.rs # Build log verification (Tier 2) cleanup.rs # Old build cleanup (Tier 2) sighup.rs # SIGHUP reload (#[serial], Tier 1) @@ -222,13 +223,16 @@ tests/integration/ cache.rs # Cache directory persistence (Tier 2) env_vars.rs # Environment variable passing (Tier 2) cli_run.rs # witryna run command (Tier 2) - cli_status.rs # witryna status command (Tier 1) + cli_status.rs # witryna status command + state.json (Tier 1) + cli_switch.rs # witryna switch command (Tier 1) + cli_cleanup.rs # witryna cleanup command (Tier 1) + cli_validate.rs # witryna validate command (Tier 1) hooks.rs # Post-deploy hooks (Tier 2) ``` #### Test Categories -- **Core pipeline** — health, auth (401), 404, deployment (202), concurrent build rejection (409), rate limiting (429) +- **Core pipeline** — health, auth (401), 404, deployment (202), concurrent build rejection (409) - **FEAT-001** — SIGHUP config hot-reload - **FEAT-002** — build config overrides from `witryna.toml` (complete and partial) - **FEAT-003** — periodic repository polling, new commit detection @@ -253,12 +257,11 @@ Follow OWASP best practices for all HTTP endpoints: - Limit request body size to prevent DoS 3. **Rate Limiting** - - Implement rate limiting per token/IP to prevent abuse - - Return `429 Too Many Requests` when exceeded + - Delegated to the reverse proxy (Caddy, nginx). See `examples/caddy/Caddyfile` and `examples/nginx/witryna.conf` for configuration. 4. **Error Handling** - Never expose internal error details in responses - - Log detailed errors server-side with `tracing` + - Log detailed errors server-side with `log` - Return generic error messages to clients 5. **Command Injection Prevention** @@ -273,10 +276,14 @@ Follow OWASP best practices for all HTTP endpoints: - Configurable working directory: `container_workdir` (relative path, no traversal) - Podman: rootless via `--userns=keep-id`; Docker: `--cap-add=DAC_OVERRIDE` for workspace access +## Backlog & Sprint Tracking + +`SPRINT.md` (gitignored) contains the project backlog, current sprint tasks, and sprint history. Always check `SPRINT.md` first when asked "what's next", what remains for a milestone, or what to work on. + ## Conventions - Use `anyhow` for error handling with context -- Use `tracing` macros for logging (`info!`, `debug!`, `error!`) +- Use `log` macros for logging (`info!`, `debug!`, `error!`) - Async-first: prefer `tokio::fs` over `std::fs` - Use `DashSet` for concurrent build tracking - `SPRINT.md` is gitignored — update it after each task to track progress, but **never commit it** @@ -285,15 +292,15 @@ Follow OWASP best practices for all HTTP endpoints: ## Branching -- Implement each new feature or task on a **dedicated branch** named after the task ID (e.g., `cli-002-man-pages`, `pkg-001-cargo-deb`) -- Branch from `main` before starting work: `git checkout -b <branch-name> main` -- Keep the branch focused on a single task — do not mix unrelated changes -- Merge back to `main` only after the task is complete and tests pass -- Do not delete the branch until the merge is confirmed +- **Only `main` is pushed to the remote.** `main` has a clean, linear history — one squash commit per feature/milestone. +- Day-to-day work happens on a **`staging`** branch (or feature branches off staging). These are never pushed. +- When work is complete and tests pass, squash-merge `staging` into `main` with a single descriptive commit. +- Feature branches (if used) branch from `staging`, not `main`. +- Do not push work-in-progress branches — Git is decentralized; only publish finished work. ## Commit Rules -**IMPORTANT:** Before completing any task, run `just test-all` to verify everything passes, then run `/commit-smart` to commit changes. +**IMPORTANT:** Before completing any task, run `just test-all` and `just lint-picky` to verify everything passes, then run `/commit-smart` to commit changes. - Only commit files modified in the current session - Use atomic commits with descriptive messages |
