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
5 changes: 0 additions & 5 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ updates:
schedule:
interval: "weekly"
open-pull-requests-limit: 5
- package-ecosystem: "pip"
directory: "/backend"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
- package-ecosystem: "pip"
directory: "/"
schedule:
Expand Down
50 changes: 24 additions & 26 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,32 +148,10 @@ jobs:
- name: Install uv
run: pip install uv

- name: uv lock --check (root PEP 621 manifest)
- name: Synchronize manifests and regenerate lockfiles for Dependabot
if: env.IS_DEPENDABOT == 'true'
run: |
if ! uv lock --check; then
if [ "$IS_DEPENDABOT" = "true" ]; then
echo "::notice::root uv.lock out of sync with pyproject.toml (expected on Dependabot PRs); regenerating."
uv lock
else
echo "::error::root uv.lock is out of sync with pyproject.toml."
echo "Regenerate it with: uv lock"
exit 1
fi
fi

- name: uv lock --check (backend)
working-directory: backend
run: |
if ! uv lock --check; then
if [ "$IS_DEPENDABOT" = "true" ]; then
echo "::notice::backend/uv.lock out of sync (expected on Dependabot PRs); regenerating."
uv lock
else
echo "::error::backend/uv.lock does not match backend/pyproject.toml."
echo "Regenerate it with: cd backend && uv lock"
exit 1
fi
fi
python scripts/sync_dependencies.py

- name: Commit regenerated lockfiles to the Dependabot branch
if: >-
Expand All @@ -183,14 +161,34 @@ jobs:
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add uv.lock backend/uv.lock backend/requirements.lock
git add pyproject.toml backend/pyproject.toml setup.cfg backend/setup.cfg \
backend/requirements-runtime.txt backend/requirements-dev.txt \
backend/requirements.lock uv.lock backend/uv.lock \
frontend/package.json frontend/package-lock.json
if git diff --cached --quiet; then
echo "Lockfiles already in sync; nothing to commit."
else
git commit -m "chore(deps): regenerate lockfiles for Dependabot update"
git push || echo "::warning::Could not push regenerated lockfiles; continuing verification against the regenerated files."
fi

- name: uv lock --check (root PEP 621 manifest)
run: |
if ! uv lock --check; then
echo "::error::root uv.lock is out of sync with pyproject.toml."
echo "Regenerate it with: uv lock"
exit 1
fi

- name: uv lock --check (backend)
working-directory: backend
run: |
if ! uv lock --check; then
echo "::error::backend/uv.lock does not match backend/pyproject.toml."
echo "Regenerate it with: cd backend && uv lock"
exit 1
fi

- name: Verify dependency manifests and lockfiles agree
run: python scripts/check_dependency_sync.py

Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Oyster360 developer shortcuts. The same commands live in README.md; run
# `make help` to list targets.
.PHONY: help ci-local deps-check setup bootstrap fresh-start up down logs ps migrate seed verify quality test test-unit test-backend test-frontend test-integration test-e2e lint
.PHONY: help ci-local deps-check deps-sync sync-deps setup bootstrap fresh-start up down logs ps migrate seed verify quality test test-unit test-backend test-frontend test-integration test-e2e lint

setup: ## Prepare a fresh clone (creates .env and starts the Docker stack)
./scripts/bootstrap.sh
Expand Down Expand Up @@ -49,6 +49,11 @@ ci-local: ## Reproduce every CI gate from a fresh clone (installs deps, no Docke
deps-check: ## Verify dependency manifests and lockfiles agree
python3 scripts/check_dependency_sync.py

deps-sync: ## Synchronize dependency manifests and regenerate lockfiles
python3 scripts/sync_dependencies.py

sync-deps: deps-sync ## Alias for deps-sync

verify: ## Run the local checks CI enforces (assumes deps are already installed)
python3 scripts/check_dependency_sync.py
cd backend && flake8 app tests --count --show-source --statistics --exclude=.venv,__pycache__,alembic/versions
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,11 @@ CI additionally runs `uv lock --check` on both `uv.lock` files, `pip install --d
### Refreshing the locks

```bash
# Automatic: synchronize manifests across the repo and regenerate all lockfiles
make deps-sync
make deps-check

