Skip to main content

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:

DateCommitContract change
2026-05-1479bc1fe1worker services were standardized under a uv workspace
2026-05-161a7ffc16uv.lock was added as the lock source
2026-05-16258735b4generated requirements.txt files and make regen-requirements became the pinned export path
2026-07-12072c96b6analysis 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

  1. Read the failing service name from the CI output.
  2. Compare the service pyproject.toml with the generated file. services/report/pyproject.toml and services/analysis/pyproject.toml are the first places to inspect for direct dependency changes.
  3. Confirm the package exists in uv.lock. The current lock manifest includes analysis-svc, chatbot-svc, grading-svc, and report-svc.
  4. 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.yml confirm the Requirements drift check step 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.

FileRole
service pyproject.tomlDeclares direct service dependencies.
uv.lockPins the resolved workspace dependency graph.
services/<svc>/requirements.txtGenerated export consumed by service builds.
.github/workflows/ci.ymlEnforces 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

  1. Identify the failing service from CI.
  2. Run make regen-requirements from lumie-worker, not from an individual service directory.
  3. Inspect the diff for unexpected dependency graph changes.
  4. If uv export --frozen fails, update or repair uv.lock before regenerating exports.
  5. If local output differs from CI, compare the local uv version with the workflow-pinned version.
  6. 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 check passing,
  • a diff review that explains any large transitive dependency movement.

Anti-Patterns

  • Editing requirements.txt directly to satisfy a missing package at runtime.
  • Regenerating only the service that happened to fail first.
  • Updating uv.lock without committing the generated exports.
  • Ignoring a local/CI uv version mismatch and repeatedly committing churn.