Generated Requirements Drift
Symptom
Worker CI fails in the Requirements drift check step with output shaped like this:
ERROR: services/<svc>/requirements.txt is out of sync with uv.lock.
Run 'make regen-requirements' and commit the result.
The current check lives in lumie-worker/.github/workflows/ci.yml and compares generated exports for grading, report, analysis, and chatbot against the committed services/<svc>/requirements.txt files.
What CI Treats As The Contract
The contract became explicit across the May 14 to May 16, 2026 dependency changes:
| Date | Commit | Contract change |
|---|---|---|
| 2026-05-14 | 79bc1fe1 | worker services were standardized under a uv workspace |
| 2026-05-16 | 1a7ffc16 | uv.lock was added as the lock source |
| 2026-05-16 | 258735b4 | generated requirements.txt files and make regen-requirements became the pinned export path |
| 2026-07-12 | 072c96b6 | analysis requirements had to be regenerated after dependency changes |
The Makefile is the local source of truth for regeneration:
EXPORT = $(UV) export --frozen --no-dev --no-emit-project --no-header
regen-requirements:
$(EXPORT) --package grading-svc --output-file services/grading/requirements.txt
$(EXPORT) --package report-svc --output-file services/report/requirements.txt
$(EXPORT) --package analysis-svc --output-file services/analysis/requirements.txt
$(EXPORT) --package chatbot-svc --output-file services/chatbot/requirements.txt
Source: lumie-worker/Makefile
CI uses the same export flags in .github/workflows/ci.yml, so any committed file that does not match that exact export output is treated as drift.
Likely Causes
A service pyproject.toml changed but requirements.txt did not
This is the most common failure. For example, the current services/analysis/pyproject.toml includes asyncpg==0.30.0 and reportlab==4.2.5. Those dependencies also appear in the current generated services/analysis/requirements.txt, along with transitive packages such as chardet and pillow.
uv.lock changed but exports were not regenerated
Because the Makefile uses uv export --frozen, the committed requirements.txt files must match the lockfile exactly, not just the service pyproject.toml inputs.
requirements.txt was hand-edited
The generated files are full lock exports with hashes and transitive dependencies. If a file looks manually shortened or partially edited, CI will diff it immediately.
Local uv output does not match CI
The Makefile comment calls out this risk directly: CI pins a canonical uv version in the requirements drift step. If local uv output differs, you can see false drift even when the dependency graph is conceptually unchanged.
Diagnostic Path
- Read the failing service name from the CI output.
- Compare the service
pyproject.tomlwith the generated file.services/report/pyproject.tomlandservices/analysis/pyproject.tomlare the first places to inspect for direct dependency changes. - Confirm the package exists in
uv.lock. The current lock manifest includesanalysis-svc,chatbot-svc,grading-svc, andreport-svc. - Reproduce the exact CI diff locally with the same export flags.
Representative check for analysis-svc:
uv export --frozen --no-dev --no-emit-project --no-header --package analysis-svc \
| diff -u services/analysis/requirements.txt -
Fix
From the lumie-worker repo root:
make regen-requirements
That command regenerates all worker service requirements.txt files from uv.lock with the same flags CI uses.
If make regen-requirements fails with --frozen, fix the lockfile first so uv.lock matches the service pyproject.toml inputs. If it succeeds but CI still reports drift, align the local uv version with the version pinned in .github/workflows/ci.yml.
Verification
- Re-run the exact diff command for the affected service and confirm there is no output.
- Check that the regenerated file still includes hashed, transitive exports rather than a short hand-maintained dependency list.
- Re-run the Worker CI check or let
.github/workflows/ci.ymlconfirm theRequirements drift checkstep passes.
Operational Detail
The generated requirements.txt files are deployment artifacts, but they are still committed because each worker service image consumes them. That makes drift a release blocker rather than a cosmetic CI failure.
| File | Role |
|---|---|
service pyproject.toml | Declares direct service dependencies. |
uv.lock | Pins the resolved workspace dependency graph. |
services/<svc>/requirements.txt | Generated export consumed by service builds. |
.github/workflows/ci.yml | Enforces that committed exports match the lock graph. |
If only one service dependency changed, regenerate all service requirement files anyway. The Makefile target is intentionally all-service so shared transitive changes do not leave a second service stale.
Recovery Playbook
- Identify the failing service from CI.
- Run
make regen-requirementsfromlumie-worker, not from an individual service directory. - Inspect the diff for unexpected dependency graph changes.
- If
uv export --frozenfails, update or repairuv.lockbefore regenerating exports. - If local output differs from CI, compare the local
uvversion with the workflow-pinned version. - Commit
pyproject.toml,uv.lock, and generated requirement changes together when all three changed.
Verification Evidence
Close the failure with:
- clean local diff checks for each affected service export,
- no hand-edited short requirement files,
- CI
Requirements drift checkpassing, - a diff review that explains any large transitive dependency movement.
Anti-Patterns
- Editing
requirements.txtdirectly to satisfy a missing package at runtime. - Regenerating only the service that happened to fail first.
- Updating
uv.lockwithout committing the generated exports. - Ignoring a local/CI
uvversion mismatch and repeatedly committing churn.