This document provides comprehensive guidelines for AI agents working on the /dev/push codebase. It covers scripts, FastAPI application code, Docker/Compose, and project-wide conventions.
- Scripts (
scripts/) - FastAPI Application (
app/) - Docker & Compose
- Project Structure
- General Conventions
- Testing & Deployment
These guidelines apply to every script under scripts/ (install/start/stop/restart/helpers/etc.).
- Always source
scripts/lib.sh(use theSCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"pattern, thensource "$SCRIPT_DIR/lib.sh"). The lib setsAPP_DIR,DATA_DIR,ENV_FILE, etc., and auto-detects dev vs prod via systemd orDEVPUSH_ENV. - Do not hardcode relative paths. Derive everything from
APP_DIR,DATA_DIR, orSCRIPT_DIR. - Expose overrides via
DEVPUSH_*env vars (already handled by the lib).
- Call
ensure_compose_cmdbefore issuing any compose commands (or rely onset_compose_basewhich calls it internally). - Use
set_compose_baseto populateCOMPOSE_BASE(readsCERT_CHALLENGE_PROVIDERfrom.envinternally). - Run compose via
run_cmd "Message..." "${COMPOSE_BASE[@]}" <subcommand> …. Never spelldocker compose/docker-composedirectly. set_compose_basealso ensuresSERVICE_UID/SERVICE_GIDare exported so Docker builds run with the correct user. Never assume UID/GID 1000; always go through the helper.
- Use
run_cmdfor every non-trivial operation (package installs, docker commands, helper scripts). It handles spinners, logging, and error capture. - At the top of each logical section add a short comment (
# Install Docker,# Start stack, etc.) so the script reads like a TOC. Skip obvious blocks likeusage(). - For blank lines between major blocks, call
printf '\n'once—no bareecho. - When printing status messages manually (e.g., final "Success" line), use
printf "${GRN}…${NC}\n"for consistency. - Use parent/child command structure for multi-step operations:
printf "Installing...\n" printf "%s Building runner images...\n" "$CHILD_MARK" run_cmd "${CHILD_MARK} Starting services..." "${COMPOSE_BASE[@]}" up -d
- Keep flag sets minimal; only add options when they're truly needed (e.g.,
--no-migrate,--timeout <value>). - In usage blocks, show value placeholders as
<value>and list allowed values inline. - Validate flag values early and exit via
usageon invalid input. - For sensitive input (tokens, passwords), make flags optional and prompt securely with
read -sif not provided and TTY is available.
- Prefer shared helpers over inline logic:
- DB migrations:
run_cmd "Running database migrations..." bash "$SCRIPT_DIR/db-migrate.sh".
- DB migrations:
- If a helper emits output, rely on its own logging (no extra text before/after unless absolutely necessary).
- Break scripts into clear sections with comments (e.g.,
# Create data directories,# Validate core env). - Within a section, keep related commands together and avoid interleaving unrelated work.
- Use
set -Eeuo pipefailand a trap that prints the last command andSCRIPT_ERR_LOG(seestart.sh/install.shfor reference). - It's fine to precede the argument-parsing block with a short comment like
# Parse CLI flagsfor readability.
- Avoid
echounless you truly need the "no newline" behavior; preferprintf. - When running commands as the service user from privileged scripts (e.g., install), wrap them in
runuser -u "$user" -- bash -c '…'so files are owned bydevpush. - When creating files/dirs that might already exist, guard with
[[ ! -f … ]]/install -d …and let them be no-ops if present. - For comment documentation aimed at future maintainers, keep it short and factual—no personal notes or TODOs; use
AGENTS.mdinstead. - Use
validate_env "$ENV_FILE"whenever you need to enforce required environment variables; it handles core values and certificate-challenge-specific secrets for production.
app/routers/: Route handlers organized by domain (auth, project, team, user, admin, etc.)app/forms/: WTForms form definitions (one file per domain)app/models.py: SQLAlchemy ORM modelsapp/services/: Business logic services (GitHub, deployment, domain, Loki, etc.)app/utils/: Utility functions (access control, pagination, color, etc.)app/templates/: Jinja2 templates organized by domainapp/workers/: Background workers (jobs, monitor) and tasksapp/config.py: Pydantic settings and configurationapp/dependencies.py: FastAPI dependencies and template helpersapp/db.py: Database connection and session management
- Router organization: One router per domain (auth, project, team, user, admin, etc.)
- Route naming: Use
name="route_name"for URL generation viarequest.url_for() - Dependencies: Use FastAPI's
Depends()for dependency injection:get_current_user: Require authenticationget_db: Database sessionget_settings: Application settingsget_team_by_slug: Team access controlget_role,get_access: Permission checks
- Template responses: Always use
TemplateResponsefromdependencies.py(handles HTMX fragment wrapping automatically) - Flash messages: Use
flash()helper fromdependencies.pyfor user notifications
- File organization: One form file per domain (e.g.,
forms/project.py,forms/team.py) - Form classes: Inherit from
StarletteForm(fromstarlette-wtf) - Validation: Use WTForms validators; custom validation in
validate_<fieldname>()methods - Translation: Use
_l()helper for translatable labels (e.g.,_l("Create team")) - Form handling: Use
await FormClass.from_formdata(request)andawait form.validate_on_submit()
- Structure: Organized by domain with
pages/,partials/, andmacros/subdirectories - Layouts:
layouts/base.html: Main layoutlayouts/app.html: Authenticated app layoutlayouts/fragment.html: HTMX fragment wrapper (auto-applied byTemplateResponse)
- HTMX: Templates automatically wrapped in fragment layout when
HX-Requestheader is present - Macros: Reusable components in
macros/(toast, dialog, select, tabs, etc.) - Translation: Use
_()function in templates (defined independencies.py) - Flash messages: Access via
get_flashed_messages()global function
- Base class: All models inherit from
Base(defined indb.py) - Async: Use async SQLAlchemy (
AsyncSession,select(),await db.execute()) - Relationships: Use
selectinload()orjoinedload()for eager loading - Timestamps: Use
utc_now()helper forcreated_at/updated_atfields - Encryption: Use
get_fernet()helper for encrypted fields (e.g., OAuth tokens)
- Purpose: Business logic that doesn't belong in routers or models
- Examples:
GitHubService,DeploymentService,DomainService,LokiService - Dependency injection: Services should be created via dependency functions (e.g.,
get_github_service())
- Settings: All configuration via
Settingsclass inapp/config.py(PydanticBaseSettings) - Paths: Centralized in
Settings:data_dir:/var/lib/devpush(prod) or./data(dev)app_dir:/opt/devpush(prod) or project root (dev)upload_dir,traefik_dir,env_file,version_file: Derived fromdata_dir
- Environment variables: Loaded from
.envfile (path fromsettings.env_file) - Secrets: Stored in
.envfile, never committed to git
- Tool: Alembic (configured in
app/alembic.ini) - Location:
app/migrations/versions/ - Naming: Descriptive names (e.g.,
454328a03102_initial.py,87a893d57c86_allowlist.py) - Running: Use
scripts/db-migrate.sh(handles waiting for DB to be ready)
- Jobs worker:
app/workers/jobs.py- Handles async job queue - Monitor worker:
app/workers/monitor.py- Monitors deployment containers - Tasks:
app/workers/tasks/- Individual task implementations (deploy, cleanup, etc.) - Job queue: Access via
get_queue()dependency (returnsArqRedisconnection)
- Imports: Group by standard library, third-party, local
- Type hints: Use modern Python type hints (
str | Noneinstead ofOptional[str]) - Async/await: All database and HTTP operations should be async
- Error handling: Use FastAPI's
HTTPExceptionfor HTTP errors - Logging: Use
logging.getLogger(__name__)for module-level loggers - Comments: Minimize comments; code should be self-documenting
- Variable names: Keep them short and simple; don't rename existing functions/variables
-
Base files:
compose/base.yml: Main application stackcompose/override.yml: Production overridescompose/override.dev.yml: Development overridescompose/ssl-*.yml: Certificate/DNS provider-specific overrides
-
Naming: Use
runmode (notapporstack) for the main application stack -
Environment variables: Use
${VAR:-default}syntax in compose files -
Volumes:
- Application data:
${DATA_DIR:-../data}→/var/lib/devpush - Named volumes:
devpush-db,loki-data,alloy-data(for stateful services)
- Application data:
- Location:
docker/directory - App:
Dockerfile.app(prod) andDockerfile.app.dev(dev). Both acceptAPP_UID/APP_GIDbuild args (populated viaSERVICE_UID/SERVICE_GID) so the container user matches the host service user. - Runners:
docker/runner/Dockerfile.*(one per language/runtime) - Entrypoints:
entrypoint.*.shscripts for container initialization
- app: FastAPI application
- worker-jobs: Jobs background worker
- worker-monitor: Monitor background worker
- pgsql: PostgreSQL database
- redis: Redis cache/queue
- traefik: Reverse proxy and TLS termination
- loki: Log aggregation
- alloy: Telemetry agent (ships logs to Loki)
/
├── app/ # FastAPI application
│ ├── routers/ # Route handlers
│ ├── forms/ # WTForms definitions
│ ├── templates/ # Jinja2 templates
│ ├── services/ # Business logic services
│ ├── workers/ # Background workers
│ ├── utils/ # Utility functions
│ ├── migrations/ # Alembic migrations
│ └── ...
├── compose/ # Docker Compose files
├── docker/ # Dockerfiles and entrypoints
├── scripts/ # Shell scripts
│ ├── lib.sh # Shared library functions
│ ├── provision/ # Provisioning scripts
│ └── upgrades/ # Version upgrade hooks
└── data/ # Local dev data (gitignored)
-
Production:
- Code:
/opt/devpush - Data:
/var/lib/devpush - Config:
/var/lib/devpush - Env:
/var/lib/devpush/.env
- Code:
-
Development:
- Code: Project root
- Data:
./data - Config:
./data - Env:
./data/.env
-
Always use
settings.data_dir,settings.app_dir, etc. fromapp/config.pyrather than hardcoding paths
-
Branches:
main: Production branchdevelopment: Staging branchfeature/name: Feature branchesissue/123-name: Issue branches
-
PRs: Submit against
development, notmain -
Commits: Use conventional commits format:
type(scope): description- Types:
feat,fix,docs,refactor,test,chore - Example:
feat(scripts): add upgrade hooks for version-specific migrations
- Types:
- Python: Follow existing patterns; minimize comments; keep variable names short
- Shell: Use
printfinstead ofecho; preferrun_cmdfor operations; keep scripts consistent - No renaming: Don't rename existing functions/variables unless explicitly requested
- Secrets: Never commit secrets; use
.envfile (gitignored) - Sessions: Use signed cookies (Starlette
SessionMiddleware) - CSRF: Enabled via
CSRFProtectMiddleware - Input validation: Always validate user input (forms, query params, etc.)
- Scripts: Use
set -Eeuo pipefailand ERR traps - FastAPI: Use
HTTPExceptionfor HTTP errors; let exceptions bubble up for 500s - Logging: Log errors with appropriate level (ERROR, WARNING, INFO)
- Start stack:
scripts/start.sh(auto-detects dev mode) - Stop stack:
scripts/stop.sh - View logs:
scripts/compose.sh logs [service] - Run migrations:
scripts/db-migrate.sh - Clean data:
scripts/clean.sh
- Install:
scripts/install.sh(run as root/sudo) - Update:
scripts/update.sh(run as root/sudo) - Start/Stop:
systemctl start/stop devpush.serviceorscripts/start.sh/scripts/stop.sh - Status:
scripts/status.shorsystemctl status devpush.service
- Hetzner: Use
scripts/provision/hetzner.shto provision a server - Token: Prompt securely if not provided via
--tokenflag
- Location:
scripts/upgrades/X.Y.Z.sh - Naming: Use stable version numbers only (no prerelease suffixes)
- Execution: Hooks run if
current_version < hook_version <= target_version - Idempotent: All operations must be idempotent (safe to run multiple times)
- Consistency: Follow existing patterns; don't introduce new conventions without good reason
- Simplicity: Keep code simple and straightforward; avoid over-engineering
- Documentation: Update this file when introducing new patterns or conventions
- User experience: Scripts should provide clear feedback (spinners, success messages, error details)
- Security: Never expose secrets; validate all input; use secure defaults
Following these guidelines keeps the codebase maintainable and consistent. When in doubt, mirror existing patterns in similar files. If you need to deviate, explain why in a comment and update this file.