# Or manually:
# Python: after editing pyproject.toml dependency pins
uv pip compile pyproject.toml --extra dev --output-file backend/requirements.lock
uv lock && (cd backend && uv lock)
Expand Down
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dependencies = [
"pyotp==2.10.0",
"qrcode[pil]==8.2",
"requests==2.34.2",
"sentry-sdk==2.68.0",
"sentry-sdk==2.68.1",
"python-json-logger==4.2.0",
]

Expand Down
2 changes: 1 addition & 1 deletion backend/requirements-runtime.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ stripe==15.5.1
pyotp==2.10.0
qrcode[pil]==8.2
requests==2.34.2
sentry-sdk==2.68.0
sentry-sdk==2.68.1
python-json-logger==4.2.0
2 changes: 1 addition & 1 deletion backend/requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ requests==2.34.2
# via
# oyster360 (pyproject.toml)
# stripe
sentry-sdk==2.68.0
sentry-sdk==2.68.1
# via oyster360 (pyproject.toml)
six==1.17.0
# via python-dateutil
Expand Down
2 changes: 1 addition & 1 deletion backend/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ install_requires =
pyotp==2.10.0
qrcode[pil]==8.2
requests==2.34.2
sentry-sdk==2.68.0
sentry-sdk==2.68.1
python-json-logger==4.2.0

[flake8]
Expand Down
139 changes: 139 additions & 0 deletions backend/tests/test_dependency_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""Tests for dependency synchronisation and manifest consistency checks."""
from __future__ import annotations

import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent.parent
if str(ROOT / "scripts") not in sys.path:
sys.path.insert(0, str(ROOT / "scripts"))

from check_dependency_sync import ( # noqa: E402
canonical,
check_node_manifests,
check_python_manifests,
errors as check_errors,
parse_requirements,
)
from sync_dependencies import ( # noqa: E402
format_toml_array,
merge_dependency_lists,
parse_dep,
parse_version,
update_setup_cfg,
update_toml_dependencies,
)


def test_canonical_names():
assert canonical("sentry-sdk") == "sentry-sdk"
assert canonical("sentry_sdk") == "sentry-sdk"
assert canonical("PyYAML") == "pyyaml"
assert canonical("argon2-cffi-bindings") == "argon2-cffi-bindings"


def test_parse_version():
assert parse_version("2.68.1") > parse_version("2.68.0")
assert parse_version("1.0.0") > parse_version("0.9.9")
assert parse_version("2.9.0.post0") > parse_version("2.9.0")
assert parse_version("2.0.0b1") < parse_version("2.0.0")
assert parse_version("==2.68.1") == parse_version("2.68.1")


def test_parse_dep():
canon, raw, op, ver = parse_dep("sentry-sdk==2.68.1")
assert canon == "sentry-sdk"
assert raw == "sentry-sdk"
assert op == "=="
assert ver == "2.68.1"

canon, raw, op, ver = parse_dep("qrcode[pil]==8.2")
assert canon == "qrcode"
assert raw == "qrcode[pil]"
assert op == "=="
assert ver == "8.2"

canon, raw, op, ver = parse_dep("starlette>=1.0.1")
assert canon == "starlette"
assert raw == "starlette"
assert op == ">="
assert ver == "1.0.1"


def test_merge_dependency_lists():
list_root = ["fastapi==0.141.1", "sentry-sdk==2.68.1", "qrcode[pil]==8.2"]
list_backend = ["fastapi==0.141.1", "sentry-sdk==2.68.0", "qrcode==8.2"]

merged = merge_dependency_lists(list_root, list_backend)
assert "sentry-sdk==2.68.1" in merged
assert "qrcode[pil]==8.2" in merged
assert "fastapi==0.141.1" in merged

# Test reverse precedence — higher version wins regardless of argument position
merged_reverse = merge_dependency_lists(list_backend, list_root)
assert "sentry-sdk==2.68.1" in merged_reverse
assert "qrcode[pil]==8.2" in merged_reverse


def test_update_toml_dependencies():
content = """[project]
name = "oyster360"
dependencies = [
"fastapi==0.141.1",
"sentry-sdk==2.68.0",
]

