Rules-as-config restart decider for Python services. Don't restart your whole stack on every git push.
You ship a one-line bug fix to one service in your monorepo. Your deploy script restarts everything — bot, API, worker, scheduler — and you eat 30 seconds of cold starts and dropped connections, every time.
Most CI/CD scripts pick between two extremes:
- Restart everything always. Safe, but wasteful and slow.
- Hand-maintained "if file in dir then restart X." Fast, but rots silently when imports cross boundaries: someone refactors
utils/db.pyand forgets thatworker/jobs.pyimports it, the worker keeps the old code, you get a Heisenbug at 3 a.m.
smart-restart is the middle ground: a small decider that takes the list of changed files and a YAML rule set, then prints which actions are needed. Optionally, it walks the AST-derived import graph in reverse so editing a leaf utility correctly triggers restarts for every service that imports it.
It does not run the actions. Your deploy script does. smart-restart just decides.
pip install smart-restart # coming with v0.1.0Write a restart-rules.yaml:
actions:
pip_install:
order: 10 # lower = runs earlier
restart_api:
order: 20
rules:
- pattern: "requirements.txt"
actions: [pip_install, restart_api]
- pattern: "src/**"
actions: [restart_api]
- pattern: "**/*.md"
actions: [] # explicitly do nothing
fallback: [restart_api] # for files that match no rulePipe a diff into it:
$ git diff --name-only HEAD~1 HEAD | smart-restart
pip_install
restart_apiThat's the output your deploy script consumes. Wire it into bash, Ansible, GitHub Actions, anything that takes stdout.
The base decider only knows about files you literally changed. That misses the case where a leaf module changes and dependents don't.
Pass --root <project> and smart-restart will parse every .py file under <project>, build a static import graph, and expand the changed file list to include every transitive importer:
src/
utils/db.py # you changed this
api/handlers.py # imports utils.db
worker/jobs.py # imports utils.db
$ echo "src/utils/db.py" | smart-restart --root src --show-expanded
INFO: import-chain expansion added 2 file(s):
+ smart_restart/api/handlers.py
+ smart_restart/worker/jobs.py
restart_api
restart_workerIf restart-rules.yaml only matches src/api/** to restart_api and src/worker/** to restart_worker, the expansion ensures both fire even though only db.py was in the diff.
If your project uses src-layout (code under src/your_pkg/), pass --root src, not --root .. The graph indexes files by their dotted module name; the dotted name has to match what your imports actually say (from your_pkg.foo resolves to <root>/your_pkg/foo.py).
| Layout | Use |
|---|---|
src/your_pkg/... (src-layout) |
--root src |
your_pkg/... (flat / package at root) |
--root . |
Multiple roots (e.g. services/api/, services/worker/) |
run once per root, merge actions yourself |
- One source of truth. Change
restart-rules.yaml, every script that consumes the decider stays in sync. - Composable. Pipe it into bash, Ansible, GitHub Actions, anything that takes stdout. No framework lock-in.
- Diffable. Restart rules live in version control, not buried in a shell script.
- Testable. Pure function, no side effects, hermetic.
actions: # required: action name -> {order: int}
<name>:
order: <int> # lower = runs earlier; default 100
rules: # required: list, evaluated top-down, first match wins
- pattern: <glob> # ** matches across directories, * within one segment
actions: [<name>...] # actions to fire (use [] to explicitly skip)
fallback: # what to do for files that match no rule
none # default: contribute no actions
| all # fire every declared action
| [<name>...] # fire just theseFirst-match-wins semantics: each file is matched against rules top-to-bottom, only the first hit contributes. This lets you put a narrow **/*.md → [] rule above a broad src/** → [restart_api] rule.
🚧 Alpha — extracted from a closed-source dogfood project (~2 years of daily use).
Done in v0.1:
- Generic rule engine with first-match-wins semantics
- YAML-declared actions (no hardcoded service names)
- Import-chain reverse closure (AST-based)
- Glob patterns with
**recursion - Pure decider — no execution, no side effects
- 37 unit tests, fixture-driven
Deferred to v0.2+:
- LLM fallback for unmatched files (the upstream project has it; needs to be re-cast as a plugin to keep v0.1 dependency-free)
- Multi-root graph merge in one invocation
- Auto-detection of src-layout from
pyproject.toml - Star-import edge tracking at the symbol level