BrownRook Ledger operations runbook¶
This runbook covers the normal build, GitOps deployment, database migration, and receipt-processing verification workflow.
Local setup and tests¶
See local macOS development for native PostgreSQL and RabbitMQ, authentication proxies, and background receipt services.
Activate the project environment:
source .venv/bin/activate
python -m pip install -e '.[dev,db]'
The install creates the primary ledger command. The former brownrook
command remains a compatibility alias. Re-run the editable install after
pulling a CLI-entry-point change so the virtual environment creates the new
executable.
Application commands accept DATABASE_URL and, where documented by --help,
HOME_BUDGET_PG_DSN; use DATABASE_URL consistently in shared environments.
Tests use the isolated home_budget_test
database and must not point at the normal home_budget database because test
setup drops and rebuilds schemas.
Run unit tests:
make test
For quick feedback before pushing, install the documentation extra once with
python -m pip install -e '.[docs]', then run:
make check-local
This runs unit tests, builds a Python wheel in dist/ using the installed build
backend, and builds the documentation with strict validation. Model unit tests
use mocked GPU polling; dedicated telemetry tests use fake HTTP responses.
make build-local builds just the wheel. These checks do not run live models or
deploy to Kubernetes. The wheel does not replace the production Linux container:
CI still runs integration and configuration checks, builds and pushes the
immutable image, and updates the GitOps image reference before Argo CD can sync it.
With Colima running, validate the exact Prometheus, OpenTelemetry Collector, Alloy, and Fluent Bit configurations used by CI:
make test-observability
Run database integration tests:
make test-db
The Make integration targets use TEST_RABBITMQ_URL when explicitly set and
otherwise fall back to RABBITMQ_URL. RabbitMQ integration tests skip when
neither variable is exported. With the local development environment loaded,
include them with:
set -a
source .env.dev
set +a
make test-db
The tests use unique test.<uuid> queues and delete them afterward. Set an
explicit TEST_RABBITMQ_URL when the test broker differs from the development
broker.
Initialize or upgrade the runtime database locally:
ledger database setup
The container sets HOME_BUDGET_SQL_DIR=/opt/app-root/src/sql; local execution
uses the repository sql/ directory.
The Dockerfile installs third-party dependencies before application source is
copied. Source-only builds therefore reuse the dependency layer; changes to
pyproject.toml intentionally invalidate it.
Make targets¶
Run make help for a compact list. The repository provides these targets:
| Target | Purpose |
|---|---|
make docs-build |
Build the online manual locally and fail on documentation warnings. |
make docs-serve |
Serve the online manual locally with live reload. |
make test |
Run unit tests without external-service integration tests. |
make check-local |
Run unit tests, build a local Python wheel, and build strict documentation. |
make build-local |
Build a Python wheel in dist/ with the installed build backend. |
make test-observability |
Validate Prometheus, OpenTelemetry Collector, Alloy, and Fluent Bit configurations with containers provided by Colima or Docker. |
make test-db-setup |
Create the disposable test database if needed, then rebuild its schemas. |
make test-db |
Rebuild the disposable test schemas and run integration tests, including RabbitMQ when a broker URL is exported. |
make test-db-verbose |
Run database setup and integration tests with full PostgreSQL output. |
make test-rabbit |
Rebuild the disposable test schemas, then run the five RabbitMQ integration tests with their names displayed. |
make test-all |
Run unit tests followed by PostgreSQL and available RabbitMQ integration tests; the integration summary displays their names. |
make test-receipts |
Copy real receipt inputs into an isolated workspace and process them against the disposable test database. |
make dev-up |
Verify native PostgreSQL from .env.dev and start Caddy and OAuth2 Proxy in Colima. |
make dev-down |
Stop the authentication proxy containers; native PostgreSQL and RabbitMQ stay running. |
make dev-web |
Run the Ledger web application against the local services. |
make dev-cert-install |
Install the local Caddy certificate authority in the macOS system keychain. |
make dev-db-reset |
Rebuild the local development database configured in .env.dev. |
make dev-receipts-start |
Enable and start the Mac background OCR worker and result collector. |
make dev-receipts-status |
Check background service state and process IDs. |
make dev-receipts-logs |
Follow local worker and collector log files. |
make dev-receipts-restart |
Restart both services to reload code and .env.dev. |
make dev-receipts-stop |
Stop both services and disable startup at login. |
make status |
Report deployment and recent workflow status. |
make enrich-products |
Start and follow a one-off Kubernetes product-enrichment job. |
make ollama-gitops-check / make ollama-gitops-apply |
Review/reconcile Arsene's Ollama LAN listener and source-restricted firewalld access. |
make enrich-products COMPARE=1 LIMIT=10 RECEIPT='receipt.pdf' |
Compare OpenAI and Qwen matches without changing accepted products; see model comparison. |
make enrich-products COLLABORATE=1 LIMIT=1 RECEIPT='receipt.pdf' |
Save a Qwen-first collaborative recommendation for review in Ledger. Paid fallback is opt-in with PAID_FALLBACK=1. |
make enrich-products COLLABORATE=1 LIMIT=1 DRY_RUN=1 |
Validate the Kubernetes Job without creating it or calling providers. |
make enrich-products COLLABORATE=1 LIMIT=1 REFRESH=1 |
Deliberately include accepted items and refresh Brave search evidence. Default runs skip accepted items and reuse persistent search results. |
make receipts-graph |
Refresh the Neo4j projection from retained PostgreSQL evidence, including recommendations and acceptance events. |
make receipts-publish NODE=longbow |
Start and follow a receipt publisher Job on Longbow using the live receipt-processor CronJob. Omit NODE to let Kubernetes select a node. |
make receipts-worker-test NODE=longbow |
Run a temporary OCR consumer on Longbow using the live receipt-worker Deployment configuration. Ctrl+C deletes the Job and initiates graceful worker shutdown. |
make deploy-k3s |
Apply PostgreSQL credentials and the OCR cache path from .env.k3s, then sync the Argo CD application. Matching settings are reused. |
make k3s-config-check |
Validate the K3s OCR cache setting in .env.k3s without cluster access. |
make k3s-config-apply |
Update receipt-runtime-config from .env.k3s and restart Deployment clients when the cache path changes. |
make postgres-config-check |
Validate PostgreSQL deployment settings in .env.k3s without cluster access. |
make postgres-config-apply |
Provision postgres-secret from .env.k3s for a new deployment. |
make postgres-password-rotate |
Rotate an existing database password and Secret, verify authentication, and restart database clients. |
Neo4j runs on Arsene as the neo4j StatefulSet. Its GitOps manifest reserves
4 GiB of memory and permits up to 6 GiB, with a 2 GiB Java heap, 1 GiB page cache,
and a 1 GiB aggregate transaction-memory budget. Transaction state shares the heap;
the remaining container allowance covers native memory and runtime overhead.
The projector commits each receipt separately, so more receipts increase total
work and storage rather than combining all receipts into one transaction.
This budget raises the previous 358.4 MiB transaction ceiling; it does not
guarantee that arbitrarily large evidence histories fit in one receipt transaction.
After syncing a memory-budget change, wait for Neo4j before rerunning projection:
make deploy-k3s
kubectl --context brownrook-k3s1 -n home-budget rollout status statefulset/neo4j --timeout=300s
make receipts-graph
Both manual receipt targets default to context brownrook-k3s1 and namespace
home-budget; override with KUBE_CONTEXT and KUBE_NAMESPACE. Add DRY_RUN=1
to validate the generated Job with the API server without creating it. The
publisher queues eligible receipts; it does not run OCR itself. The worker test
consumes real queued receipts continuously, including result publication to the
collector. It waits if the queue is empty, so start it before publishing work.
Stopping the test allows the current receipt to finish within the deployed
termination grace period. Publisher Jobs expire 24 hours after completion.
To queue a full OCR refresh for exactly one receipt, including a completed receipt, specify its path relative to the receipt inbox:
make receipts-publish NODE=longbow RECEIPT='2026-08-14/receipts_20260814_0001.pdf'
This replaces that receipt's saved OCR and extraction results after processing.
The shared queue allows any active worker to claim it; NODE selects the
publisher's node. Watch the Longbow worker logs to confirm where OCR runs.
The test-db* targets destroy and recreate schemas only in TEST_DB, which
defaults to home_budget_test. make test-receipts also recreates
RECEIPT_TEST_ROOT, which defaults to .receipt-test.
Install the documentation dependencies with
python -m pip install -e '.[docs]'. Pull requests build the manual with strict
validation. Merges to main publish it to GitHub Pages at
https://larocquemb.github.io/home-budget-pipeline/.
For private-LAN deployments, select either deploy/private-lan or
deploy/rabbitmq-private after satisfying the DNS, TLS, Entra callback,
Traefik, and firewall prerequisites in the
network access guide.
Kubernetes logs are collected with the external Method B collector:
namespace-scoped API access from Alloy on monitoring.idc.brownrook.net, then
verified TLS delivery to Loki. The opt-in Method A overlay adds a node-level
Fluent Bit DaemonSet for side-by-side comparison without changing application
logging. See Kubernetes log forwarding for credential and
mTLS provisioning, local dual-path tests, rollout, validation, metrics, rotation,
and rollback procedures. The monitoring host, its Loki and Tempo services, and
the Kubernetes logging and telemetry TLS Secrets are reconciled from the
Git-controlled ops/monitoring Ansible playbook with
make monitoring-gitops-check and make monitoring-gitops-apply; all secret
inputs stay outside Git.
Receipt traces and metrics are available through the opt-in
deploy/rabbitmq-private-telemetry overlay. It adds an mTLS OpenTelemetry
Collector without changing the default deployment path. See
Receipt-processing OpenTelemetry for the repeatable
Intermediate-CA YubiKey issuance and rotation ceremony, PKI inputs,
configuration, deployment, live correlation, failure testing, and rollback.
For initial issuance, insert the Intermediate CA YubiKey and run
scripts/issue_telemetry_certificates.sh from the repository root.
After monitoring reconciliation, use Dashboards → Home Budget → Home Budget
Receipt Telemetry in Grafana for receipt, RabbitMQ queue, and telemetry-pipeline
health, and Home Budget OCR Performance for engine/configuration throughput
and latency. Dashboard variables filter the bounded environment, service,
worker, OCR configuration, outcome, and queue dimensions. Exemplar markers on
the application time-series panels open the corresponding Tempo trace.
Prometheus alerts cover missing or failed telemetry delivery, receipt failures,
review-required ratios, OCR failures and latency, growing or stale work queues,
and dead-lettered jobs.
Commit and pull request¶
git status
git switch -c <short-branch-name>
git add <changed-files>
git commit -m "Describe the change"
git push -u origin HEAD
gh pr create --base main
gh pr checks --watch
gh pr merge --squash --delete-branch
After a merge, CI writes a deployment commit to main after publishing an
immutable image tag. Start each branch from an updated main; if the deployment
commit arrives first, update the branch from origin/main before merging.
Never force-push main to resolve this race.
Documentation-only change¶
Start from synchronized main, build the manual with strict validation, and
review the staged change before creating a pull request:
git switch main
git pull --ff-only
git switch -c docs/<short-topic>
make docs-build
git add docs/ README.md mkdocs.yml
git diff --cached --check
git commit -m "Describe the documentation change"
git push -u origin HEAD
gh pr create --base main --fill
gh pr checks --watch
gh pr merge --squash --delete-branch
git switch main
git pull --ff-only
Stage only files that belong to the change; git add docs/ README.md mkdocs.yml
is appropriate when all current documentation edits are intentional. Pull
requests validate the manual. After merge, the Documentation workflow publishes
GitHub Pages and the CI workflow validates the repository on main.
GitHub Actions and GHCR¶
List recent workflows:
gh run list --limit 5
Watch a run that is currently in progress:
gh run watch --exit-status
If no run is in progress, gh run watch reports that directly; use
gh run list to confirm the latest completed result. A successful main-branch
workflow tests the project, builds the amd64 image, pushes commit and main
tags to GHCR, publishes the same image under the stable PostgreSQL bootstrap
alias, and updates k8s/kustomization.yaml in a deployment commit.
The PostgreSQL init container uses
ghcr.io/larocquemb/home-budget-schema-bootstrap:main. This name is separate
from the Kustomize-managed application image, so ordinary application releases
do not change the StatefulSet pod template or restart PostgreSQL. A fresh
PostgreSQL volume still receives the latest bootstrap files. The versioned
PreSync migration image remains tied to the application deployment.
K3s runs the pinned home-budget-postgres:18.6-oidc-* image and mounts the
explicit postgres18-data claim at /var/lib/postgresql, matching PostgreSQL
18's versioned data layout. The legacy postgres-data-postgres-0 claim is not
mounted; it is retained only as a PostgreSQL 17 rollback copy. This cutover does
not migrate database contents from PostgreSQL 17. A fresh PostgreSQL 18 claim is
initialized from the staged bootstrap SQL.
Argo CD deployment¶
Argo CD owns application deployments and workload restarts. make deploy-k3s
provisions the local environment's PostgreSQL Secret and OCR ConfigMap, then
requests an Argo CD sync. Configuration changes that need fresh pods use Argo CD
Deployment restart actions. The helpers do not apply workload manifests or run
kubectl rollout restart. They require an Argo CD login and permission to run
the application's restart actions. ARGO_APP and ARGO_SERVER configure both
sync and restart operations.
Open the private Argo CD UI at
https://argocd.brownrook.net/applications/ledger. For CLI operations, log in
through the same private hostname:
argocd login argocd.brownrook.net --grpc-web
argocd account get-user-info
The current Argo CD installation uses native Argo CD accounts; SSO is not configured. The login command prompts for the username and password, then stores its session in the user's Argo CD configuration. Do not put an Argo CD password or token in the repository or shell history.
Show recent CI runs, Argo status, Kubernetes Jobs, and Pods in one read-only dashboard:
make status
The underlying script is scripts/deployment_status.sh. Its defaults can be
overridden when needed:
RUN_LIMIT=10 ARGO_APP=ledger KUBE_NAMESPACE=home-budget make status
make status does not require an Argo CD login. It reads the Application in
core mode through the current Kubernetes context and falls back to kubectl.
It does require working GitHub CLI authentication, Kubernetes access, and
jq. When multiple kubeconfigs exist, set
KUBECONFIG=~/.kube/config-brownrook first.
The ledger application tracks main. Its current sync policy is manual:
merging a PR and publishing an image does not deploy it until Argo CD is synced.
Use make deploy-k3s to provision environment configuration and request the sync,
or use the Argo CD UI/CLI when configuration is already provisioned.
A PreSync hook runs home-budget-db-setup before workloads are
updated. The hook applies current constraints, audit and description fields,
merchant aliases, OCR learning, product enrichment, and blue/green receipt
processing state.
If it fails, Argo correctly leaves application workloads OutOfSync.
Check status:
argocd app get ledger --refresh
kubectl -n argocd get application ledger
kubectl -n home-budget get jobs,pods
To refresh repository state and retry a failed sync:
argocd app get ledger --hard-refresh
argocd app sync ledger --grpc-web
argocd app wait ledger --sync --health --timeout 600 --grpc-web
make status
Do not pass a commit with --revision while the application is configured to
track main. If Argo reports another operation in progress, inspect it first.
Terminate it only when it is genuinely stuck:
argocd app terminate-op ledger
Capturing PreSync hook failures¶
Argo may remove hook Jobs and pods quickly. Start a watcher before retrying:
while true; do
pod=$(kubectl -n home-budget get pods \
-l app=receipt-processing-schema-upgrade \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
[ -n "$pod" ] && kubectl -n home-budget logs -f "$pod" && break
sleep 1
done
Run argocd app sync ledger in another terminal. A successful hook may be
deleted automatically after completion.
Verify the rollout¶
kubectl -n home-budget rollout status deployment/ledger-web
kubectl -n home-budget get pods
argocd app get ledger
Old failed receipt-processor pods may remain because the CronJob retains failed history. Judge the current state using the newest Job and its logs.
Receipt processor verification¶
The CronJob runs every 15 minutes. Find and inspect the newest scheduled Job:
latest=$(kubectl -n home-budget get jobs \
-l app=receipt-processor \
--sort-by=.metadata.creationTimestamp \
-o jsonpath='{.items[-1:].metadata.name}')
kubectl -n home-budget logs "job/$latest"
For an immediate smoke test:
kubectl -n home-budget create job \
--from=cronjob/receipt-processor receipt-processor-test
kubectl -n home-budget logs -f job/receipt-processor-test
Success requires failed: 0. review_required is a data-quality outcome, not
an infrastructure failure; inspect those receipts in the web review reports.
Web and data-quality verification¶
After rollout, use the dashboard in this order:
- Receipts
- Extraction audit
- Review queue
- Expenses and evidence
- Category rules
- Receipt processing
- Duplicates
- Category spend and transactions
The detailed workflow and status definitions are in the user guide.