[project.optional-dependencies]
dev = [
"flake8==7.3.0",
]
"""
updated = update_toml_dependencies(
content,
["fastapi==0.141.1", "sentry-sdk==2.68.1"],
["flake8==7.3.0", "pytest==9.1.1"],
)
assert '"sentry-sdk==2.68.1",' in updated
assert '"pytest==9.1.1",' in updated


def test_update_setup_cfg():
content = """[metadata]
name = oyster360

[options]
python_requires = >=3.11
install_requires =
fastapi==0.141.1
sentry-sdk==2.68.0

[flake8]
select = E9
"""
updated = update_setup_cfg(
content,
["fastapi==0.141.1", "sentry-sdk==2.68.1"],
)
assert "sentry-sdk==2.68.1" in updated
assert "[flake8]" in updated


def test_parse_requirements():
lines = [
"# A comment",
"fastapi==0.141.1",
"qrcode[pil]==8.2",
"-r requirements-runtime.txt",
"",
]
parsed = parse_requirements(lines)
assert parsed["fastapi"] == "0.141.1"
assert parsed["qrcode"] == "8.2"
assert "requirements-runtime.txt" not in parsed


def test_current_manifests_pass_checks():
check_errors.clear()
check_python_manifests()
check_node_manifests()
assert check_errors == [], f"Unexpected dependency sync errors: {check_errors}"
8 changes: 4 additions & 4 deletions backend/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ test genuinely needs infrastructure, mark it `@pytest.mark.integration`.

1. **Lockfile reproducibility** — `uv lock --check` (root and backend),
`scripts/check_dependency_sync.py`, `pip install --dry-run -r
requirements.lock`, and `npm ci --dry-run`. On Dependabot PRs the uv
lockfiles are regenerated and pushed back to the branch instead of failing.
requirements.lock`, and `npm ci --dry-run`. On Dependabot PRs manifests
and lockfiles are automatically synchronized, regenerated, and pushed back
to the branch instead of failing.
2. **Lint** — flake8 (backend) and `eslint --max-warnings=0` (frontend).
3. **Typecheck** — `mypy app` and `tsc --noEmit`. Both are blocking.
4. **Test** — fast offline lane, full suite with the 80% coverage gate, Vitest
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ dependencies = [
"pyotp==2.10.0",
"qrcode[pil]==8.2",
"requests==2.34.2",
"sentry-sdk==2.68.0",
"sentry-sdk==2.68.1",
"python-json-logger==4.2.0",
]

Expand Down
15 changes: 15 additions & 0 deletions scripts/check_dependency_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,28 @@ def check_node_manifests() -> None:


def main() -> int:
import argparse
parser = argparse.ArgumentParser(description="Verify that dependency manifests and lockfiles agree.")
parser.add_argument("--fix", action="store_true", help="Automatically synchronize manifests and lockfiles.")
args = parser.parse_args()

if args.fix:
if str(ROOT / "scripts") not in sys.path:
sys.path.insert(0, str(ROOT / "scripts"))
from sync_dependencies import sync_python_manifests, recompile_lockfiles, sync_node_manifests
sync_python_manifests()
recompile_lockfiles()
sync_node_manifests()
errors.clear()

check_python_manifests()
check_node_manifests()

if errors:
print("Dependency manifests are out of sync:\n", file=sys.stderr)
for item in errors:
print(f" - {item}", file=sys.stderr)
print("\nFix automatically with: make deps-sync (or python scripts/sync_dependencies.py)", file=sys.stderr)
return 1

print("Dependency manifests and lockfiles are consistent:")
Expand Down
Loading
Loading