Troubleshooting

On this page

Docker & DevStack

Garage initialization fails

The S3-compatible Garage service needs bucket and key initialization after startup. Since C4-gate (#1012) that runs as the in-graph garage-init compose one-shot on every docker compose up path — xtask and manual alike — before the seed (which gates on it completing). If you see errors about missing buckets or access denied:

  1. Ensure Garage is healthy: docker compose ps garage

  2. Check the one-shot’s outcome: docker compose ps -a garage-init and docker compose logs garage-init

  3. Re-run it in place: docker compose up -d garage-init (idempotent — a persisted garage volume makes it a fast no-op)

Keycloak hostname errors

Keycloak 26+ uses "hostname v2" configuration. The KC_HOSTNAME_PORT variable is silently ignored. KC_HOSTNAME must be a full URL including the port:

KC_HOSTNAME: http://host.docker.internal:8180  # Correct
KC_HOSTNAME: localhost                           # Wrong — port will default to 443

Postgres "permission denied" / 42501 after #1310

Since #1310 the devstack splits each service’s DB credential: gates connect as craig_<svc>_owner, serving containers as craig_<svc>_app (DML-only). Symptoms and remedies:

  • Services fail auth (password authentication failed for user "craig_<svc>_…") right after pulling #1310: the roles are provisioned by devstack/postgres/init.sql, which runs only on a FRESH data directory — run cargo xtask dev reseed.

  • permission denied (SQLSTATE 42501) on a table: the table was created by a role other than the service’s owner role (e.g. hand-created as the craig superuser), so the owner’s default privileges never fired. Recreate it through a migration (gate-applied), or GRANT SELECT, INSERT, UPDATE, DELETE ON <table> TO craig_<svc>_app;.

  • A 42501 on CREATE/ALTER/DROP from a serving container is the boundary WORKING — serving binaries never issue DDL (ADR-063); schema changes go through the migration gates.

RabbitMQ authentication failures

The RabbitMQ user must be defined in devstack/rabbitmq/definitions.json with a SHA-256 password hash. Environment variables alone do not configure users. If you change the password, regenerate the hash and update definitions.json.

Port conflicts

Default service ports:

Service Port

craig-rules

8001

craig-cases

8002

craig-placement

8003

craig-exchange

8004

craig-financial

8005

craig-reporting

8006

craig-security

8007

craig-intake

8008

craig-composition

8009

craig-intake-standalone

8010

craig-web

8080

Keycloak

8180

PostgreSQL

5432

RabbitMQ

5672

Garage (S3)

3900

If a port is in use, check for lingering Docker containers: docker ps -a

Testing

Random 30-second reqwest timeouts

Nextest defaults to logical CPU count for parallelism (e.g., 32 threads on a dev machine). This overwhelms the devstack with concurrent connections.

Fix: Set test-threads = 16 (physical core count) in .config/nextest.toml. This is already configured in the repository.

Keycloak 401 errors during long test runs

Keycloak access tokens have a 30-minute lifetime. If your test session exceeds this, you’ll see 401 Unauthorized errors.

Fix: Re-run the tests. For very long sessions, consider increasing the token lifetime in the Keycloak admin console (Realm Settings → Tokens).

E2E tests fail with DNS errors in CI

CI runs in Docker-in-Docker (DinD). Services are reachable via Docker Compose service names, not host.docker.internal or localhost.

Correct CI URLs:

CRAIG_E2E_BASE_URL=http://craig-web:8080        # Not http://localhost:8080
OIDC_ISSUER=http://keycloak:8080/realms/craig # Not http://host.docker.internal:8180

Test results location

All test output is saved to test-results/ at the repo root:

  • test-results/e2e/artifacts/ — failure screenshots and traces

  • test-results/e2e/junit.xml — structured test results

  • test-results/ci/unit.xml — unit test results (CI)

  • test-results/ci-integration/integration.xml — integration test results (CI)

Pre-push sdk-test pypi DNS flake

The pre-push battery’s final sdk-test stage runs the Python SDK suite in a python:3.13-alpine container that pip install`s the build backend (`hatchling) from pypi.org. It intermittently fails to resolve pypi.org with [Errno -3] Try again (EAI_AGAIN) — a transient DNS failure inside the container, not a code or test defect:

NameResolutionError(host='pypi.org', port=443): Failed to resolve 'pypi.org' ([Errno -3] Try again)
ERROR: No matching distribution found for hatchling

