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:
parent
a95a7e20e2
commit
77b5e01198
3 changed files with 73 additions and 55 deletions
|
|
@ -82,8 +82,13 @@ Via `systemd/units` metadata in `metadata.py` (consumed by `bundles/systemd/`):
|
|||
|
||||
### Action chains — deploy lifecycle
|
||||
|
||||
- `git_deploy` → `pip_install` (non-editable; setuptools writes egg-info to
|
||||
a left4me-writable tempdir) → `alembic_upgrade` → `seed_overlays` + web restart.
|
||||
- `git_deploy` → `uv_sync` (`uv sync --frozen` against the workspace's
|
||||
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).
|
||||
- Post-git-deploy reloads: `systemctl daemon-reload`, `sysctl --system`.
|
||||
- Post-apply self-test: `verify-hardening-dropins` (asserts the drop-ins are
|
||||
|
|
|
|||
|
|
@ -287,15 +287,17 @@ git_deploy = {
|
|||
'repo': node.metadata.get('left4me/git_url'),
|
||||
'rev': node.metadata.get('left4me/git_branch'),
|
||||
'triggers': [
|
||||
# Rebuild + reinstall the packages whenever the checkout
|
||||
# changes. pip_install does its own out-of-tree build (copies
|
||||
# source to a left4me-owned tempdir before invoking pip), so
|
||||
# the source tree itself stays root-owned and untouched.
|
||||
# pip_install cascades into alembic_upgrade → web restart.
|
||||
'action:left4me_pip_install',
|
||||
# Re-sync the workspace whenever the checkout changes. uv reads
|
||||
# the committed uv.lock at /opt/left4me/src and installs both
|
||||
# workspace members (l4d2host, l4d2web) editable into
|
||||
# /var/lib/left4me/.venv. Hatchling's PEP 660 editable install
|
||||
# doesn't write to the source tree, so /opt/left4me/src stays
|
||||
# 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
|
||||
# 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',
|
||||
# Reload systemd unit definitions whenever the checkout changes;
|
||||
# handles updates to hardening drop-in content without requiring
|
||||
|
|
@ -325,56 +327,69 @@ actions['left4me_chmod_scripts'] = {
|
|||
],
|
||||
}
|
||||
|
||||
actions['left4me_create_venv'] = {
|
||||
'command': 'sudo -u left4me /usr/bin/python3 -m venv /var/lib/left4me/.venv',
|
||||
'unless': 'test -x /var/lib/left4me/.venv/bin/python',
|
||||
actions['left4me_install_uv'] = {
|
||||
# uv is not in Debian Trixie's apt archive (only experimental/sid).
|
||||
# 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,
|
||||
'needs': [
|
||||
'directory:/var/lib/left4me',
|
||||
'pkg_apt:python3-venv',
|
||||
'user:left4me',
|
||||
],
|
||||
'triggers': [
|
||||
'action:left4me_pip_upgrade',
|
||||
'pkg_apt:curl',
|
||||
],
|
||||
# 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'] = {
|
||||
'command': 'sudo -u left4me /var/lib/left4me/.venv/bin/python -m pip install --upgrade pip',
|
||||
'triggered': True,
|
||||
'cascade_skip': False,
|
||||
'needs': [
|
||||
'pkg_apt:python3-pip',
|
||||
],
|
||||
# No triggers — pip_install is driven by git_deploy on actual code
|
||||
# updates, not by venv setup. Keeps pip_upgrade scoped to exactly
|
||||
# its purpose.
|
||||
}
|
||||
|
||||
actions['left4me_pip_install'] = {
|
||||
# Non-editable install of l4d2host + l4d2web into the venv. We have
|
||||
# to copy the source to a left4me-writable tempdir first because
|
||||
# setuptools.build_meta writes <pkg>.egg-info/ into the source dir
|
||||
# during `get_requires_for_build_wheel`, and the source tree is
|
||||
# root-owned. cp -r is fast (small tree, world-readable), the build
|
||||
# itself happens in $tmpdir, and pip installs the resulting wheel
|
||||
# into /var/lib/left4me/.venv/site-packages. --force-reinstall
|
||||
# because the version string in pyproject.toml (0.1.0) doesn't
|
||||
# change commit-to-commit; without it pip would skip on no-op.
|
||||
# triggered:True so this only fires on actual git_deploy changes
|
||||
# (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"
|
||||
'""",
|
||||
actions['left4me_uv_sync'] = {
|
||||
# The whole "install/refresh the workspace" deploy step, in one
|
||||
# action. uv reads /opt/left4me/src/uv.lock + the workspace's
|
||||
# pyproject.toml and installs both members (l4d2host, l4d2web)
|
||||
# editable into /var/lib/left4me/.venv. Hatchling's PEP 660
|
||||
# editable install drops a .pth pointing at the source tree — no
|
||||
# writes to source, so the root-owned /opt/left4me/src stays clean.
|
||||
#
|
||||
# UV_PROJECT_ENVIRONMENT redirects uv's default venv path
|
||||
# (<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
|
||||
# the left4me user). cd /var/lib/left4me ensures uv's project-config
|
||||
# walk-up doesn't trip over an unreadable parent (e.g., /root or
|
||||
# /home/ckn). --frozen requires uv.lock to be present and
|
||||
# consistent with pyproject.toml — refuses to silently update the
|
||||
# lockfile during deploy.
|
||||
'command': (
|
||||
'sudo -u left4me sh -c "'
|
||||
'cd /var/lib/left4me && '
|
||||
'env HOME=/var/lib/left4me '
|
||||
'UV_PROJECT_ENVIRONMENT=/var/lib/left4me/.venv '
|
||||
'/usr/local/bin/uv sync --frozen --project /opt/left4me/src'
|
||||
'"'
|
||||
),
|
||||
'triggered': True,
|
||||
'cascade_skip': False,
|
||||
'needs': [
|
||||
'git_deploy:/opt/left4me/src',
|
||||
'action:left4me_create_venv',
|
||||
'action:left4me_install_uv',
|
||||
'directory:/var/lib/left4me',
|
||||
'user:left4me',
|
||||
],
|
||||
'triggers': [
|
||||
'action:left4me_alembic_upgrade',
|
||||
|
|
@ -389,14 +404,14 @@ actions['left4me_alembic_upgrade'] = {
|
|||
'sudo -u left4me sh -c "'
|
||||
'cd /opt/left4me/src/l4d2web && '
|
||||
'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'
|
||||
'"'
|
||||
),
|
||||
'triggered': True,
|
||||
'cascade_skip': False,
|
||||
'needs': [
|
||||
'action:left4me_pip_install',
|
||||
'action:left4me_uv_sync',
|
||||
'file:/etc/left4me/host.env',
|
||||
'file:/etc/left4me/web.env',
|
||||
],
|
||||
|
|
@ -411,7 +426,7 @@ actions['left4me_seed_overlays'] = {
|
|||
'command': (
|
||||
'sudo -u left4me sh -c "'
|
||||
'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 '
|
||||
'seed-script-overlays /opt/left4me/src/examples/script-overlays'
|
||||
'"'
|
||||
|
|
|
|||
|
|
@ -34,8 +34,6 @@ defaults = {
|
|||
'curl': {},
|
||||
'ca-certificates': {},
|
||||
'python3': {},
|
||||
'python3-venv': {},
|
||||
'python3-pip': {},
|
||||
'python3-dev': {},
|
||||
# steamcmd is a 32-bit ELF; needs i386 multiarch + these libs.
|
||||
# `_` → `:` is bundlewrap's pkg_apt convention for multiarch
|
||||
|
|
|
|||
Loading…
Reference in a new issue