refactor(left4me): collapse venv chain into uv sync

Replace left4me_create_venv + left4me_pip_upgrade + left4me_pip_install
(the tempdir-copy dance) with a single left4me_uv_sync action driven by
left4me's committed uv.lock. Deterministic dep versions, no source-tree
mutation during build (hatchling PEP 660 editable installs don't write
to source), one action instead of three.

uv is not in Trixie's apt archive (experimental/sid only), so a new
left4me_install_uv action downloads a pinned 0.11.8 binary tarball
from astral-sh/uv releases, SHA256-verifies against the published
.sha256 sibling, and installs into /usr/local/bin. Idempotent via
`unless` on the version string — only re-runs when the pin is bumped.
Pattern matches left4me_install_steamcmd elsewhere in this bundle.

apt.packages: drop python3-pip and python3-venv (uv replaces both;
no other consumer in the bundle). Keep python3 and python3-dev — uv
shells out to the system Python interpreter.

PYTHONPATH=/opt/left4me/src removed from left4me_alembic_upgrade and
left4me_seed_overlays — was a workaround for the previous layout's
package-dir indirection; with the new standard layout + editable
install, the venv resolves both members natively.

Per left4me/docs/superpowers/plans/2026-05-15-uv-workspace-execution.md.
Requires the matching commit on left4me's master.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
CroneKorkN 2026-05-15 22:07:47 +02:00
parent a95a7e20e2
commit 77b5e01198
Signed by: cronekorkn
SSH key fingerprint: SHA256:v0410ZKfuO1QHdgKBsdQNF64xmTxOF8osF1LIqwTcVw
3 changed files with 73 additions and 55 deletions

View file

