A deployment helper with scoped policy
Working directory, environment overlays, a timeout, and cleanup, all under one narrowed scope.
01 — Problem
The job
A deployment step is several commands that belong together: render a configuration file for the target environment, pack a release, check what went into it. Each runs in the same working directory, with the same environment, under the same deadline, and the scratch directory has to go away afterwards whether the step succeeded or not.
In Cuprum the shared policy is one ScopeConfig: the
programs allowed, a default deadline, an environment overlay, and a hook
that records every command's outcome. The working directory is a per-run
ExecutionContext, and cleanup is an ordinary
with block.
02 — Before you start
Before you start
- Level
- Intermediate
- Needs
- Python 3.12 or later, and tar on PATH (any POSIX system)
- Install
- Cuprum 0.2.0b2 from PyPI; see Getting started
- Tested
- 2026-10-19 against Cuprum 0.2.0b2 on Linux, Python 3.12; both runs repeated eight times with identical output
- Uses
- ScopeConfig (allowlist, timeout, env_overlay, after_hooks), ExecutionContext(cwd=...), TimeoutExpired
03 — Code
The code
import sys
import tempfile
from pathlib import Path
from cuprum import (
TAR,
ExecutionContext,
Program,
ProgramCatalogue,
ScopeConfig,
scoped,
sh,
)
PYTHON = Program(sys.executable)
CATALOGUE = ProgramCatalogue.from_programs(PYTHON, TAR, name="deploy")
python = sh.make(PYTHON, catalogue=CATALOGUE)
tar = sh.make(TAR, catalogue=CATALOGUE)
audit: list[str] = []
def record(cmd, result):
tool = "python" if cmd.program == PYTHON else cmd.program
audit.append(f"{tool} {cmd.argv[0]} exited {result.exit_code}")
RENDER = (
"import os, pathlib\n"
"pathlib.Path('app.conf').write_text(f\"env={os.environ['DEPLOY_ENV']}\\n\")"
)
policy = ScopeConfig(
allowlist=CATALOGUE.allowlist,
timeout=10.0,
env_overlay={"DEPLOY_ENV": "staging"},
after_hooks=(record,),
)
with tempfile.TemporaryDirectory(prefix="deploy-") as workdir:
staging = Path(workdir)
here = ExecutionContext(cwd=staging)
with scoped(policy):
python("-c", RENDER).run_sync(context=here)
packed = tar("-czf", "release.tgz", "app.conf").run_sync(context=here)
listed = tar("-tzf", "release.tgz").run_sync(context=here)
rendered = (staging / "app.conf").read_text()
print(f"rendered {rendered.strip()!r}")
print(f"packed ok={packed.ok}")
print(f"archive holds {listed.stdout.split()}")
print(f"cleaned up: {not staging.exists()}")
for line in audit:
print(line)
04 — Output
Expected output
rendered 'env=staging' packed ok=True archive holds ['app.conf'] cleaned up: True python -c exited 0 tar -czf exited 0 tar -tzf exited 0
The first line comes from the file the child wrote: the child saw
DEPLOY_ENV=staging from the scope's overlay without the
parent's environment changing. The archive holds the rendered file,
packed in the scratch directory because every command ran with that
cwd.
The directory is gone once the with block closes, and the
after hook saw each of the three commands in the order they finished.
05 — Failure path
When it fails
Now a migration step that hangs. The scope's one-second default applies to
every command inside it, so the step is torn down: asked to terminate,
given cancel_grace to do so, then killed.
import sys
import tempfile
from pathlib import Path
from cuprum import (
ExecutionContext,
Program,
ProgramCatalogue,
ScopeConfig,
TimeoutExpired,
scoped,
sh,
)
PYTHON = Program(sys.executable)
CATALOGUE = ProgramCatalogue.from_programs(PYTHON, name="deploy")
python = sh.make(PYTHON, catalogue=CATALOGUE)
MIGRATE = "import time; print('migrating', flush=True); time.sleep(30)"
policy = ScopeConfig(allowlist=CATALOGUE.allowlist, timeout=1.0)
staging = None
try:
with tempfile.TemporaryDirectory(prefix="deploy-") as workdir:
staging = Path(workdir)
with scoped(policy):
python("-c", MIGRATE).run_sync(
context=ExecutionContext(cwd=staging, cancel_grace=0.5)
)
except TimeoutExpired as exc:
print(f"timed out after {exc.timeout}s")
print(f"partial output: {exc.output!r}")
print(f"cleaned up: {staging is not None and not staging.exists()}")
timed out after 1.0s partial output: 'migrating\n' cleaned up: True
TimeoutExpired carries what the child had printed
before its deadline, and the scratch directory was still removed: the
exception unwound through the with block like any other.
Every expiry also writes a subprocess_timeout_expired
warning to the cuprum.timeout logger on stderr, with the
process ID and the deadline.
06 — Why it is allowed
Why the policy permits it
The catalogue lists this interpreter and tar, and the scope's
allowlist is that catalogue's, so nothing else can run inside the block. The
overlay, the deadline, and the audit hook apply to every command in the
scope and disappear when it closes; none of them changes the process's own
environment.