actbreak is a local breakpoint debugger for GitHub Actions: pause a workflow mid-step and get a real shell inside the still-running job container.
Built on act, which runs GitHub Actions workflows
locally but has no way to pause mid-run on its own. actbreak injects the breakpoint,
waits for the job container to reach it, execs you in, and resumes the run when you're
done.
Zero runtime dependencies. Python 3.9+, stdlib only.
Early / v0.2.0. Core injection and selection logic is unit tested; the
run/resume/clean orchestration against real act + Docker/Podman is covered by
a CI integration test rather than exercised in every environment this ships to.
pipx install actbreak
Or from a clone, since it's stdlib-only:
git clone https://github.com/munzzyy/actbreak
cd actbreak
pip install -e .
Requires act on PATH, and one of Docker or Podman.
actbreak run <workflow.yml> --break-before <step>
actbreak run <workflow.yml> --break-after <step>
actbreak run <workflow.yml> --break-on-failure
actbreak resume
actbreak clean
actbreak init-vscode
<workflow.yml> is either a path to a workflow file, or a bare name looked up
under .github/workflows/.
A step selector is either a step's name: value, or <job>:<index> to select
by zero-based position (use this for steps with no name:).
| Flag | Meaning |
|---|---|
--break-before STEP |
pause immediately before STEP runs |
--break-after STEP |
pause immediately after STEP runs |
--break-on-failure |
if act exits nonzero, attach to the last job container for post-mortem |
--job JOB |
disambiguate a multi-job workflow |
--runtime {docker,podman,auto} |
container runtime to use (default: auto-detect) |
--no-attach |
don't exec a shell automatically; print the attach command and hold |
--act-arg ARG |
extra argument passed through to act (repeatable) |
-v, --verbose |
print the injection/act commands being run |
actbreak init-vscode
Scans every .github/workflows/*.yml (and .yaml) and writes one VS Code
task per step: actbreak: <workflow> / <job> / <step>, each running the real
actbreak run <workflow> --break-before "<job>:<index>" command in the
integrated terminal. Instead of typing the step selector by hand, open the
command palette (Cmd/Ctrl+Shift+P → "Tasks: Run Task") and pick the step.
Reruns are idempotent: a second init-vscode replaces only the tasks it
generated last time (matched by the actbreak: label prefix) and leaves
every other task in .vscode/tasks.json untouched. If that file already has
// comments or a trailing comma (both legal in VS Code's own format, not in
plain JSON), it's left alone entirely and the generated tasks go to
.vscode/actbreak-tasks.json instead, for you to merge in by hand.
actbreak --completions bash (or zsh) prints a completion script built
from the argparse parser, so new flags show up without touching it:
# bash
source <(actbreak --completions bash)
# zsh
source <(actbreak --completions zsh)actbreak run ci.yml --break-before "Run tests"
actbreak run ci.yml --job build --break-before build:2
actbreak run ci.yml --break-after "Build" --no-attach
actbreak run ci.yml --break-on-failure
- Finds the target workflow and, using the given job/step selector, resolves an exact step in it.
- Copies the workflow to a temp file and splices a synthetic step in immediately before or after the target, using line-based text injection (never a YAML parse-and-re-serialize round trip; see below for why).
- The injected step drops a sentinel file (
/tmp/actbreak/hold) and blocks on it inside the container. - Runs
act -W <temp copy> --reuseso the container stays alive after the run "finishes" (i.e. hangs at the hold). - Polls
docker ps/podman psfor the job's container, then for the sentinel file, to know the breakpoint has been hit. - Execs an interactive shell into the container. Exiting the shell (or
running
actbreak resume) deletes the sentinel and lets the job continue.
- The breakpoint step needs a real shell in the job container: it runs
shwithmkdir,printf, andsleep. Ascratchor distroless image without those won't hold at the breakpoint. act --reusekeeps the job container alive so you can attach to it. actbreak reaps that container once the run finishes cleanly (resumed to the end, the breakpoint never hit, or a--break-on-failurerun that passed), so a normal run doesn't leave a stopped container behind. The one it keeps on purpose is--no-attach, which parks the container foractbreak resumeto pick up later — clear those withactbreak clean.
Because round-tripping a workflow through a generic YAML library corrupts it.
PyYAML's default loader coerces an unquoted on: key to the boolean True
under YAML 1.1 rules, and any generic dumper throws away comments, quoting
style, and anchors. actbreak never deserializes the file. It scans for
jobs:, then the target job, then its steps: list, using indentation alone,
and splices in new lines at the right point. Every other byte in the file is
untouched.
pip install -e ".[dev]"
python -m unittest
pytest
The integration pytest marker (pytest -m integration) runs a real
act + Docker/Podman end-to-end test; it's auto-skipped unless both are on
PATH, which in practice means it only runs in CI.
MIT — free to use, change, and ship, commercial or not. See LICENSE.
If actbreak saved you a round of push-and-pray debugging, sponsoring is what keeps it maintained.