actl

module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 10, 2026 License: MIT

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 needs seeding, secrets / vars / env loading, a committable .actl.yml config with per-environment overlays, -list inventory, 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/model parses workflows; act/pkg/exprparser evaluates ${{ }}. Imported as-is.
  • Soft fork for the pause hook. act's per-step machinery is unexported, so a tiny patch interleaves a barrier common.Executor between steps and exposes a resume channel on runner.Config. The fork lives in third_party/act, wired in via a replace directive in go.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 — .secretssecrets.*, .varsvars.*, .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 a GITHUB_TOKEN in .secrets, else ambient gh auth token; the two stay equal as on GitHub. It is not auto-exported as $GITHUB_TOKEN (faithful — map it via env:). Heads-up: your token's scope differs from CI's ephemeral, repo-scoped one.
  • Workflow inputs-input NAME=VALUE (repeatable) for workflow_dispatch / workflow_call; act applies the declared default: and boolean typing itself, so you only supply the values you want to override.
  • Event payload-event-file PATH.json sets github.event.*.
  • github.* context-repository/-ref/-sha/-actor override the respective fields; otherwise repository/ref/sha are derived from your local git (origin remote, HEAD). actor, run_id, and run_number are 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.

GCPactl 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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL