Run GitHub Actions workflows locally with act
Run the portable jobs of an existing workflow on this machine, see why the rest stay on GitHub, and move shell steps into shared scripts as you migrate.
Last updated
orgabot ci-actions runs the compatible parts of an existing GitHub Actions workflow on this machine, through one pinned version of act. It is a migration bridge. GitHub workflow YAML is not Orgabot's pipeline language; it is a source Orgabot can read, check, and partly run while jobs move to provider-neutral targets.
The adapter never runs a feature it cannot reproduce faithfully. Every job, and every entry of a job's matrix, is classified before anything executes: it either runs locally, or it is routed back to GitHub with the exact reasons, or the run is blocked.
Set up the pinned runtime
orgabot ci-actions doctor
orgabot ci-actions installdoctor checks the runtime and Docker and prints the exact fix for each failing check. install downloads act 0.2.89 for your platform, checks the archive against the SHA-256 recorded in Orgabot before extracting it, and records the binary's own hash. Before every run Orgabot re-hashes the binary and asks it for its version. An act that is merely on your PATH is never used.
If you cannot download from this machine, fetch the release archive yourself and pass it in: orgabot ci-actions install --archive act_Darwin_arm64.tar.gz. A file whose checksum does not match the pin is refused.
You need a running Docker Engine serving Linux containers (Docker Desktop or Colima). On Apple silicon the job containers run as linux/arm64, while GitHub-hosted Ubuntu runners are linux/amd64. doctor warns about this, and --container-architecture linux/amd64 emulates x64 when a toolchain depends on it.
See what can run locally
orgabot ci-actions analyze .github/workflows/ci.yml
orgabot ci-actions plan .github/workflows/ci.yml --event pushanalyze lists every target as local or remote. A remote target always shows its reasons, for example oidc: id-token: write requests a GitHub OIDC token or runs_on.macos_windows: runs on windows-latest. A local target lists the adaptations it relies on.
plan decides what one run would do. A local run is closed under needs: every job a selected job depends on runs in the same invocation, so outputs and results pass between them exactly as on GitHub. When a dependency must stay remote, the dependent is blocked rather than run with invented outputs. If you name a remote-only job with --job, the plan blocks and tells you to generate a dispatch wrapper.
Secrets and variables are never guessed. Map each one the local jobs read: --secret NPM_TOKEN=<orgabot secret handle> and --var REGION=us-east1. An unmapped one blocks the run instead of evaluating to an empty string. GITHUB_TOKEN is never provided locally.
Run it
orgabot ci-actions run .github/workflows/ci.yml --secret DEMO_TOKEN=<handle> --var GREETING=helloThe run tests the commit that is checked out. A worktree with uncommitted changes is refused, because act copies the working directory and the result would not describe any commit; pass --allow-dirty to record a dirty run on purpose.
Each run writes an evidence directory under ~/.orgabot/cicd/runs/<run id>/: the derived workflow that actually ran, the event payload built from the observed commit, act's JSON log, artifacts with their SHA-256, and result.json. The result records the act version and hash, the Docker engine, each runner image and its digest, per-job and per-matrix-entry status, job outputs (an output that copies github.sha or github.ref_name is filled from the observed commit on push and workflow_dispatch runs, never on pull_request), cancellation, and the targets that still have to run on GitHub. Press Ctrl+C to cancel: act is interrupted and the run is reported as cancelled.
A run is reported as succeeded only when act's log has a result for every local target. If act exits 0 but a target has no result, the run is failed and names that target. A --platform-image label=image value must be a plain runner label and a Docker image reference; anything else blocks the run before it starts.
What is never run locally
| Feature | Why it stays on GitHub |
|---|---|
environment: | Protection rules, required reviewers and environment secrets do not exist locally |
permissions: id-token: write, OIDC login actions | Orgabot never fabricates a GitHub OIDC token |
Write permissions, secrets.GITHUB_TOKEN | Only GitHub can issue its scoped, run-lifetime token |
| Signing secrets and signing actions | Signing keys stay with the authorized remote target |
| macOS and Windows runners | There is no container image, and act's host mode is never chosen automatically |
pull_request_target, workflow_run | They depend on GitHub's trust boundary or another run |
| Privileged containers, host-path volumes and bind mounts | No job gets --privileged, extra capabilities, host namespaces, or a host mount. Container options are parsed the way Docker parses them, so --mount type=bind, --volume=... and quoted forms are caught too |
| Container options Orgabot cannot screen | Options computed by an expression, options with unbalanced quotes, and flags outside the screened set (for example --tmpfs, --sysctl, -p) run on GitHub instead of being guessed safe |
Remote reusable workflows, secrets: inherit | They need GitHub's token and pass every secret |
The full list, with what act does and what Orgabot does for every feature, is the compatibility matrix: orgabot ci-actions matrix.
Local act, self-hosted runners, and GitHub-hosted runners are different modes
orgabot ci-actions modes prints the comparison. In short:
- Local act runs on this machine in unprivileged containers with CPU, memory and process limits. It has no GitHub token, no OIDC, and no environment protection, and its Ubuntu image only approximates GitHub's.
- A GitHub-managed self-hosted runner runs on a machine you operate, but GitHub issues its token, enforces environments, and provides OIDC.
- GitHub-hosted Actions run on GitHub's own VMs, with all of GitHub's controls.
A local pass is evidence about the steps. It never satisfies a protected-environment, OIDC, or token-permission control, whatever its exit code.
Send the remote part back to GitHub without re-running everything
orgabot ci-actions wrap .github/workflows/release.yml --job release --out .github/workflows/orgabot-dispatch-release.ymlInstead of dispatching the whole original workflow again, which would re-run the jobs you already ran locally, wrap writes a small workflow_dispatch workflow that contains only the remote jobs and any remote jobs they need. Permissions, environments, concurrency and OIDC steps are copied exactly as written, so GitHub still enforces them. Outputs the remote jobs read from a local job become typed inputs you pass at dispatch time. A split matrix keeps only its remote entries.
The wrapper is a proposal: review it before committing it. Orgabot never commits or dispatches it, and prints the dispatch command for you to approve. orgabot ci-actions wrap --check <wrapper> <workflow> reports whether the source workflow has changed since the wrapper was generated.
Move shell steps into shared scripts
orgabot ci-actions extract .github/workflows/ci.yml --job test
orgabot ci-actions extract .github/workflows/ci.yml --job test --write
orgabot ci-actions extract --checkextract moves a job's run: steps into scripts/ci/<workflow>/<job>/ and records each script as a provider-neutral target in scripts/ci/targets.json. Every ${{ }} expression the step used is passed in through the step's env:, and the command prints the replacement step to paste into the workflow. After that, the workflow and the provider-neutral target run the same file, so there is one command set to maintain.
Do it one step or one job at a time. extract --check fails if a script is missing, if the workflow step no longer calls its script, or if a workflow still carries an inline copy of an extracted body.