Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,28 @@ jobs:
- name: pytest (integration)
run: uv run pytest -v -m integration

client-drift:
name: client-drift
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- name: Install Python
run: uv python install 3.12

# Regenerate each vendored generated client from its committed OpenAPI
# snapshot and fail on any diff vs the committed generated/ tree — turns a
# stale client (the #66 failure mode) into a red build. Stdlib-only driver
# (--no-project skips the root sync); the per-SDK regen syncs that SDK's
# own lockfile, pinning its openapi-python-client + ruff versions.
- name: detect generated-client drift vs committed OpenAPI snapshot
run: uv run --no-project --python 3.12 python scripts/check_client_drift.py

changelog:
name: changelog
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ htmlcov/
.worktrees/
.claude/settings.local.json
node_modules/

# Ephemeral regen dirs from scripts/check_client_drift.py (cleaned on exit;
# ignored so an interrupted run can't be committed accidentally).
.drift-*/
16 changes: 14 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,20 @@ clients/python/ archiver_client SDK v3.x (generated + hand-writte
clients/watcher-python/ watcher_client SDK — Archiver adapter for the Watcher service
(httpx-based; wraps provision, patch, get, check-now, list-revisions)
Regen: bash clients/watcher-python/scripts/regen.sh
watcher-openapi.json: committed OpenAPI snapshot
(contract-of-record). CI `client-drift` job fails
if generated/ != regen-from-snapshot (catches the
#66 stale-client drift). Fix hand-edits: python
scripts/check_client_drift.py --write watcher; on
a real Watcher change re-run regen.sh (refreshes
snapshot + tree).
alembic/ Migration root (information schema scoped within the archiver database)
tests/ Mirrors src/ structure; tests/integration/ for cross-component flows
(HTTP + DB + bus); tests/api/ for single-route HTTP behavior
scripts/ dump_openapi.py +
check_client_drift.py (regen vendored clients from
committed OpenAPI snapshots; diff vs generated/;
CI gate, see client-drift job) +
check_changelog_on_push.sh (pre-push guard;
wired via .pre-commit-config.yaml)
deploy/ Systemd unit (archiver.service)
Expand All @@ -83,8 +93,10 @@ skills-vendor/ Git submodules for external skill repos
.claude/skills/ Claude Code skill discovery (symlinks → ../../skills/<name>)
.github/workflows/ CI — lint job (ruff check + ruff format --check),
test job (Postgres service container, alembic upgrade,
pytest), and changelog job (feat/fix changes must
touch CHANGELOG.md; opt out via `no-changelog` PR
pytest), client-drift job (regen vendored clients
from committed OpenAPI snapshots, fail on diff),
and changelog job (feat/fix changes must touch
CHANGELOG.md; opt out via `no-changelog` PR
label). Triggers on push/PR to main.
.pre-commit-config.yaml ruff check + ruff format + standard pre-commit-hooks
(pre-commit stage), plus a pre-push guard
Expand Down
22 changes: 19 additions & 3 deletions clients/watcher-python/scripts/regen.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
#!/usr/bin/env bash
# Regenerate clients/watcher-python/src/watcher_client/generated/ from the
# Watcher service's OpenAPI schema. Idempotent — safe to run repeatedly.
# Regenerate the watcher_client SDK from the Watcher service's live OpenAPI.
#
# Writes BOTH artifacts in lockstep so the CI drift gate
# (scripts/check_client_drift.py) is a no-op afterward:
# 1. clients/watcher-python/watcher-openapi.json — committed contract-of-record
# snapshot (pretty-printed, order-preserving).
# 2. clients/watcher-python/src/watcher_client/generated/ — regenerated FROM
# the snapshot (not the raw live bytes), so the snapshot is authoritative.
#
# Use this when Watcher legitimately changes shape. Idempotent — safe to re-run.
#
# Requires: Watcher running on http://localhost:8000
# Env: WATCHER_API_KEY (used for authenticated endpoints; openapi.json is public)
Expand All @@ -9,20 +17,28 @@ set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
SDK_DIR="${REPO_ROOT}/clients/watcher-python"
GEN_DIR="${SDK_DIR}/src/watcher_client/generated"
SNAPSHOT="${SDK_DIR}/watcher-openapi.json"

cd "${REPO_ROOT}"
TMP_SPEC="$(mktemp -t watcher-openapi-XXXXXX.json)"
trap 'rm -f "${TMP_SPEC}"' EXIT

curl -sf http://localhost:8000/openapi.json -o "${TMP_SPEC}"

# Canonicalize into the committed snapshot: pretty-print, order-preserving.
# NOT sort_keys — openapi-python-client emits model fields in spec property
# order, so sorting would reshape (not just reformat) the generated tree.
uv run --no-project python -c "import json,sys; d=json.load(open(sys.argv[1])); open(sys.argv[2],'w').write(json.dumps(d, indent=2)+'\n')" \
"${TMP_SPEC}" "${SNAPSHOT}"

cd "${SDK_DIR}"
rm -rf "${GEN_DIR}"
uv run openapi-python-client generate \
--path "${TMP_SPEC}" \
--path "${SNAPSHOT}" \
--meta none \
--output-path "${GEN_DIR}" \
--overwrite

uv run ruff format "${GEN_DIR}" || true # cosmetic; don't fail regen on format diffs
echo "Regenerated: ${SNAPSHOT}"
echo "Regenerated: ${GEN_DIR}"
Loading
Loading