traintraintrain
Runs your releases like a railway. Each version is a train that calls at a series of stops in order: a canary first, then progressively larger groups of platforms (regions, clusters, …). Each platform keeps its own timetable, so deploys only start during its service hours, in its own timezone. After each deploy the train dwells at the platform while it's inspected (health checks). On failure it rolls back and halts. A signal can hold the train at a stop until a person clears it.
traintraintrain does not deploy anything itself. You supply shell commands (helm, kubectl, terraform, CodeDeploy, etc.), and traintraintrain decides when and where to run them.
Vocabulary
| Term | Meaning |
|---|---|
| train | One version being rolled out |
| stop | A step in the rollout: a group of platforms served together |
| platform | One deploy target: a region, cluster, data centre, … |
| timetable | When a platform may receive deploys: service hours in a timezone, minus line closures |
| held at signal | The platform is waiting for its timetable to come into service |
| dwell | How long the train waits at a platform after deploying, while inspections (health checks) run |
| headway | Minimum gap between finishing one stop and starting the next |
| signal | A manual gate: the stop waits until someone clears the signal |
| emergency brake | Stop the train at its next checkpoint (resumable) |
Features
- Stops: a canary first, then progressively larger stops, serving up to
parallel_platformsplatforms at a time. - Timetables per platform: e.g. "Mon–Thu 09:00–16:00 Europe/Dublin". DST is handled, service hours can cross midnight, and line closures can apply globally or to one timetable (holiday freezes, launch days).
- Dwell and inspection: after each deploy, the platform dwells while inspections run. On failure the train rolls back (the platform, the stop or the whole train) and halts.
- Signals: a stop can wait for someone to clear its signal before deploying.
- Resumable: the train survives crashes and restarts. Re-run the same command to continue, or retry just the failed platforms.
- Slack: one live-updating message per train, with details in its thread and a mention on failures.
- Web UI: a departure board where you can dispatch trains, clear signals, pull the brake and retry, with an audit trail of who did what.
- Small: one runtime dependency (PyYAML). The UI has no build step and needs no CDN.
Install
Requires Python 3.10+, on Linux or macOS.
pip install traintraintrain
This installs the traintraintrain command.
Or use the container image; see Docker and Kubernetes.
Quick start
traintraintrain -c examples/train.yaml timetable # projected departure per platform
traintraintrain -c examples/train.yaml service # upcoming service hours
traintraintrain -c examples/train.yaml depart --version v1.4.0 --previous-version v1.3.2
traintraintrain -c examples/train.yaml board --version v1.4.0
traintraintrain -c examples/train.yaml clear-signal --version v1.4.0 --stop atlantic
traintraintrain -c examples/train.yaml brake --version v1.4.0 # from another shell
traintraintrain -c examples/train.yaml ui # web dashboard on http://127.0.0.1:8080
How a journey runs
- The train calls at stops in order. It leaves a stop only after every platform there
has finished dwelling. Then it waits out the next stop's
headway, if one is set. - If a stop has
signal: true, the train waits there until someone clears the signal. - At each stop, up to
parallel_platformsplatforms are served at once. Each platform:- is held at a signal until its timetable is in service. If that's further away
than
max_hold, the platform fails. - deploys by running
deploy_command, which is subject todeploy_timeout. - dwells for
dwell, runninginspection_commandeveryinspection_interval.
- is held at a signal until its timetable is in service. If that's further away
than
- On any failure the train halts and serves no new platforms. Platforms already being
served finish, and rollback runs according to
rollback_scope:noneplatform: only the failed platformstop: the failed stoptrain: every platform updated on this journey
- State is saved to
.traintraintrain/<train>-<version>.json. Re-running the same command resumes from that state, and platforms that succeeded are skipped. Failed platforms need--retry-failed.
Config
See examples/train.yaml. timetable, dwell and
deploy_timeout can be set per platform, per stop, or as train defaults
(default_timetable, default_dwell, deploy_timeout). The most specific setting wins.
| Timetable key | Meaning |
|---|---|
timezone |
IANA zone for the service hours, e.g. Europe/Dublin. DST is handled. |
service_hours[].days |
mon-thu, [mon, wed], fri-mon (wraps around) or all |
service_hours[].start/end |
"HH:MM" local time. If end <= start, service runs past midnight. |
min_remaining |
Don't start a deploy unless at least this much service time is left |
line_closures |
{start, end, reason, timezone?}. A date-only end is inclusive. |
Global train.line_closures, such as a holiday freeze, apply to every timetable. A
timetable without service_hours is always in service. The built-in timetable always
is also available.
Commands are templates using {train} {platform} {stop} {version} {previous_version}
plus any platform vars. The same values are exported as env vars (DT_PLATFORM,
DT_STOP, DT_CLUSTER, …). Escape literal braces as {{ }}.
Signals
A stop with signal: true stops the train before deploying. The train's status becomes
at_signal, and Slack posts a message that links to the UI if ui_url is set. The
train proceeds once someone clears the signal, either in the web UI or with
traintraintrain clear-signal --version V --stop S. The name of whoever cleared it is
recorded. If you brake and then resume, a signal that was already cleared stays cleared.
Web UI
traintraintrain -c train.yaml ui --host 127.0.0.1 --port 8080
What you can see
- Departures, showing each train's status and progress.
- Route view. Each platform shows its status and any failure output. It also shows whether its timetable is in service, or when service resumes in the platform's own timezone.
- Events, with routine inspections hidden unless you ask for them.
- Live log of the journey.
- "Who did what", an audit trail of actions taken in the UI.
- Timetable, showing each platform's service right now and projected departures for a train leaving now.
What you can do
- New departure, with a dry-run option and a preview of the timetable.
- Clear a signal.
- Pull the emergency brake.
- Resume an interrupted or braked journey.
- Retry failed platforms.
How it works
- The UI only reads state files. Each train runs as a separate
traintraintrain departprocess, logging to.traintraintrain/<train>-<version>.log. The UI can restart without affecting trains en route. - The brake and signals use the same marker files as the CLI.
- Only one process may drive a train at a time. A train whose process died shows as interrupted, and you can resume it.
Access and auth
- By default the UI listens only on localhost.
- Shared deployments: put the UI behind your SSO proxy (oauth2-proxy, IAP, etc.).
The user name is taken from
X-Forwarded-User/X-Auth-Request-User(or the matching...-Emailheaders) and recorded on every action. - Shared token: alternatively, set
TRAINTRAINTRAIN_UI_TOKENto require a token. Users then enter a name in the header, which is self-reported. - Every POST must send
X-Requested-With: traintraintrain(CSRF protection). - Actions are appended to
.traintraintrain/<train>.audit.jsonl.
Slack notifications
notifications:
slack:
channel: "#deploys" # bot mode needs a channel plus $SLACK_BOT_TOKEN
token_env: SLACK_BOT_TOKEN # default
webhook_url_env: SLACK_WEBHOOK_URL # default; used when no bot token is set
mention_on_failure: "<!here>" # or "<!subteam^S0123ABCD>" / "<@U0123ABCD>"
ui_url: https://traintraintrain.example.com # signal messages link here
events: [...] # optional filter, see below
Bot mode (recommended). Create a Slack app with the chat:write scope, install it,
invite it to the channel and export SLACK_BOT_TOKEN.
- Each train gets one message in the channel. It is edited live with a per-stop
status board, e.g.
canary ✅ us-west-2 · atlantic 🚦 eu-west-1 🚀 us-east-1. - Every event is posted as a reply in that message's thread.
- Failures, red signals and the final result are also broadcast to the channel.
- If you resume the train, updates go to the same thread.
Webhook mode. Export SLACK_WEBHOOK_URL (an incoming webhook). Each event is posted as
its own message.
The available events are train_started, train_resumed, train_finished,
stop_started, stop_completed, stop_at_signal, signal_cleared, platform_held,
platform_deploying, platform_succeeded, platform_failed and
platform_rolled_back. All are sent by default except platform_deploying. Failure
messages include the last part of the failing command's output, plus the
mention_on_failure mention.
If Slack is unavailable. A Slack error or a missing credential is logged as a warning and never stops a deploy.
traintraintrain notify-testsends a test message so you can check the setup.depart --no-notifyturns notifications off for one journey.- Notifications are always off in
--dry-run.
Docker and Kubernetes
Images are published to GitHub Container Registry for linux/amd64 and linux/arm64:
ghcr.io/vishnut-procore/traintraintrain:<version>, plus :edge built from main.
# Web UI, with config mounted read-only and state on a volume
docker run -p 127.0.0.1:8080:8080 -e TRAINTRAINTRAIN_UI_TOKEN=change-me \
-v ./config:/config:ro -v traintraintrain-state:/state ghcr.io/vishnut-procore/traintraintrain:0.1.1
# One train as a one-off job (exits 0 when it arrives, 1 if it fails or is braked)
docker run -v ./config:/config:ro -v traintraintrain-state:/state \
ghcr.io/vishnut-procore/traintraintrain:0.1.1 depart --version v1.4.0 --previous-version v1.3.2
Add your deploy tools to the image. Your deploy_command, inspection_command and
rollback_command run inside the container. Build your own image FROM this one with
kubectl, helm, aws, … installed;
examples/docker/Dockerfile.tools shows how.
How the image behaves
- Config and state: the config is read from
/config/train.yaml(TRAINTRAINTRAIN_CONFIG), and progress is saved under/state(TRAINTRAINTRAIN_STATE_DIR, which overrides the config'sstate_dir). Keep/stateon a persistent volume. - Non-root: the image runs as user
10001. Bind-mounted state directories must be writable by that user. - Health check:
/healthzneeds no token, and the image'sHEALTHCHECKuses it. - Stopping a container (
docker stop, a pod shutting down) is graceful. Adepartjob pulls its train's emergency brake. The UI brakes the trains it dispatched and waits up toTRAINTRAINTRAIN_SHUTDOWN_GRACEseconds (default 20). The journey can then be resumed. - Restarts are recoverable. The process driving a train writes a heartbeat to the
state file. If a container is replaced mid-journey, the train shows as interrupted
once its heartbeat is 90 seconds old, and you can resume it with no
--force. - Run one UI instance per state directory. Two instances could drive the same train twice.
Examples
examples/docker/docker-compose.yml: the UI with a persistent volume. Run it withTRAINTRAINTRAIN_UI_TOKEN=change-me docker compose up --build.examples/kubernetes/ui.yaml: the UI as a single-replica Deployment (Recreatestrategy) with a PersistentVolumeClaim, probes, a non-root security context and a Service.examples/kubernetes/job.yaml: one train per release as a Kubernetes Job, for CD pipelines.
Running it in CI
A train can take days to complete its journey while it waits for service hours, so run
it as a long-lived job, such as a Kubernetes Job (see above) or a self-hosted runner,
with state_dir on persistent storage. Alternatively, run it on a schedule (cron or a
scheduled pipeline) with a short max_hold. Each invocation then resumes and travels as
far as the current timetables allow.
Upgrading from the old names
Earlier development versions used generic terms, and the project was called
deploytrain. The old names are still accepted, so existing configs, scripts and state
files keep working. Using an old name and its new name together is an error.
| Old | New |
|---|---|
waves: / regions: |
stops: / platforms: |
windows: (profiles) / windows: (inside a profile) |
timetables: / service_hours: |
window: / default_window |
timetable: / default_timetable |
blackouts |
line_closures |
bake / default_bake |
dwell / default_dwell |
gap |
headway |
max_parallel |
parallel_platforms |
approval: true |
signal: true |
health_check_command / health_check_interval |
inspection_command / inspection_interval |
window_wait_timeout |
max_hold |
rollback_scope: region / wave |
rollback_scope: platform / stop |
{region} / {wave}, DT_REGION / DT_WAVE |
{platform} / {stop}, DT_PLATFORM / DT_STOP |
plan, windows, run, status, approve --wave, abort |
timetable, service, depart, board, clear-signal --stop, brake |
Slack events wave_* / region_* |
stop_*, signal_cleared / platform_* |
deploytrain command, python -m deploytrain |
traintraintrain (the deploytrain command still works) |
DEPLOYTRAIN_* environment variables |
TRAINTRAINTRAIN_* (old names still read; new ones win) |
.deploytrain state folder |
.traintraintrain (an existing .deploytrain folder keeps being used) |
import deploytrain, image ghcr.io/vishnut-procore/deploytrain |
import traintraintrain, image ghcr.io/vishnut-procore/traintraintrain |
Contributing
Issues and pull requests are welcome. See CONTRIBUTING.md for the development setup, and our Code of Conduct. To report a security issue, please follow SECURITY.md rather than opening a public issue.
pip install -e ".[dev]" && python -m pytest
License
Apache License 2.0. Copyright 2026 Vishnuvardhan.
Metadata
Release files for traintraintrain 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| traintraintrain-0.1.1.tar.gz | 70.8 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| traintraintrain-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 128.0 kB
Release files / traintraintrain-0.1.1.tar.gz
| Download URL | traintraintrain-0.1.1.tar.gz |
|---|---|
| Size | 70.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
abad0387808062fc7ad3174877c268aead702fba0ae311e168f68abd61a62f17
|
|
BLAKE2b-256 checksum How to use checksums |
000cf712c02b3d669c5a376da926703e61b7d14ca0cc9b63de330f1fadf41164
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / traintraintrain-0.1.1-py3-none-any.whl
| Download URL | traintraintrain-0.1.1-py3-none-any.whl |
|---|---|
| Size | 57.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7bc49221be3914240f80e0e3ec2e122d168f2523b7b4991378589bb77aec9cc7
|
|
BLAKE2b-256 checksum How to use checksums |
1de4eb3f5cef1eb2fa6f29b4c000dffc835c36a8a1fbad042183559719b4298e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logSecurity for traintraintrain 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for traintraintrain-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.