* chore(oss): isolate customer-specific deploy bits from scripts/grpn/ (#88) Vendor-neutralization step before public release. The directory mixed two concerns: (1) generic ops scripts referenced from mainline OSS infrastructure (TLS rotation, auto-upgrade cron) and (2) one operator's hackathon manual-deploy helper with hardcoded GCP project IDs, VM names, and admin emails. Splitting them per concern. Moved (still in OSS, just under a vendor-neutral name): - scripts/grpn/agnes-tls-rotate.sh → scripts/ops/agnes-tls-rotate.sh - scripts/grpn/agnes-auto-upgrade.sh → scripts/ops/agnes-auto-upgrade.sh Removed (belongs in private consumer infra repos, not upstream OSS): - scripts/grpn/Makefile (hardcoded prj-grp-foundryai-dev-7c37, foundryai-development VM name, e_zsrotyr@groupon.com bootstrap email) - scripts/grpn/README.md (GRPN hackathon deploy walkthrough) - docs/superpowers/plans/2026-04-22-grpn-deploy-learnings.md (org-specific deploy log) Cross-refs updated in README.md, CLAUDE.md, docs/DEPLOYMENT.md, docker-compose.yml. CHANGELOG entry flags BREAKING (ops) for any consumer infra repo that installs these scripts via path-based systemd timers. This is the first wave of #88 — the remaining leaks (test data with prj-grp-dataview-prod-1ff9, AIAgent.FoundryAI tags in OpenMetadata test fixtures, docstrings in connectors/openmetadata/enricher.py) will be a separate, smaller PR. Refs #88. * chore(oss): comprehensive vendor-neutralization (#88 wave 2 + review fixes) PR #94 review found that the original wave-1 grep was scoped wrong and many leaks survived. This commit closes wave 1 properly AND folds in all wave-2 anonymization in a single pass — easier to review than two PRs. Wave-1 review-fix corrections: - Caddyfile: scripts/grpn/agnes-tls-rotate.sh → scripts/ops/ (the original wave-1 grep filter excluded extensionless files like Caddyfile). - CHANGELOG bullet rewritten — original wording implied an in-repo migration for infra/modules/customer-instance/, which is wrong (the TF module embeds the script inline via heredoc, never sourced from scripts/grpn/). Now flags downstream consumer infra repos only. - infra/modules/customer-instance/variables.tf: Czech docstring with `grpn` example → English description with `acme, example` placeholders. Wave-2 anonymization: - Code docstrings (connectors/openmetadata/{client,transformer,enricher}.py, src/catalog_export.py, scripts/duckdb_manager.py): prj-grp-… → my-bq-project / prj-example-1234, AIAgent.FoundryAI → AIAgent.MyAgent, FoundryAIDataModel → AnalyticsDataModel. - Test fixtures (4 files): same set of replacements — 157 tests still pass. - .github/workflows/keboola-deploy.yml: "Groupon-side dev VMs" comment → generic "per-developer dev VMs". - docs/auth-groups.md + scripts/debug/probe_google_groups.py: kids-ai-data-analysis project name → acme-internal-prod placeholder. - 5 planning/spec docs under docs/superpowers/{plans,specs}/2026-04-21-*: hardcoded IPs (34.77.94.14, 34.77.102.61) → <dev-vm-ip>/<prod-vm-ip>; GRPN/Groupon → Acme/another-customer; prj-grp-… → prj-example-…. - scripts/switch-dev-vm.sh deleted — hackathon-era helper hardcoded to a specific shared dev VM. Per-developer dev VMs are the supported pattern. Final grep `groupon|grpn|foundryai|prj-grp|groupondev|34\.77\.(94|102)\.…|kids-ai-data` returns zero hits (excluding CHANGELOG.md historical entries). CHANGELOG entry expanded to document both waves under one bullet, with the BREAKING (ops) clarification about the TF module being unaffected. Refs review of #94, closes #88. * fix(oss): close remaining #94 review-2 findings (Czech, padak refs, CHANGELOG) Reviewer of PR #94 round 2 caught 4 remaining items the wave-2 pass missed: 1. infra/modules/customer-instance/variables.tf had Czech descriptions on 8 more variables. Previous review only flagged line 19; this round audited the rest. Translated lines 2, 28, 42-46 (heredoc), 60, 65, 71, 78, 84 to English. Same review concern: a Terraform module that is the customer-facing API surface in Czech is unfit for OSS distribution. 2. infra/modules/customer-instance/outputs.tf had Czech descriptions on four outputs. Same fix. 3. docs/padak-security.md referenced a private repo (padak/keboola_agent_cli#206) in two places. Replaced with generic 'tracked upstream in the auth-CLI repo' per CLAUDE.md vendor-agnostic rule (no cross-refs to private repos). 4. scripts/fetch-env-from-secrets.sh:41 had a Czech comment. Translated. 5. CHANGELOG cosmetic: bullet said 'AIAgent.FoundryAI -> AIAgent.MyAgent' but the actual code uses both MyAgent (in docstrings) and Example (in test fixtures). Reworded to mention both targets. Final grep across all shipping file types (.md, .py, .yml, .yaml, .sh, Makefile, .json, .tf, .tpl, Caddyfile, .toml) for groupon|grpn|foundryai| prj-grp|groupondev|34.77.94.14|34.77.102.61|kids-ai-data|padak/keboola_agent_cli returns ZERO hits (excluding CHANGELOG.md). Czech-diacritic grep across .tf/.toml/Caddyfile/Makefile/.yml returns ZERO hits. 157/157 OpenMetadata + DuckDB tests still pass. * fix(oss): close #94 round-3 leaks (env.template, instance.yaml.example, padak typo) Round-3 reviewer caught two MUST-FIX leaks the round-2 grep missed (grep was scoped to extensions that did not include .template / .example suffixes — the audit was right, the previous grep was not paranoid enough): 1. config/instance.yaml.example:114 — '(optional - Groupon-specific)' brand leak in a shipping config example. Replaced with '(optional)'. 2. config/.env.template:68 — stale path 'scripts/grpn/agnes-tls-rotate.sh' in operator-facing env-template comment. The script lives at scripts/ops/ now (commit 16a85cc); this comment had been pointing operators at a non-existent path. 3. docs/padak-security.md:188 — phrase duplication 'tracked in tracked upstream' from a sloppy substitution in round-2. Trivial wording fix. Final paranoid grep across .md/.py/.yml/.yaml/.sh/Makefile/.json/.tf/.tpl/ Caddyfile/.toml/.template/.example/.env* with the full token set (groupon|grpn|foundryai|prj-grp|groupondev|34\.77\.94\.14|34\.77\.102\.61| kids-ai-data|padak/keboola_agent_cli) returns ZERO hits, excluding CHANGELOG.md historical entries. * fix(oss): #94 round-4 — QUICKSTART.md + rename padak-security.md Devin Review caught two findings on the latest round-3 commit: 1. docs/QUICKSTART.md:67 still pointed users at the deleted scripts/switch-dev-vm.sh. A Quickstart user following step-by-step would hit a missing-file error at the final step. Replaced with the inline gcloud-ssh equivalent that the Removed bullet documents. 2. docs/padak-security.md filename retains the personal identifier 'padak'. The PR fixed the body content (replaced padak/keboola_agent_cli#206 references with generic wording) but missed the filename. Renamed to docs/security-audit-2026-04.md (date-anchored, vendor-neutral). Updated the historical CHANGELOG link to point at the new path with an inline note about the rename. * fix(oss): redact remaining hardcoded IPs from planning docs + remove default email Devin Review caught two more leaks: 1. scripts/fetch-env-from-secrets.sh line 16 had a hardcoded personal-email default (zdenek.srotyr@keboola.com). Replaced with ':?' bash error so SEED_ADMIN_EMAIL must be explicitly set — safer than carrying any specific identity. 2. Planning docs still had 35.195.96.98 and 34.62.223.189 (legacy prod/dev IPs) that the round-1 IP-replace pattern missed (it only targeted 34.77.x.x). Generic regex redaction across all five planning docs replaces every public IP with <redacted-ip>, preserving private/loopback/IAP ranges.
304 lines
17 KiB
Markdown
304 lines
17 KiB
Markdown
# AI Data Analyst
|
|
|
|
Open-source data distribution platform for AI analytical systems. Extracts data from sources into DuckDB, serves via FastAPI, and distributes parquets to analysts who use Claude Code for local analysis.
|
|
|
|
## First-Time Setup
|
|
|
|
When a user opens this project for the first time, guide them through interactive setup:
|
|
|
|
### Step 1: Gather Information
|
|
Ask the user for:
|
|
1. Company domain (e.g., "acme.com") - used for Google OAuth
|
|
2. Data source type: keboola / bigquery / csv
|
|
3. Instance name (e.g., "Acme Data Analyst")
|
|
|
|
### Step 2: Generate Configuration
|
|
1. Copy `config/instance.yaml.example` to `config/instance.yaml`
|
|
2. Fill in values from Step 1
|
|
3. If Keboola: ask for Storage API token, stack URL, project ID
|
|
4. Create `.env` from `config/.env.template`
|
|
|
|
### Step 3: Register Tables
|
|
1. Use the FastAPI admin API (`POST /api/admin/tables/{id}`) or webapp UI to register tables
|
|
2. Tables are stored in DuckDB `table_registry` with source_type, bucket, source_table, query_mode
|
|
3. For migration from old format: `python scripts/migrate_registry_to_duckdb.py`
|
|
|
|
### Step 4: Docker Deployment
|
|
```bash
|
|
docker compose up # Start app + scheduler
|
|
docker compose --profile full up # Include telegram bot
|
|
|
|
# HTTPS mode — Caddy + corporate-CA certs at /data/state/certs
|
|
docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.tls.yml \
|
|
--profile tls up -d
|
|
```
|
|
|
|
See `docs/DEPLOYMENT.md` → **TLS** for cert provisioning + `scripts/ops/agnes-tls-rotate.sh` (daily refetch from `TLS_FULLCHAIN_URL`, `SIGUSR1` reload on diff, no-op when unchanged). The infra repo's `startup.sh` installs this as a systemd timer automatically.
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
├── src/ # Core engine
|
|
│ ├── db.py # DuckDB schema (system.duckdb, analytics.duckdb)
|
|
│ ├── orchestrator.py # SyncOrchestrator — ATTACHes extract.duckdb files
|
|
│ ├── repositories/ # DuckDB-backed CRUD (sync_state, table_registry, users, etc.)
|
|
│ ├── profiler.py # Data profiling
|
|
│ └── catalog_export.py # OpenMetadata catalog export
|
|
├── app/ # FastAPI application
|
|
│ ├── main.py # App setup, router registration
|
|
│ ├── api/ # REST API (sync, data, catalog, admin, auth)
|
|
│ └── web/ # HTML dashboard routes
|
|
├── connectors/ # Data source connectors (extract.duckdb contract)
|
|
│ ├── keboola/ # Keboola: extractor.py (DuckDB extension) + client.py (fallback)
|
|
│ ├── bigquery/ # BigQuery: extractor.py (remote-only via DuckDB BQ extension)
|
|
│ └── jira/ # Jira: webhook + incremental parquet → extract.duckdb
|
|
├── cli/ # CLI tool (`da sync`, `da query`, `da admin`)
|
|
├── app/auth/ # Authentication (FastAPI-based providers)
|
|
├── services/ # Standalone services (scheduler, telegram_bot, ws_gateway, etc.)
|
|
├── server/ # Legacy deployment infrastructure
|
|
├── scripts/ # Utility + migration scripts
|
|
├── config/ # Configuration templates (instance.yaml.example)
|
|
├── docs/ # Documentation + metric YAML definitions
|
|
└── tests/ # Test suite (633 tests)
|
|
```
|
|
|
|
## Architecture: extract.duckdb Contract
|
|
|
|
Every data source produces the same output:
|
|
```
|
|
/data/extracts/{source_name}/
|
|
├── extract.duckdb ← _meta table + views
|
|
└── data/ ← parquet files (local sources only)
|
|
```
|
|
|
|
### Remote table support (`_remote_attach`)
|
|
|
|
Extractors with remote/passthrough tables (query_mode='remote') include a `_remote_attach` table
|
|
in extract.duckdb so the orchestrator can re-ATTACH the external DuckDB extension at query time:
|
|
|
|
```sql
|
|
CREATE TABLE _remote_attach (
|
|
alias VARCHAR, -- DuckDB alias used in views, e.g. 'kbc'
|
|
extension VARCHAR, -- Extension name, e.g. 'keboola'
|
|
url VARCHAR, -- Connection URL
|
|
token_env VARCHAR -- Env-var name holding the auth token (NOT the token itself)
|
|
);
|
|
```
|
|
|
|
The orchestrator reads this table, installs/loads the extension, reads the token from the
|
|
environment, and ATTACHes the external source. Views referencing `kbc."bucket"."table"` then
|
|
resolve correctly. This mechanism is generic — any connector can use it.
|
|
|
|
The SyncOrchestrator scans `/data/extracts/*/extract.duckdb`, ATTACHes each into master `analytics.duckdb`, and creates views.
|
|
|
|
```
|
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
│ Keboola │ │ BigQuery │ │ Jira │
|
|
│ extractor │ │ extractor │ │ webhooks │
|
|
│ (DuckDB ext) │ │ (remote BQ) │ │ (incremental)│
|
|
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
|
│ │ │
|
|
▼ ▼ ▼
|
|
extract.duckdb extract.duckdb extract.duckdb
|
|
+ data/*.parquet (views → BQ) + data/*.parquet
|
|
│ │ │
|
|
└─────────────────┼─────────────────┘
|
|
▼
|
|
SyncOrchestrator.rebuild()
|
|
ATTACH → master views in analytics.duckdb
|
|
│
|
|
┌──────────┼──────────┐
|
|
▼ ▼ ▼
|
|
FastAPI CLI
|
|
(serve) (da sync)
|
|
```
|
|
|
|
Three source types:
|
|
- **Batch pull** (Keboola): DuckDB extension downloads to parquet, scheduled
|
|
- **Remote attach** (BigQuery): DuckDB BQ extension, no download, queries go to BQ
|
|
- **Real-time push** (Jira): Webhooks update parquets incrementally
|
|
|
|
## Configuration
|
|
|
|
Instance-specific config: `config/instance.yaml` (see example).
|
|
Environment variables: `.env` (never committed).
|
|
Table definitions: DuckDB `table_registry` table in `system.duckdb`.
|
|
|
|
## Development
|
|
|
|
```bash
|
|
# Setup
|
|
python3 -m venv .venv && source .venv/bin/activate
|
|
uv pip install ".[dev]"
|
|
|
|
# Run FastAPI locally
|
|
uvicorn app.main:app --reload
|
|
|
|
# Run tests
|
|
pytest tests/ -v
|
|
|
|
# Trigger sync manually
|
|
curl -X POST http://localhost:8000/api/sync/trigger
|
|
|
|
# Docker
|
|
docker compose up
|
|
```
|
|
|
|
## Business Metrics
|
|
|
|
Standardized metric definitions live in DuckDB (`metric_definitions` table). Import starter pack:
|
|
|
|
```bash
|
|
da metrics import docs/metrics/
|
|
```
|
|
|
|
### For AI agents analyzing data:
|
|
Before computing any business metric, look up the canonical definition:
|
|
1. `da metrics list` — find the relevant metric
|
|
2. `da metrics show revenue/mrr` — read the SQL and business rules
|
|
3. Use the SQL from the metric definition, adapt to the specific question
|
|
|
|
Never invent metric calculations — always use the canonical definitions.
|
|
|
|
## Hybrid Queries (BigQuery + Local)
|
|
|
|
For tables too large to sync locally, use hybrid queries that JOIN local data with on-demand BigQuery results:
|
|
|
|
```bash
|
|
da query --sql "SELECT o.*, t.views FROM orders o JOIN traffic t ON o.date = t.date" \
|
|
--register-bq "traffic=SELECT date, SUM(views) as views FROM dataset.web WHERE date > '2026-01-01' GROUP BY 1"
|
|
```
|
|
|
|
The `--register-bq` flag executes a BigQuery subquery, loads the result into memory, and makes it available as a DuckDB view for the final SQL. Multiple `--register-bq` flags can be used for multiple BQ sources.
|
|
|
|
For complex SQL, use stdin mode:
|
|
```bash
|
|
echo '{"register_bq": {"traffic": "SELECT ..."}, "sql": "SELECT ..."}' | da query --stdin
|
|
```
|
|
|
|
## Extensibility
|
|
|
|
### Data Sources (extract.duckdb contract)
|
|
New connector = `connectors/<name>/extractor.py` producing `extract.duckdb + data/`.
|
|
Must create `_meta` table with columns: table_name, description, rows, size_bytes, extracted_at, query_mode.
|
|
Orchestrator ATTACHes it automatically.
|
|
|
|
### Authentication
|
|
Auth providers in `app/auth/` (FastAPI-based):
|
|
- **Google**: OAuth via Google (Workspace group memberships pulled at sign-in — see `docs/auth-groups.md` for the GCP setup checklist + the `security` label gotcha)
|
|
- **Email**: Email magic link (itsdangerous token)
|
|
- **Desktop**: JWT for API
|
|
|
|
### RBAC (role-based access control)
|
|
|
|
Three-layer model: external Cloud Identity groups → admin-curated `group_mappings` → internal roles (`internal_roles` table) → resolved into `session["internal_roles"]` at sign-in OR fetched from `user_role_grants` per request for PAT/headless callers. `core.*` roles (viewer/analyst/km_admin/admin) carry the legacy hierarchy via `implies` JSON; module authors register their own keys (e.g. `corporate_memory.curator`) at import time and gate endpoints with `Depends(require_internal_role("<key>"))`.
|
|
|
|
**Contributors building a new module or capability — read [`docs/RBAC.md`](docs/RBAC.md) before adding endpoints.** It covers: picking a role key (naming convention, namespace), `register_internal_role` lifecycle, gating with `require_internal_role` vs. the `require_admin` / `require_role(Role.X)` thin wrappers, declaring implies hierarchies inside your module, the `_hydrate_legacy_role` shim that keeps `user["role"]` reads working, and the admin workflows (UI / CLI / REST) for binding groups and granting roles. Quickstart sections by audience: operator, end-user, module author.
|
|
|
|
## Release & deploy workflows
|
|
|
|
Two separate release.yml-style workflows produce GHCR images. Pick the one that matches what you're shipping.
|
|
|
|
### `release.yml` — auto-build on every push
|
|
Runs on **every** push to **every** branch.
|
|
- Push to `main` → `:stable`, `:stable-YYYY.MM.N` (CalVer).
|
|
- Push to non-main `<prefix>/<branch>` → `:dev`, `:dev-YYYY.MM.N`, `:dev-<branch-slug>`, and (when prefix isn't a Git Flow convention) `:dev-<prefix>-latest` alias.
|
|
|
|
VMs that pin to a floating tag (`:dev`, `:dev-<prefix>-latest`) auto-upgrade within ~5 min via the cron in `agnes-auto-upgrade.sh`. Convenient for per-developer dev VMs; **footgun for shared dev VMs** (last pusher wins, regardless of who).
|
|
|
|
### `keboola-deploy.yml` — tag-triggered, explicit deploy only
|
|
Runs **only** on git tags matching `keboola-deploy-*`. Publishes:
|
|
- `:keboola-deploy-<git-tag-suffix>` — immutable, tied to the exact commit
|
|
- `:keboola-deploy-latest` — floating alias the consumer pins to
|
|
|
|
**Operator workflow:**
|
|
```bash
|
|
git checkout <commit-or-branch>
|
|
git tag keboola-deploy-<descriptive-name>
|
|
git push origin keboola-deploy-<descriptive-name>
|
|
# → workflow builds + publishes both tags
|
|
# → VM cron picks up :keboola-deploy-latest within ~5 min
|
|
# → manual cron trigger (skip the wait): sudo /usr/local/bin/agnes-auto-upgrade.sh on the VM
|
|
```
|
|
|
|
Use this when the consumer (e.g. a customer dev VM) needs **deploy-when-I-decide** semantics — no surprise rollouts from upstream branch pushes by other contributors. The infra repo pins `image_tag = "keboola-deploy-latest"` on the relevant VM.
|
|
|
|
### Module versioning
|
|
The customer-instance Terraform module under `infra/modules/customer-instance/` is published as `infra-vMAJOR.MINOR.PATCH` git tags (separate from app CalVer tags). Bump on any module-API change; downstream infra repos pin to the tag in their `source = "github.com/keboola/agnes-the-ai-analyst//infra/modules/customer-instance?ref=infra-v1.X.Y"`.
|
|
|
|
After merging a module change to `main`:
|
|
```bash
|
|
git tag infra-vX.Y.Z origin/main
|
|
git push origin infra-vX.Y.Z
|
|
```
|
|
|
|
### Replacing a VM after a startup-script change
|
|
Module sets `lifecycle { ignore_changes = [metadata_startup_script] }` on `google_compute_instance.vm` so normal `terraform apply` doesn't churn running VMs. To propagate a startup-script update, trigger the consumer's apply workflow manually with the VM resource address — typical workflow_dispatch input is `recreate_targets='module.agnes.google_compute_instance.vm["<vm-name>"]'`.
|
|
|
|
## Key Implementation Details
|
|
|
|
### DuckDB Schema (src/db.py)
|
|
- Schema v9 with auto-migration v1→…→v9 (v5 adds `users.active`, v6 adds `personal_access_tokens`, v7 adds `personal_access_tokens.last_used_ip`, v8 adds `internal_roles` + `group_mappings`, v9 adds `user_role_grants` + `internal_roles.implies/is_core` and seeds `core.*` hierarchy from legacy `users.role`)
|
|
- `table_registry`: id, name, source_type, bucket, source_table, query_mode, sync_schedule, etc.
|
|
- `sync_state`, `sync_history`: track extraction progress
|
|
- `users`, `dataset_permissions`, `audit_log`: auth + RBAC
|
|
- System DB at `{DATA_DIR}/state/system.duckdb`
|
|
- Analytics DB at `{DATA_DIR}/analytics/server.duckdb`
|
|
|
|
### SyncOrchestrator (src/orchestrator.py)
|
|
- `rebuild()`: scans extracts dir, ATTACHes all, creates master views, updates sync_state
|
|
- `rebuild_source(name)`: single source (used after Jira webhooks)
|
|
- Thread-safe via `_rebuild_lock`
|
|
|
|
### Connector Pattern
|
|
- **Keboola**: `connectors/keboola/extractor.py` uses DuckDB Keboola extension, fallback to `client.py`
|
|
- **BigQuery**: `connectors/bigquery/extractor.py` uses DuckDB BQ extension (remote-only, no download)
|
|
- **Jira**: `connectors/jira/webhook.py` → `incremental_transform.py` → `extract_init.py` updates `_meta`
|
|
- `connectors/keboola/client.py`: legacy Keboola Storage API wrapper (kept as fallback)
|
|
|
|
### Config Loading
|
|
1. `config/loader.py` loads `instance.yaml`
|
|
2. `app/instance_config.py` exposes `get_data_source_type()`, `get_value()`
|
|
3. Table config lives in DuckDB `table_registry` (not markdown files)
|
|
|
|
### Files NOT to modify (stable infrastructure)
|
|
- `connectors/jira/file_lock.py` - Advisory file locking
|
|
- `connectors/jira/transform.py` - Core Jira transform logic
|
|
- `services/ws_gateway/` - WebSocket notification gateway
|
|
|
|
## Vendor-agnostic OSS — no customer-specific content
|
|
|
|
This repo is the public OSS distribution. **Nothing customer-specific belongs in code, configuration defaults, comments, docs, commit messages, PR titles, or PR bodies.** That includes:
|
|
|
|
- Specific deployments or brands (private VM names, internal product brands, organization names that aren't already public sponsors).
|
|
- Cloud project IDs, internal hostnames, runbook paths from a particular install (`/opt/<deployment>`, `<host>.<internal-domain>`, `prj-<org>-…`, internal SA emails).
|
|
- Cross-references to private repos (`<private-org>/<private-repo>#NN`). Describe the integration in generic terms or link to public examples instead.
|
|
|
|
When you motivate a change, frame it abstractly ("behind a TLS-terminating reverse proxy", "in containerized deploys") rather than naming a specific operator. When you show examples, use placeholders (`example.com`, `<your-host>`, `<install-dir>`). When config has reasonable defaults pulled from one deployment's habits, generalize them or surface them as documented examples — not hard-coded assumptions.
|
|
|
|
Customer-specific automation, hostnames, and identities live in private infra repos that *consume* this OSS. The OSS describes capabilities, defaults, and configuration knobs — not how a specific operator wired them up.
|
|
|
|
## Changelog discipline — non-negotiable
|
|
|
|
**Every PR that adds, removes, or changes user-visible behavior MUST update `CHANGELOG.md` in the same PR.** No exceptions, no follow-ups, no "I'll do it after merge". User-visible = anything an operator, end-user, or downstream integrator can observe: CLI flags / output / exit codes, REST endpoints / payloads / status codes, web UI, `instance.yaml` schema, env vars, `extract.duckdb` contract, Docker / compose / Caddyfile knobs, default behaviors, breaking changes, security fixes.
|
|
|
|
**How:**
|
|
- Add a bullet under the topmost `## [Unreleased]` heading (create one if missing — it sits above the latest released version).
|
|
- Group by `### Added` / `### Changed` / `### Fixed` / `### Removed` / `### Internal` (Keep-a-Changelog sections).
|
|
- Mark breaking changes with `**BREAKING**` at the start of the bullet — operators grep for that string before bumping the pin.
|
|
- Reference the relevant doc/runbook if one exists (e.g. `see docs/auth-groups.md`), don't restate it.
|
|
- Internal-only changes (refactors, test additions, dependency bumps without behavior change) go under `### Internal` — still log them, just keep them terse.
|
|
|
|
**When you cut a release:**
|
|
- Rename `## [Unreleased]` → `## [X.Y.Z] — YYYY-MM-DD`.
|
|
- Append a new empty `## [Unreleased]` section at the top so the next PR has somewhere to land.
|
|
- Bump `version` in `pyproject.toml` to match `X.Y.Z`.
|
|
- Tag the merge commit as `vX.Y.Z` and push the tag.
|
|
|
|
**If you find yourself opening a PR without a CHANGELOG entry, stop and add one before requesting review.** Reviewers should bounce PRs that touch user-visible behavior without a changelog update — same way they'd bounce a PR with no test changes for new logic.
|
|
|
|
## Git Commits & Pull Requests
|
|
|
|
- Keep commit messages clean and concise
|
|
- Do not include AI attribution in commits or PRs
|
|
- Before opening a PR, scan the diff and the PR body for the customer-specific tokens listed above (`grep -niE '<token1>|<token2>|...'`). If anything matches, generalize or remove it.
|