Fix: If the push fails only at the sdk-test/Python stage with this pypi DNS error and every earlier battery stage passed, just re-push (the whole battery re-runs). Confirm the branch actually landed afterwards with git ls-remote.

reqwest "dns error" against an IPv4 literal = redirect chain

A reqwest error like ConnectError("dns error", …​) whose url is a literal IPv4 address (e.g. http://127.0.0.1:38901/) is almost always a redirect chain landing on an unresolvable hostname — not an actual DNS problem with the literal IP. reqwest’s Error::url() preserves the original requested URL even when the failure occurs mid-redirect, so the message names 127.0.0.1 while the failed lookup was really something like host.docker.internal (a container-only name that does not resolve from outside containers).

Fix: Before suspecting nsswitch / NSS / IPv4-vs-IPv6, disable redirect following — Client::builder().redirect(reqwest::redirect::Policy::none()). If the error disappears, you were following a redirect chain to an unreachable host. (A DNS error against a real hostname such as https://example.com may be a genuine DNS problem; this rule applies when the failing URL is a literal IP and the error is unexpected.)

Development

gen or ref used as identifier

Rust 2024 reserves gen and ref as keywords. If you see a compilation error about reserved keywords, rename the identifier.

For Askama templates specifically:

{% if let Some(x) = value %}    // Correct
{% if let Some(ref x) = value %} // Wrong — `ref` is reserved

curl -sf returns wrong status code

When using curl with -f (fail on HTTP errors) and -w '%{http_code}', the -f flag causes the status code to be concatenated with the error output.

Fix: Use -s without -f, and handle errors with || true:

STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/health || true)

CI Pipeline

Pipeline stages

The CI pipeline has 4 stages: scan → promote → deploy → triage (see .gitlab-ci.yml).

  • scan: the security, docs, and lint gates — SAST, secret detection, dependency scanning, cargo audit (change-gated to manifest/advisory files), ci-tests, cargo-deny, secrets-policy, the docs checks, and the scheduled pentest/perf jobs

  • promote: docker-promote builds and pushes service images (main/tag commits); sbom + release on tags

  • deploy: Antora docs (review apps on MRs, Pages on main)

  • triage: the weekly scheduled triage:stale job (see Stale Work Policy)

Why CI doesn’t run the full battery

The full test battery (validate, integration, e2e, perf, security regression) runs in the pre-push hook on every developer machine — CI deliberately does not duplicate it (see the deployment guide's CI/CD Pipeline section). CI does run a devstack-free subset: ci-tests executes cargo xtask validate --skip-devstack --skip-docker (fmt, clippy, build, the ci nextest profile’s unit-test pass) on MR pipelines touching Rust/Cargo/xtask paths, and the scheduled pentest/perf-load/perf-stress/cluster-tests jobs exercise the promoted images out-of-band. Anything devstack-bound ran in the pre-push battery, not CI.

GitLab API gotchas

Epic notes POST returns 404

Adding a comment to an epic via POST /groups/<id>/epics/<iid>/notes returns 404 for this group. This is stable behavior (tied to the gitlab.com epic-vs-work-item migration state), not a transient bug or a permissions problem — issue notes (POST /projects/<id>/issues/<iid>/notes) work normally, and only epic notes 404.

Workaround: Append the comment text to the epic’s description instead — GET /groups/<id>/epics/<iid>, then PUT /groups/<id>/epics/<iid> with {"description": "<old>\n\n---\n\n<new>"}. Closing the epic still works normally via PUT /groups/<id>/epics/<iid> with {"state_event": "close"}.

glab api -f field=@file sends the literal "@file"

glab api --method PUT …​ -f "description=@/tmp/file" does not read the file — it sends the literal string @/tmp/file as the field value, silently overwriting the target field with that string.

Fix: For large or multi-line PUT bodies (such as an epic/issue description), use curl with an explicit JSON content type:

curl -sS --request PUT \
  --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  --header "Content-Type: application/json" \
  --data @/tmp/body.json \
  "https://gitlab.com/api/v4/groups/<id>/epics/<iid>"   # body.json = {"description": "..."}

glab api --input file.json returns 415 on this endpoint (it does not set Content-Type: application/json). Always re-fetch after a description PUT and assert the expected markers are present — a silent wrong-body write is otherwise invisible.

Edit this page · latest