@ -82,8 +82,13 @@ Via `systemd/units` metadata in `metadata.py` (consumed by `bundles/systemd/`):
### Action chains — deploy lifecycle ### Action chains — deploy lifecycle
- `git_deploy``pip_install` (non-editable; setuptools writes egg-info to - `git_deploy``uv_sync` (`uv sync --frozen` against the workspace's
a left4me-writable tempdir) → `alembic_upgrade``seed_overlays` + web restart. committed `uv.lock`; hatchling PEP 660 editable, doesn't touch source)
`alembic_upgrade``seed_overlays` + web restart.
- One-shot bootstrap: `install_uv` downloads a pinned `uv` binary
(SHA256-verified) into `/usr/local/bin` because `uv` isn't in Trixie's
apt archive. `unless`-gated, so it's a no-op once the version pin is
installed; re-runs only when the constant is bumped.
- Idempotent gates: `chmod-sudoers` (0440 root:root), `chmod-scripts` (0755 root:root). - Idempotent gates: `chmod-sudoers` (0440 root:root), `chmod-scripts` (0755 root:root).
- Post-git-deploy reloads: `systemctl daemon-reload`, `sysctl --system`. - Post-git-deploy reloads: `systemctl daemon-reload`, `sysctl --system`.
- Post-apply self-test: `verify-hardening-dropins` (asserts the drop-ins are - Post-apply self-test: `verify-hardening-dropins` (asserts the drop-ins are

View file

@ -287,15 +287,17 @@ git_deploy = {
'repo': node.metadata.get('left4me/git_url'), 'repo': node.metadata.get('left4me/git_url'),
'rev': node.metadata.get('left4me/git_branch'), 'rev': node.metadata.get('left4me/git_branch'),
'triggers': [ 'triggers': [
# Rebuild + reinstall the packages whenever the checkout # Re-sync the workspace whenever the checkout changes. uv reads
# changes. pip_install does its own out-of-tree build (copies # the committed uv.lock at /opt/left4me/src and installs both
# source to a left4me-owned tempdir before invoking pip), so # workspace members (l4d2host, l4d2web) editable into
# the source tree itself stays root-owned and untouched. # /var/lib/left4me/.venv. Hatchling's PEP 660 editable install
# pip_install cascades into alembic_upgrade → web restart. # doesn't write to the source tree, so /opt/left4me/src stays
'action:left4me_pip_install', # root-owned and untouched. uv_sync cascades into
# alembic_upgrade → seed_overlays → web restart.
'action:left4me_uv_sync',
# alembic upgrade head is idempotent — keeping it as a direct # alembic upgrade head is idempotent — keeping it as a direct
# trigger off git_deploy is belt-and-braces in case the # trigger off git_deploy is belt-and-braces in case the
# pip_install cascade is ever short-circuited. # uv_sync cascade is ever short-circuited.
'action:left4me_alembic_upgrade', 'action:left4me_alembic_upgrade',
# Reload systemd unit definitions whenever the checkout changes; # Reload systemd unit definitions whenever the checkout changes;
# handles updates to hardening drop-in content without requiring # handles updates to hardening drop-in content without requiring
@ -325,56 +327,69 @@ actions['left4me_chmod_scripts'] = {
], ],
} }
actions['left4me_create_venv'] = { actions['left4me_install_uv'] = {
'command': 'sudo -u left4me /usr/bin/python3 -m venv /var/lib/left4me/.venv', # uv is not in Debian Trixie's apt archive (only experimental/sid).
'unless': 'test -x /var/lib/left4me/.venv/bin/python', # Pin to a specific release; download the tarball + its SHA256
# sibling from astral-sh/uv releases, verify, install to
# /usr/local/bin. Idempotent via `unless` — only re-runs when the
# pinned version changes (bump the constant in two places below).
# Pattern matches left4me_install_steamcmd (curl+tar) elsewhere in
# this bundle. Bump cadence: as needed; both dev (brew uv) and
# prod should track the same minor.
'command': """set -e
tmpdir=$(mktemp -d); trap "rm -rf $tmpdir" EXIT
base=https://github.com/astral-sh/uv/releases/download/0.11.8
tar=uv-x86_64-unknown-linux-gnu.tar.gz
curl -fsSL -o $tmpdir/$tar $base/$tar
curl -fsSL -o $tmpdir/$tar.sha256 $base/$tar.sha256
(cd $tmpdir && sha256sum -c $tar.sha256)
tar -xzf $tmpdir/$tar -C $tmpdir --strip-components=1
install -m 0755 $tmpdir/uv /usr/local/bin/uv
install -m 0755 $tmpdir/uvx /usr/local/bin/uvx
""",
'unless': '/usr/local/bin/uv --version 2>/dev/null | grep -qx "uv 0.11.8"',
'cascade_skip': False, 'cascade_skip': False,
'needs': [ 'needs': [
'directory:/var/lib/left4me', 'pkg_apt:curl',
'pkg_apt:python3-venv',
'user:left4me',
],
'triggers': [
'action:left4me_pip_upgrade',
], ],
# No triggers — install_uv is a one-shot bootstrap. uv_sync needs
# it (via `needs`), so the dependency runs install_uv first on a
# clean host. After that, this action is a no-op on every apply
# unless the version pin changes.
} }
actions['left4me_pip_upgrade'] = { actions['left4me_uv_sync'] = {
'command': 'sudo -u left4me /var/lib/left4me/.venv/bin/python -m pip install --upgrade pip', # The whole "install/refresh the workspace" deploy step, in one
'triggered': True, # action. uv reads /opt/left4me/src/uv.lock + the workspace's
'cascade_skip': False, # pyproject.toml and installs both members (l4d2host, l4d2web)
'needs': [ # editable into /var/lib/left4me/.venv. Hatchling's PEP 660
'pkg_apt:python3-pip', # editable install drops a .pth pointing at the source tree — no
], # writes to source, so the root-owned /opt/left4me/src stays clean.
# No triggers — pip_install is driven by git_deploy on actual code #
# updates, not by venv setup. Keeps pip_upgrade scoped to exactly # UV_PROJECT_ENVIRONMENT redirects uv's default venv path
# its purpose. # (<project>/.venv) to our writable runtime location. HOME is set
} # explicitly so uv's cache lands in /var/lib/left4me/.cache/uv
# instead of the inherited sudo HOME (which can be unwritable for
actions['left4me_pip_install'] = { # the left4me user). cd /var/lib/left4me ensures uv's project-config
# Non-editable install of l4d2host + l4d2web into the venv. We have # walk-up doesn't trip over an unreadable parent (e.g., /root or
# to copy the source to a left4me-writable tempdir first because # /home/ckn). --frozen requires uv.lock to be present and
# setuptools.build_meta writes <pkg>.egg-info/ into the source dir # consistent with pyproject.toml — refuses to silently update the
# during `get_requires_for_build_wheel`, and the source tree is # lockfile during deploy.
# root-owned. cp -r is fast (small tree, world-readable), the build 'command': (
# itself happens in $tmpdir, and pip installs the resulting wheel 'sudo -u left4me sh -c "'
# into /var/lib/left4me/.venv/site-packages. --force-reinstall 'cd /var/lib/left4me && '
# because the version string in pyproject.toml (0.1.0) doesn't 'env HOME=/var/lib/left4me '
# change commit-to-commit; without it pip would skip on no-op. 'UV_PROJECT_ENVIRONMENT=/var/lib/left4me/.venv '
# triggered:True so this only fires on actual git_deploy changes '/usr/local/bin/uv sync --frozen --project /opt/left4me/src'
# (the cp + build is too heavy to run on every apply). '"'
'command': """sudo -u left4me sh -c ' ),
set -e
tmpdir=$(mktemp -d -t left4me-build-XXXXXX)
trap "rm -rf \\"$tmpdir\\"" EXIT
cp -r /opt/left4me/src/l4d2host /opt/left4me/src/l4d2web "$tmpdir/"
/var/lib/left4me/.venv/bin/pip install --force-reinstall "$tmpdir/l4d2host" "$tmpdir/l4d2web"
'""",
'triggered': True, 'triggered': True,
'cascade_skip': False, 'cascade_skip': False,
'needs': [ 'needs': [
'git_deploy:/opt/left4me/src', 'git_deploy:/opt/left4me/src',
'action:left4me_create_venv', 'action:left4me_install_uv',
'directory:/var/lib/left4me',
'user:left4me',
], ],
'triggers': [ 'triggers': [
'action:left4me_alembic_upgrade', 'action:left4me_alembic_upgrade',
@ -389,14 +404,14 @@ actions['left4me_alembic_upgrade'] = {
'sudo -u left4me sh -c "' 'sudo -u left4me sh -c "'
'cd /opt/left4me/src/l4d2web && ' 'cd /opt/left4me/src/l4d2web && '
'set -a && . /etc/left4me/host.env && . /etc/left4me/web.env && set +a && ' 'set -a && . /etc/left4me/host.env && . /etc/left4me/web.env && set +a && '
'env JOB_WORKER_ENABLED=false PYTHONPATH=/opt/left4me/src ' 'env JOB_WORKER_ENABLED=false '
'/var/lib/left4me/.venv/bin/alembic -c /opt/left4me/src/l4d2web/alembic.ini upgrade head' '/var/lib/left4me/.venv/bin/alembic -c /opt/left4me/src/l4d2web/alembic.ini upgrade head'
'"' '"'
), ),
'triggered': True, 'triggered': True,
'cascade_skip': False, 'cascade_skip': False,
'needs': [ 'needs': [
'action:left4me_pip_install', 'action:left4me_uv_sync',
'file:/etc/left4me/host.env', 'file:/etc/left4me/host.env',
'file:/etc/left4me/web.env', 'file:/etc/left4me/web.env',
], ],
@ -411,7 +426,7 @@ actions['left4me_seed_overlays'] = {
'command': ( 'command': (
'sudo -u left4me sh -c "' 'sudo -u left4me sh -c "'
'set -a && . /etc/left4me/host.env && . /etc/left4me/web.env && set +a && ' 'set -a && . /etc/left4me/host.env && . /etc/left4me/web.env && set +a && '
'env JOB_WORKER_ENABLED=false PYTHONPATH=/opt/left4me/src ' 'env JOB_WORKER_ENABLED=false '
'/var/lib/left4me/.venv/bin/flask --app l4d2web.app:create_app ' '/var/lib/left4me/.venv/bin/flask --app l4d2web.app:create_app '
'seed-script-overlays /opt/left4me/src/examples/script-overlays' 'seed-script-overlays /opt/left4me/src/examples/script-overlays'
'"' '"'

View file

@ -34,8 +34,6 @@ defaults = {
'curl': {}, 'curl': {},
'ca-certificates': {}, 'ca-certificates': {},
'python3': {}, 'python3': {},
'python3-venv': {},
'python3-pip': {},
'python3-dev': {}, 'python3-dev': {},
# steamcmd is a 32-bit ELF; needs i386 multiarch + these libs. # steamcmd is a 32-bit ELF; needs i386 multiarch + these libs.
# `_` → `:` is bundlewrap's pkg_apt convention for multiarch # `_` → `:` is bundlewrap's pkg_apt convention for multiarch