README
¶
actl
A TUI-first, interactive step-debugger for GitHub Actions workflows that runs them
locally — pause before any step, inspect the environment, drop into the job container,
re-run a step — with faithful uses: execution, because it stands on
nektos/act instead of reimplementing the Actions engine.
Status: pre-v0.1, under active development. Go. MIT. FOSS, no monetization.
Working today: single-job debugging through act's real engine — pause before/after every step, env inspector, drop into the live job container, edit a step's command or env and re-run it in place, breakpoints + run-to-cursor, job selection, matrix pinning, isolated-run
needsseeding, secrets / vars / env loading, a committable.actl.ymlconfig with per-environment overlays,-listinventory, GitHub & runtime-context seeding, and ambient GCP and AWS identity substitution — each with a transparency line.
Install
All three routes need Docker at runtime — actl starts a real job container via act.
Homebrew (macOS):
brew install ruzmuh/actl/actl
(A Homebrew cask — macOS only. On Linux, use a prebuilt binary or build from source.)
Prebuilt binary (macOS & Linux, amd64/arm64): grab the archive for your platform from the latest release, then:
tar -xzf actl_*_$(uname -s)_$(uname -m).tar.gz
sudo mv actl /usr/local/bin/ # or anywhere on your PATH
From source: the act fork is a git submodule wired in via a filesystem replace in
go.mod, so go install github.com/ruzmuh/actl/...@latest won't work (it can't resolve
the local replace). Clone with the submodule first, then install from the checkout:
git clone --recurse-submodules https://github.com/ruzmuh/actl
cd actl
go install ./cmd/actl # builds with the submodule on disk; binary lands in $GOBIN
Requires Go (the module pins the toolchain to match act; go auto-fetches it).
Why
act runs Actions workflows locally and faithfully — but as a batch runner: it has no
breakpoints, no pause-before-step, no drop-into-shell. actl adds that debug layer on top
of act, so uses: (docker / composite / node actions) runs through act's real engine
while you step through the job.
Architecture (short version)
- Reuse, don't rebuild.
act/pkg/modelparses workflows;act/pkg/exprparserevaluates${{ }}. Imported as-is. - Soft fork for the pause hook. act's per-step machinery is unexported, so a tiny patch
interleaves a barrier
common.Executorbetween steps and exposes a resume channel onrunner.Config. The fork lives inthird_party/act, wired in via areplacedirective ingo.mod, pinned to a release. We keep the diff tiny and aim to upstream the hook. - Frontend-agnostic core. The debug engine (
internal/debugger) owns no terminal and imports no frontend; the TUI is one consumer, with headless/agent and DAP front-ends as future peers behind the same API.
Layout
cmd/actl/ TUI entry point
cmd/spike-barrier/ line-based driver over the core (dev/debug aid)
internal/debugger/ the pause-barrier core: Session, pause/step/continue, log capture
internal/tui/ Bubble Tea front-end over the core
internal/config/ loads .actl.yml (the debug slice: job/matrix/breakpoints/images/…)
internal/workflow/ thin wrapper over act/pkg/model
internal/expr/ thin wrapper over act/pkg/exprparser
third_party/act/ soft fork of act — git submodule → ruzmuh/act (branch actl), pinned by SHA
testdata/workflows/ sample workflows
Develop
Requires Go (the module pins the toolchain to match act; go auto-fetches it) and Docker
(act starts a real job container and execs each step into it).
The act fork lives in a submodule, so clone with --recurse-submodules (or run
git submodule update --init afterwards):
git clone --recurse-submodules https://github.com/ruzmuh/actl
go run ./cmd/actl # debug the sample workflow in the TUI
go run ./cmd/actl path/to/workflow.yml # your own workflow
go run ./cmd/actl -image node:20-bullseye-slim # smaller image for quick run-only workflows
go test ./... runs the tests (no Docker needed).
TUI keys
When paused: s/enter step · c continue · g run-to-cursor · ↑↓/jk move cursor ·
b toggle breakpoint · e env pane · i edit step command · E edit job env ·
r re-run the step in the live container · d drop into a shell in the container · q quit.
The log pane scrolls any time (paused or running): PgUp/PgDn page, ctrl+u/ctrl+d
half-page, home/end jump to top/bottom, mouse wheel. The run halts before the first
step; break-on-error halts after a failing step.
Listing the workflow (-list)
-list inventories a workflow — its jobs, each job's steps, any matrix combinations, and
the deployment environment: — and exits without running Docker or shelling out for
identity. Use it to discover the job and step names you'll target.
go run ./cmd/actl -list testdata/workflows/pipeline.yml
Selecting a job & seeding needs
actl debugs one job at a time, in isolation — the job's upstream needs jobs are not
run. Pick the job, and seed the upstream outputs/results you want it to see; the same flags
on the command line mean a re-run reproduces the exact state.
go run ./cmd/actl testdata/workflows/pipeline.yml # lists jobs if there's more than one
go run ./cmd/actl -job deploy testdata/workflows/pipeline.yml
# seed what the upstream job would have produced (paths mirror the needs.* context):
go run ./cmd/actl -job deploy \
-need 'build.outputs.image=ghcr.io/acme/app:1.4.2' \
-need 'build.result=success' \
-env 'STAGE=prod' \
testdata/workflows/pipeline.yml
Unseeded outputs resolve to empty (exactly as a non-existent output does in GitHub); an
unseeded result defaults to success. The TUI prints a transparency line per need so you
see precisely what the isolated run stands on.
Prefer to exercise the real dependencies instead of seeding them? --with-deps runs the
upstream jobs for real to completion first, then pauses only on the target job's steps — so
needs.* are genuine and there's nothing to seed (upstream output streams to the log pane):
go run ./cmd/actl -job deploy --with-deps testdata/workflows/pipeline.yml
Matrix
A job whose matrix expands to more than one combination must be pinned to exactly one —
-list shows the combinations, and -matrix KEY=VALUE (repeatable) selects it:
go run ./cmd/actl -job test \
-matrix 'os=ubuntu-latest' -matrix 'go=1.22' \
testdata/workflows/matrix.yml
The single -image default maps ubuntu-latest; to map other runner labels to images use
-platform LABEL=IMAGE (repeatable, act's -P; overrides the images: map in .actl.yml).
When a job declares services:, act starts those service containers and the TUI prints a
line naming them.
Secrets, vars & env
actl reads act's dotenv triple from the working dir — .secrets → secrets.*,
.vars → vars.*, .env → env vars — so ${{ secrets.X }}, ${{ vars.X }} and $X
resolve as on GitHub. These files are gitignored; keep them out of commits. Override
individual keys with repeatable -secret/-var/-env KEY=VALUE (these win over the
files), or point at a file outside the repo with -secret-file/-var-file/-env-file.
printf 'TOKEN=s3cr3t\n' > .secrets # gitignored
printf 'REGION=eu-west-1\n' > .vars
printf 'STAGE=dev\n' > .env
go run ./cmd/actl testdata/workflows/config.yml
# keep secrets outside the repo and override one key for this run:
go run ./cmd/actl -secret-file ~/.config/actl/demo.secrets \
-var 'REGION=us-east-1' testdata/workflows/config.yml
The TUI prints a redacted transparency line naming what loaded — counts and names only, never values — and act masks secret values in the step logs.
Configuration (.actl.yml)
For a real workflow, stash the debug slice in a committable .actl.yml instead of a
flag soup. It's auto-discovered as .actl.yml in the working dir (point elsewhere with
-config FILE). Precedence is CLI flag > .actl.yml > built-in default, and unknown
keys are rejected so typos surface immediately.
# .actl.yml — every key optional
workflow: .github/workflows/deploy.yml # a path arg still wins
job: deploy
event: push
matrix: # pin one combination
os: ubuntu-latest
with-deps: false # true = run upstream needs for real first
images: # act's -P: runner label -> docker image
ubuntu-latest: catthehacker/ubuntu:act-latest
ubuntu-22.04: catthehacker/ubuntu:act-22.04
breakpoints: # step index OR step name
- 0
- "Build"
# workdir: . # bind-mount this dir (writable) so local 'uses: ./' resolve
# source: . # tree a default actions/checkout copies from
secret-file: .secrets # secrets are FILE-ONLY here (see below)
vars:
REGION: us-east-1
env:
LOG_LEVEL: debug
inputs: # workflow_dispatch / workflow_call
version: "1.2.3"
# needs: # seed upstream needs for isolated debugging
# lint:
# result: success
# outputs: { sha: abc123 }
Because .actl.yml is committable, secrets can't be inlined — an inline secrets:
map (top level or under an environment) is a hard error; reference a gitignored dotenv via
secret-file: instead. vars/env are not sensitive and may be inlined. A copy with
inline comments lives in .actl.yml.sample.
Per-environment overlays
GitHub scopes secrets.*/vars.* by deployment environment:. When the debugged job
targets one, the matching block under environments: overlays the flat secret-file/
vars defaults (a CLI -secret/-var still wins). The TUI prints which overlay loaded
(counts only):
environments:
production:
secret-file: .secrets.prod
vars: { REGION: us-west-2 }
staging:
vars: { REGION: eu-west-1 }
GitHub & runtime context
GitHub injects context a clean local runner lacks; actl seeds it and prints a
transparency line for each:
github.token/secrets.GITHUB_TOKEN— from-github-token, else aGITHUB_TOKENin.secrets, else ambientgh auth token; the two stay equal as on GitHub. It is not auto-exported as$GITHUB_TOKEN(faithful — map it viaenv:). Heads-up: your token's scope differs from CI's ephemeral, repo-scoped one.- Workflow inputs —
-input NAME=VALUE(repeatable) forworkflow_dispatch/workflow_call; act applies the declareddefault:and boolean typing itself, so you only supply the values you want to override. - Event payload —
-event-file PATH.jsonsetsgithub.event.*. github.*context —-repository/-ref/-sha/-actoroverride the respective fields; otherwise repository/ref/sha are derived from your local git (originremote, HEAD).actor,run_id, andrun_numberare honest placeholders.
Workspace
By default the job runs with an empty workspace (the repo is kept out of the container),
so remote uses: actions work but local uses: ./… actions and actions/checkout of the
working repo won't find any files — the TUI flags this when it spots local actions. That's the
common case; reach for -workdir only when you actually have local actions to debug.
-workdir DIR bind-mounts DIR as the workspace so local actions resolve. Note the
tradeoff: a mounted workspace is writable, so steps running in the container can change
your working tree (build artifacts, generated files). The TUI shows a transparency line when
a workspace is mounted.
go run ./cmd/actl -workdir . path/to/workflow.yml
Checkout
A default actions/checkout (no ref/repository/path) would clone a remote over the
workspace — losing your local changes. actl intercepts it: it copies your working tree
(current dir, or -source DIR) into the workspace at the checkout step's position, honouring
.gitignore and without mounting (no host writes). Steps before checkout still see an empty
workspace, exactly as on GitHub; steps after see your code, including uncommitted changes. A
checkout pinned to another repo/ref/path is left as a real clone.
go run ./cmd/actl testdata/workflows/checkout.yml
Cloud identity (GCP & AWS)
In real CI, a login action (google-github-actions/auth, aws-actions/configure-aws-credentials)
plus id-token: write mints a GitHub-signed OIDC token and exchanges it for short-lived
cloud credentials. Locally there is no GitHub OIDC issuer, so that step can't federate —
it would fail and kill the job. actl intercepts it: rewrites it to a no-op and injects
your ambient credentials, so later steps run as you. The TUI prints a transparency line
— what the step would federate as vs the local identity it runs as — because you're testing
under your own permissions, not the workflow's federated scope.
GCP — actl injects your gcloud Application Default Credentials so later steps
(gcloud/gsutil, setup-gcloud, client libraries, terraform) run as you:
gcloud auth application-default login # once, so the ADC file exists
go run ./cmd/actl testdata/workflows/gcp-auth.yml
It mounts your ADC file read-only into the container and sets
GOOGLE_APPLICATION_CREDENTIALS (so client libraries and terraform discover it) plus
CLOUDSDK_AUTH_ACCESS_TOKEN (so gcloud/gsutil and setup-gcloud authenticate). Point
at a specific credential file with -gcp-credentials FILE, or leave the auth step
untouched (real federation) with -gcp-identity=false. The access token lives ~1h; the
credential file keeps client libraries working past that. actl doesn't inject a
project — that's the workflow's concern, exactly as on GitHub (the command's
--project, or a GOOGLE_CLOUD_PROJECT you pass via .env / -env). A gcloud call
with no project resolved fails the same way it would in CI ("Project id: 0 is invalid").
AWS — same shape: actl injects your ambient AWS credentials (env-only, no file
mount), resolved from the default profile / environment or -aws-profile NAME; the region
is injected when known. Disable with -aws-identity=false to leave the step untouched.
aws sso login # or any way your ambient credentials are established
go run ./cmd/actl -aws-profile dev testdata/workflows/aws-auth.yml
GCP and AWS are done; Azure is the remaining provider on the roadmap.
Roadmap
Done so far: library spike ✓ → fork + pause barrier ✓ → frontend-agnostic core ✓ → TUI
(step/inspect/shell/edit/re-run/breakpoints/run-to-cursor) ✓ → job selection + isolated
needs seeding ✓ → run-dependencies-then-debug (--with-deps) ✓ → remote uses:
(node / docker / composite) ✓ → workspace mount for local actions (-workdir) ✓ →
faithful actions/checkout (copies your local working tree) ✓ → secrets / vars / env from
act's dotenv triple ✓ → matrix selection + services line ✓ → .actl.yml config + -list
inventory ✓ → per-environment overlays ✓ → GitHub & runtime-context seeding ✓ → ambient
GCP and AWS identity substitution ✓.
Next: Azure identity → full multi-job graph → high-fidelity OIDC → headless / agent mode → upstream the hook(s).
License
MIT, like act.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
actl
command
Command actl is the TUI step-debugger for GitHub Actions workflows.
|
Command actl is the TUI step-debugger for GitHub Actions workflows. |
|
internal
|
|
|
config
Package config loads actl's optional project config file, `.actl.yml`.
|
Package config loads actl's optional project config file, `.actl.yml`. |
|
debugger
Package debugger: see session.go for the package doc.
|
Package debugger: see session.go for the package doc. |
|
tui
Package tui is the Bubble Tea front-end for actl.
|
Package tui is the Bubble Tea front-end for actl. |
|
workflow
Package workflow is a thin, intentionally boring wrapper around act's pkg/model.
|
Package workflow is a thin, intentionally boring wrapper around act's pkg/model. |