Enables automatic requirements and venv updating.
Called directly -- or, typically, by a cron job -- this script:
- checks a bunch of expectations about the project
- if expectations fail, an email noting the failure will go out to:
- the target-project admins if possible
- otherwise the auto-updater admins
- if expectations fail, an email noting the failure will go out to:
- on
staging, runsuv sync --upgrade --group staging --dry-run --output-format jsonto see whether a real update is needed before mutating the target repo.- if the dry run reports no changes, the script does nothing further.
- if the dry run reports lockfile-only metadata churn (currently the
exclude-newercase), the script does nothing further. - if the dry run reports a substantive dependency change, the script performs the real update, then re-runs the project's tests, runs django's
collectstaticif necessary, and notifies the project-admins.
- on
production, skips upgrade discovery entirely and runsuv sync --locked --group prodso the deployed.venvmatches the already-committeduv.lock.
(see the auto_updater.py manage_update() function, for details)
- Performs these initial checks:
- validates submitted project-path
- determines the admin-emails
- validates expected branch
- validates expected git-status
- ensures a python version is listed
- ensures
pyproject.tomlincludes a[tool.uv] exclude-newersetting - determines the local/staging/production environment
- validates the
uvpath - determines the group
- validates group and permissions on the venv and the
requirements_backupsdirectories - runs project's
run_tests.py
- If any of the above steps fail, emails project-admins (or updater-admins)
- On
staging:- runs
uv sync --upgrade --group staging --dry-run --output-format json - if the dry run reports no changes, stops without backing up
uv.lockor updating.venv - if the dry run reports lockfile-only metadata churn, stops without backing up
uv.lockor updating.venv - if the dry run reports a substantive change:
- saves a backup of
uv.lockto../uv.lock.bak - runs the real
uv sync --upgrade --group staging - evaluates whether
uv.lockactually changed - runs project's
run_tests.py- on test success
- performs a diff on new and old
uv.lockshowing the change, and creates diff text - if a django app
- runs its
collectstaticcommand - runs the usual
touchcommand to let passenger know to reload the django-app
- runs its
- runs a git-pull, then a git-add, then a git-commit, then a git-push
- emails the diff (and any issues) to the project-admins
- performs a diff on new and old
- on test failure
- restores original
uv.lock - runs
uv sync --frozen --group staging - runs project's
run_tests.pyagain - emails the canceled-diff (and test-failures) to the project-admins
- restores original
- on test success
- saves a backup of
- runs
- On
production:- skips upgrade discovery entirely
- runs
uv sync --locked --group prod - does not back up or diff
uv.lock - does not run git commit/push operations
- skips tests as before
- still runs django
collectstaticand restart handling if the installed django version changed across the locked sync
- Finally, attempts to update group & permissions on the venv and the
requirements_backupsdirectories
-
Directly:
$ cd "/path/to/requirements-auto-updater/" $ uv run --env-file "../.env" ./auto_updater.py --project "/path/to/project_to_update_code_dir/" -
Via cron on servers (eg to run every day at 12:01am) (all one line):
1 0 * * * PATH=/usr/local/bin:$PATH cd "/path/to/requirements-auto-updater/" && /path/to/uv run --env-file "../.env" ./auto_updater.py --project "/path/to/project_to_update_code_dir/"
- A
pyproject.tomlfile exists, with adependencies = []entry, a[dependency-groups]entry withstagingandprodentries, and a[tool.uv]section with anexclude-newersetting (for example,"2 days"). - The dependencies use tilde-notation (
package~=1.2.0) wherever possible. That third numeral is important; we only want to update thepatchversion. - There is a
.envfile in the "outer-stuff" directory. - The
.envfile contains anADMINS_JSONentry with the following structure:ADMINS_JSON=' [ [ "exampleFirstname1 exampleLastname1", "example1@domain.edu" ], [ "exampleFirstname2 exampleLastname2", "example2@domain.edu" ] ]'
-
Assumes
uvis accessible- Checks the
which uvpath. If nothing found, will raise an exception and email the project-admins. This is the reason for updatingPATHin the cron-call.
- Checks the
-
Suggestion: we do not tweak this script for different project-structures; rather, we restructure our apps to fit these assumptions (to keep this script simple, and for the benefits of a more standardized project structure).
-
The
exclude-newersetting reduces the chance of pulling a newly published poisoned registry package. The specific value can vary by project. -
To monitor: using the tilde-notation to ensure that only the
patchversion is updated could still install new non-patch-level versions of dependencies. In the many months of monitoring this in real usage on three projects, this has not caused an issue.
We need to make upgrading dependency-packages more sustainable.
By definition, the third part of package-notation (major, minor, patch) infers both backwards-compatibility and bug-fixes, so this should lighten the technical-debt load. However it is possible for patch upgrades to contain backwards-incompatibilites -- example.