uv sync # deps → .venv
uv run pytest # tests (testcontainers → needs Docker/OrbStack)
uv run pytest --cov # coverage gate, fail_under 85, branch
uv run ruff check . && uv run ruff format --check .
uv run basedpyright # strict for src/
RIPTIDE_DB_URL=... uv run alembic upgrade head # / downgrade base
podman-compose up # Postgres + migrations + app on :8000docker ps fails → ask the user to start OrbStack.
- Append-only. Handlers
INSERT … ON CONFLICT (delivery_id) DO NOTHING. NeverUPDATE/DELETEevent rows.delivery_id= per-source dedup key, so retries are idempotent. - Raw payload always stored in
payload JSONB, whole body, even for fields already extracted into columns. Don't drop unused fields. riptide.jsonis config, not data. Teams + org-wide automation rules. Edits via PR, pod hot-reloads by mtime. Never move it into Postgres.- Team keys are a separate file, production-mounted from a Secret, never committed. Stored sha256, hot-reloaded. The bearer is the team identity — every webhook tagged
team = caller_team. - No
servicecolumn, noservice_idon the wire. Aggregate per source byrepo_full_name/pipeline_name/app_name/repo, org-wide byteam. Join identifiers are lowercased at ingest (commit_sha,revision,repo_full_name,branch_name,repo) → case-stable. It served only single-pane labelling and was dropped; never propose it again. - Metrics computed on read. No aggregation tables, no rollup jobs in v1. Schema additions preserve raw events.
- Correlation, in priority order. Bitbucket↔Pipeline:
commit_sha(App-repo SHA both sides, deterministic). Argo CD: the full image reference — senders reportpipeline_events.image_ref(registry/path:tag), Argo stores the same strings inpayload->'images'.argocd_events.revisionis the GitOps-repo SHA (four Apps of one service share one) and matches neither other source. Image tags are not SHAs — measured: 0 of 4 936 refs, all semver; never parse a SHA out of a tag. Pre-image_refrows: read-time fallback indocs/correlating-deploys-to-commits.md. Neverservice_idor name mappings. repo:refs_changedis ref movement, not developer activity. Measured: of 16 295 master-ref events ~15 000 were release tooling (maven/gradle release plugins, component-version job, Renovate); the 1 210 human-authored ones were merge commits already counted aspr:merged. Read activity andchange_typeoff PR events — amasterpush has no branch prefix, so change mix over all events reads 83 %other. Never infer intent from an event-type name; checkauthorand the commit message.- Lead time is per commit, against the first deploy that carried it — the
lead_time_changesview (overcommit_sightings+deploy_commit_ranges). Exclude merge commits and service-account commits; report bots as their own line, never blended (87 % of commits reaching prod were Renovate's). Never quote the newest-commit-per-release shortcut as lead time: measured 26.7 h against a real 193.6 h. change_typeon Bitbucket events only. Don't denormalise onto pipeline / Argo rows; join at read time.- Automation detection is config-last. Order: configured
automationauthors (matched against login and display name, case-insensitive) → acting user'stype == "SERVICE"from the payload →*-botname shape. Senders also declare themselves (reviewer_handle/actor_handle+ account kind, read-time filter). Only accounts nobody reports get a config entry.automationis org-wide, at the config root. - CI events are source-tagged, not source-routed. Every CI lands in
pipeline_eventsviaPOST /webhooks/pipeline, told apart bysource. No per-CI tables or endpoints. Dedup keysource#pipeline_name#run_id#phase. - Noergler carries finops + reviewer-precision only.
event_type∈pr_completed|feedback(historical rows: pre-0002completed). Never re-emit PR lifecycle —bitbucket_eventscovers open / merged / declined. Dedup keyspr_completed#<pr_key>#<outcome>,feedback#<finding_id>#<verdict>.pr_completedis also the source for PR diff size (Bitbucket webhooks carry none) and for the reviewer's own account. - Senders verify at startup via
GET /auth/ping— authenticated, returns the caller's team, so a wrong token fails fast. Never reuse/health(unauth liveness) or/ready(unauth readiness). modified_athas a Postgres trigger (riptide_set_modified_at), not just SQLAlchemyonupdate, so raw-SQL updates bump it too. Keep the trigger when changing migrations.- Database is external. Never add a Postgres Deployment to
openshift/.
- Layering. Routers: HTTP + auth + dispatch + config-derived fields + persist. Extraction:
parsers_<source>.py, pure functions returning a typed*EventDraft, no HTTP / DB / config. Keep JSON-coercion helpers beside the extractor using them. Never extract in a router. - Pass the config to a router only when it needs
automationrules or team metadata. - Schemas strict for
/webhooks/pipeline,/webhooks/argocd,/webhooks/noergler— we own those contracts, invalid payloads must 422. Bitbucket is permissive raw-dict parsing; its shapes vary. - Optional fields: accept
""as absent. A templated-but-unset param arrives empty far more often than missing, and rejecting it drops the whole event. - Coerce arbitrary JSON with the
_as_dict()/_as_list()helpers — basedpyright strict rejects chained.get()onOptional[dict]. - Pyright strict for
src/, standard fortests/andmigrations/. NoAnyleaks insrc/. - Single flat package
riptide_collector. Future suite components get their own top-level package. - Tests: real Postgres via testcontainers, never SQLite. Per-test truncation via the
session_factoryfixture. .pre-commit-config.yaml= ruff + basedpyright + uv-lock-check; CI enforces the same.
- One JSON object per line on stdout, auto-extracted by Splunk (
KV_MODE=json, sourcetyperiptide:collector:json). configure_logging()is the single entry point; stdlib loggers (uvicorn, sqlalchemy, alembic) are bridged through structlog. Never add handlers or re-initlogging.basicConfig.- Splunk-reserved kwargs are forbidden:
source,sourcetype,host,index,time,_time,_raw,event. CI vendor →ci_system, event name →msg, severity →log_level._strip_reservedis a safety net, not a licence. - Field names generic across sources (
event_type,status,phase,delivery_id,team,repo,commit_sha). Never pre-namespace with the source —webhook_sourcealready disambiguates. Namespace only on a genuine collision of meaning. - Exactly one
msg=webhook_processedper request:webhook_source∈ {bitbucket,pipeline,argocd,noergler},outcome∈ {accepted,deduped,ignored,skipped},delivery_id,team, plus source-specific fields. Includedelivery_ideven on ignored / skipped so triage has a key. outcome=dedupedcomes fromRETURNING delivery_id— aNonescalar means the row existed. Preserve when adding sources.- Persist failures:
try/except Exception: logger.exception("webhook_persist_failed", …); raise. Never swallow. - Access log binds
request_idto contextvars so every log in the request inherits it./healthand/readysilenced; uvicorn.access at WARNING. - Splunk
props.confis owned by the platform team; reference copy indocs/splunk-props.conf.
openshift/ is suite-level, one directory per component. New component → own openshift/<component>/kustomization.yaml, added to resources: in openshift/kustomization.yaml. Every container: explicit cpu+memory requests AND limits, no exceptions. runAsNonRoot: true, readOnlyRootFilesystem: true, never a fixed runAsUser — OpenShift assigns a random UID per project.
Push back unless the user is explicit:
- Change failure rate / failed deployment recovery time — no reliable incident source; schema leaves room for rollback-proxy detection
- Backfill workers (ingestion is forward-only)
- Aggregation API or metric endpoints (reads are SQL, or a future sibling component)
- Helm chart (Kustomize suffices)
- Postgres manifests