mizchi/actrun

GitHub Actions compatible local runner

MoonBit

673

323 commits

updated Oct 3, 2026

See the code

README

actrun

A local GitHub Actions runner built with MoonBit. Run and debug GitHub Actions workflows locally with a gh-compatible CLI.

actrun keeps its release contract as close as possible to existing GitHub Actions semantics. Workflow YAML and action metadata stay on a GitHub-compatible surface, while WASM support is treated as a self-hosted runner optimization. See docs/public-api.md for the contract boundary and ADR 0001 for the rationale.

Install

# npx (no install required)
npx @mizchi/actrun workflow run .github/workflows/ci.yml

# curl (Linux / macOS)
curl -fsSL https://raw.githubusercontent.com/mizchi/actrun/main/install.sh | sh

# Docker
docker run --rm -v "$PWD":/workspace -w /workspace ghcr.io/mizchi/actrun workflow run .github/workflows/ci.yml

# npm global install
npm install -g @mizchi/actrun

# Nix (run without installing)
nix run github:mizchi/actrun -- workflow run .github/workflows/ci.yml

# Nix (install into profile)
nix profile install github:mizchi/actrun

# moon install
moon install mizchi/actrun/cmd/actrun

# Build from source
git clone https://github.com/mizchi/actrun.git && cd actrun
moon build src/cmd/actrun --target native

Nix

Run directly

nix run github:mizchi/actrun -- workflow run .github/workflows/ci.yml

Build from source

nix build github:mizchi/actrun
./result/bin/actrun workflow run .github/workflows/ci.yml

Development shell

With direnv (recommended)

With direnv and nix-direnv:

echo "use flake" > .envrc
direnv allow

Or without direnv:

nix develop

Adding the overlay to your flake.nix

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    actrun.url = "github:mizchi/actrun";
  };

  outputs = { nixpkgs, actrun, ... }:
    let
      system = "aarch64-darwin"; # or "x86_64-linux"
      pkgs = import nixpkgs {
        inherit system;
        overlays = [ actrun.overlays.default ];
      };
    in
    {
      packages.${system}.default = pkgs.actrun;
      devShells.${system}.default = pkgs.mkShell {
        packages = [ pkgs.actrun ];
      };
    };
}

Without flakes

# default.nix provides a ready-to-use derivation
let
  actrun = import (builtins.fetchTarball "https://github.com/mizchi/actrun/archive/main.tar.gz") { };
in
  actrun

Quick Start

# Run a workflow locally
actrun workflow run .github/workflows/ci.yml

# Show execution plan without running
actrun workflow run .github/workflows/ci.yml --dry-run

# Emit flow cache plan JSON for an external orchestrator
actrun workflow run .github/workflows/ci.yml \
  --dry-run \
  --json \
  --flow-cache-store /tmp/flow-cache.json \
  --flow-signature build=sig-build

# Skip actions not needed locally (e.g. setup tools already installed)
actrun workflow run .github/workflows/ci.yml \
  --skip-action actions/checkout \
  --skip-action extractions/setup-just

# Run in isolated worktree
actrun workflow run .github/workflows/ci.yml \
  --workspace-mode worktree

# Generate config file
actrun init

# View results
actrun run view run-1
actrun run logs run-1 --task build/test

Configuration

actrun init generates an actrun.toml in the current directory:

# Workspace mode: local, worktree, tmp, docker
workspace_mode = "local"

# Skip actions not needed locally
local_skip_actions = ["actions/checkout"]

# Trust all third-party actions without prompt
trust_actions = true

# Nix integration: "auto" (force), "off" (disable), or empty (auto-detect)
nix_mode = ""

# Additional nix packages
nix_packages = ["python312", "jq"]

# Container runtime: docker, podman, container, lima, nerdctl
container_runtime = "docker"

# Include uncommitted changes in worktree/tmp workspace
# include_dirty = true

# Default local GitHub context when `--event` is omitted
# [local_context]
# repository = "owner/repo"
# ref_name = "main"
# before_rev = "HEAD^"
# after_rev = "HEAD"
# actor = "your-name"

# Override actions with local commands
# [override."actions/setup-node"]
# run = "echo 'using local node' && node --version"

# Affected file patterns per workflow
# [affected."ci.yml"]
# patterns = ["src/**", "package.json"]

When --event is omitted, actrun auto-detects github.repository, github.ref_name, github.sha, and github.actor from the local git repository when possible. Use [local_context] only when you need to pin or override those values. See Local GitHub Context for precedence and examples.

CLI flags always override actrun.toml settings. See Cheatsheet for quick reference and Advanced Workflow for details.

Flow Cache

Use --flow-cache-store <path> together with one or more --flow-signature <job-or-task>=<fingerprint> flags to exchange task cache state with the embedded bitflow planner.

--dry-run --json includes a flow_cache.plan payload with per-task hit/miss information. A normal run writes successful task fingerprints back to the same store and persists both the plan and writeback result under run.json as flow_cache.plan and flow_cache.writeback.

CLI Reference

Workflow Commands

actrun workflow list                 # List workflows in .github/workflows/
actrun workflow run <workflow.yml>    # Run a workflow locally

Run Commands

actrun run list                      # List past runs
actrun run view <run-id>             # View run summary
actrun run view <run-id> --json      # View run as JSON
actrun run watch <run-id>            # Watch until completion
actrun run logs <run-id>             # View all logs
actrun run logs <run-id> --task <id> # View specific task log
actrun run download <run-id>         # Download all artifacts

Analysis Commands

# Lint: type check expressions and detect dead code
actrun lint                          # Lint all .github/workflows/*.yml
actrun lint .github/workflows/ci.yml # Lint a specific file
actrun lint --ignore W001            # Suppress a rule (repeatable)

# Visualize: render workflow job dependency graph
actrun viz .github/workflows/ci.yml              # ASCII art (terminal)
actrun viz .github/workflows/ci.yml --mermaid    # Mermaid text (for Markdown)
actrun viz .github/workflows/ci.yml --detail     # Mermaid with step subgraphs
actrun viz .github/workflows/ci.yml --svg        # SVG image
actrun viz .github/workflows/ci.yml --svg --theme github-light

Lint Diagnostics

RuleSeverityDescription
undefined-contexterrorUndefined context (e.g. foobar.x)
wrong-arityerrorWrong function arity (e.g. contains('one'))
unknown-functionerrorUnknown function (e.g. myFunc())
unknown-propertywarningUnknown property (e.g. github.nonexistent)
type-mismatchwarningComparing incompatible types
unreachable-stepwarningUnreachable step (if: false)
future-step-referrorReference to future step
undefined-step-referrorReference to undefined step
undefined-needserrorUndefined needs job reference
circular-needserrorCircular needs dependency
unused-outputswarningUnused job outputs
duplicate-step-iderrorDuplicate step IDs in same job
missing-runs-onerrorMissing runs-on
empty-joberrorEmpty job (no steps)
uses-and-runerrorStep has both uses and run
empty-matrixwarningMatrix with empty rows
invalid-useserrorInvalid uses syntax
invalid-globwarningInvalid glob pattern in trigger filter
redundant-conditionwarningAlways-true/false condition
script-injectionwarningScript injection risk (untrusted input in run:)
permissive-permissionswarningOverly permissive permissions
deprecated-commandwarningDeprecated workflow command (::set-output etc.)
missing-prt-permissionswarningpull_request_target without explicit permissions
if-alwayswarningBare always() — prefer success() || failure()
dangerous-checkout-in-prterrorCheckout PR head in pull_request_target
secrets-to-third-partywarningSecrets passed via env to third-party action
missing-timeoutwarningNo timeout-minutes (opt-in: --strict)
mutable-action-refwarningTag ref instead of SHA pin (opt-in: --online)
action-not-founderrorAction ref not found on GitHub (opt-in: --online)

Configure lint behavior in actrun.toml:

[lint]
preset = "default"  # default, strict, oss
ignore_rules = ["unknown-property", "unused-outputs"]
PresetIncludes
defaultAll rules except missing-timeout and online checks
strictdefault + missing-timeout
ossstrict + mutable-action-ref / action-not-found (network)

Visualization Example

$ actrun viz .github/workflows/release.yml

┌───────┐    ┌─────┐    ┌────────┐
│ build │    │ npm │    │ docker │
└───────┘    └─────┘    └────────┘
    │
    └────────────┐
                 │
            ┌─────────┐
            │ release │
            └─────────┘

Artifact & Cache Commands

actrun artifact list <run-id>                          # List artifacts
actrun artifact download <run-id> --name <name>        # Download artifact
actrun cache list                                      # List cache entries
actrun cache prune --key <key>                         # Delete cache entry

Workflow Run Flags

FlagDescription
--dry-runShow execution plan without running
--skip-action <pattern>Skip actions matching pattern (repeatable)
--workspace-mode <mode>worktree (default), local, tmp, docker
--repo <path>Run from a git repository
--event <path>Push event JSON file
--repository <owner/repo>GitHub repository name
--ref <ref>Git ref name
--run-root <path>Run record storage root
--nixForce nix wrapping for run steps
--no-nixDisable nix wrapping even if flake.nix/shell.nix exists
--nix-packages <pkgs>Ad-hoc nix packages (space-separated)
--container-runtime <name>Container runtime: docker, podman, container, lima, nerdctl
--wasm-runner <kind>Wasm runner kind: wasmtime, deno, v8
--affected [base]Only run if files matching patterns changed (see below)
--retryRe-run only failed jobs from the latest run
--include-dirtyInclude uncommitted changes in worktree/tmp workspace
--jsonJSON output for read commands and --dry-run

Affected Runs

Skip workflows when no relevant files have changed. Patterns are resolved in order:

  1. actrun.toml [affected."<workflow>"] patterns
  2. on:push:paths from the workflow file (automatic fallback)
# Compare against last successful run (default)
actrun ci.yml --affected

# Compare against a specific rev
actrun ci.yml --affected HEAD~3
actrun ci.yml --affected abc1234

# Preview what would happen (shows plan even if skipped)
actrun ci.yml --affected HEAD~1 --dry-run

Configure patterns in actrun.toml:

[affected."ci.yml"]
patterns = ["src/**", "package.json"]

[affected.".github/workflows/lint.yml"]
patterns = ["src/**", "*.config.*"]

If actrun.toml has no patterns, on:push:paths from the workflow is used automatically:

on:
  push:
    paths: ["src/**", "*.toml"]  # actrun --affected uses these

Workspace Modes

ModeDescription
localRun in-place in the current directory
worktreeCreate an isolated git worktree for execution (default)
tmpClone to a temp directory via git clone
dockerRun in a Docker container

Container Runtime

actrun supports multiple container runtimes for job container:, services:, and docker:// actions.

RuntimeBinaryNotes
dockerdockerDefault
podmanpodmanDocker-compatible CLI
containercontainerApple container runtime (macOS)
nerdctlnerdctlcontainerd CLI
limalima nerdctlLima VM with nerdctl (wrapper script auto-generated)
# CLI flag
actrun workflow run ci.yml --container-runtime podman

# actrun.toml
container_runtime = "podman"

# Environment variable (also works)
ACTRUN_CONTAINER_RUNTIME=podman actrun workflow run ci.yml

Supported GitHub Actions

Builtin Actions (deterministic emulation)

ActionSupported Inputs
actions/checkout@*path, ref, fetch-depth, clean, sparse-checkout, submodules, lfs, fetch-tags, persist-credentials, set-safe-directory, show-progress
actions/upload-artifact@*name, path, if-no-files-found, overwrite, include-hidden-files
actions/download-artifact@*name, path, pattern, merge-multiple
actions/cache@*key, path, restore-keys, lookup-only, fail-on-cache-miss
actions/cache/save@*key, path
actions/cache/restore@*key, path, restore-keys, lookup-only, fail-on-cache-miss
actions/setup-node@*node-version, node-version-file, cache, registry-url, always-auth, scope

Remote Actions (fetch + execute)

  • GitHub repo node actions with pre/main/post lifecycle
  • GitHub repo docker actions with pre-entrypoint/entrypoint/post-entrypoint lifecycle
  • Composite actions (local and remote)
  • docker://image direct execution

Self-Hosted WASM Optimization

  • A self-hosted runner may prefer a sibling *.wasm file next to a standard node* action runs.main
  • The same action still runs on GitHub Actions through the normal JS fallback path
  • The runtime family is selected with --wasm-runner / ACTRUN_WASM_RUNNER

Protocol extensions such as wasm://... and runs-on: wasi are experimental / internal and are not part of the release contract. See docs/public-api.md for details.

Local-Only Execution Flag

actrun sets ACTRUN_LOCAL=true in the execution environment. Use this in if: conditions to skip steps locally or run steps only locally:

steps:
  # Skipped when running locally (runs on GitHub Actions)
  - uses: actions/checkout@v5
    if: ${{ !env.ACTRUN_LOCAL }}

  # Runs only locally (skipped on GitHub Actions)
  - run: echo "local debug info"
    if: ${{ env.ACTRUN_LOCAL }}

On GitHub Actions, ACTRUN_LOCAL is not set, so !env.ACTRUN_LOCAL evaluates to true and all steps run normally.

Action Overrides

Replace specific uses: action steps with custom run: commands via actrun.toml. This is useful when you have tools installed locally and want to skip the action's setup logic.

[override."actions/setup-node"]
run = "echo 'using local node' && node --version"

When a workflow step matches uses: actions/setup-node@*, actrun replaces it with the specified run: command before execution.

Combine with local_skip_actions for full control:

local_skip_actions = ["actions/checkout"]

[override."actions/setup-node"]
run = "echo 'using local node'"

[override."actions/setup-python"]
run = "python3 --version"

Secrets & Variables

# Provide secrets via environment variables
ACTRUN_SECRET_MY_TOKEN=xxx actrun workflow run ci.yml

# Provide variables
ACTRUN_VAR_MY_VAR=value actrun workflow run ci.yml

Secrets are automatically masked in stdout, stderr, logs, and run store. The ::add-mask:: workflow command is also supported.

Environment Variables

VariableDescription
ACTRUN_SECRET_<NAME>${{ secrets.<name> }}
ACTRUN_VAR_<NAME>${{ vars.<name> }}
ACTRUN_NODE_BINNode.js binary path
ACTRUN_DOCKER_BINDocker binary path
ACTRUN_WASM_RUNNERWasm runner kind: wasmtime, deno, v8
ACTRUN_WASM_BINWasm runtime binary (default: wasmtime)
ACTRUN_GIT_BINGit binary path
ACTRUN_GITHUB_BASE_URLGitHub API base URL
ACTRUN_ARTIFACT_ROOTArtifact storage root
ACTRUN_CACHE_ROOTCache storage root
ACTRUN_GITHUB_ACTION_CACHE_ROOTRemote action cache root
ACTRUN_ACTION_REGISTRY_ROOTCustom registry root
ACTRUN_NIXSet to false to disable nix wrapping

ACTRUN_WASM_RUNNER を指定した場合、default bin は wasmtime / deno / ACTRUN_NODE_BIN (v8) に切り替わります。ACTRUN_WASM_BIN を併用すると、runner kind は固定したまま実行 binary だけ上書きできます。

Nix Integration

actrun automatically detects flake.nix or shell.nix in the workspace root and wraps run: steps in the corresponding nix environment. This lets workflows written for ubuntu-latest run locally with nix-managed toolchains.

Auto-detection

ConditionWrapping
flake.nix existsnix develop --command <shell> <script>
shell.nix existsnix-shell --run '<shell> <script>'
Neither existsNo wrapping (host environment)

Detection requires nix to be installed. If nix is not found, wrapping is silently skipped.

Examples

# Auto-detect flake.nix / shell.nix
actrun workflow run .github/workflows/ci.yml

# Disable nix wrapping
actrun workflow run .github/workflows/ci.yml --no-nix

# Ad-hoc packages without flake.nix
actrun workflow run .github/workflows/ci.yml --nix-packages "python312 jq"

# Disable via environment variable
ACTRUN_NIX=false actrun workflow run .github/workflows/ci.yml

Typical flake.nix for Rust

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
  outputs = { self, nixpkgs }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
      forAllSystems = nixpkgs.lib.genAttrs systems;
    in {
      devShells = forAllSystems (system:
        let pkgs = nixpkgs.legacyPackages.${system};
        in { default = pkgs.mkShell { packages = [ pkgs.rustc pkgs.cargo ]; }; });
    };
}

Typical flake.nix for Python + uv

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
  outputs = { self, nixpkgs }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
      forAllSystems = nixpkgs.lib.genAttrs systems;
    in {
      devShells = forAllSystems (system:
        let pkgs = nixpkgs.legacyPackages.${system};
        in { default = pkgs.mkShell { packages = [ pkgs.python312 pkgs.uv ]; }; });
    };
}

Notes

  • Only run: steps are wrapped. uses: action steps are not affected.
  • Job container: steps skip nix wrapping (container has its own environment).
  • nix develop / nix-shell is invoked per step, so the nix environment is consistent across steps.

Workflow Features

  • Push trigger filter (branches, paths)
  • strategy.matrix (axes, include, exclude, fail-fast, max-parallel)
  • Job/step if conditions (success(), always(), failure(), cancelled())
  • needs dependencies with output/result propagation
  • Reusable workflows (workflow_call) with inputs, outputs, secrets, secrets: inherit, nested expansion
  • Job container and services with Docker networking
  • Expression functions: contains, startsWith, endsWith, fromJSON, toJSON, hashFiles
  • File commands: GITHUB_ENV, GITHUB_PATH, GITHUB_OUTPUT, GITHUB_STEP_SUMMARY
  • Shell support: bash, sh, pwsh, custom templates ({0})
  • step.continue-on-error, steps.*.outcome / steps.*.conclusion

Performance

Benchmark on Apple Silicon (M-series):

ModeStartupCPU (Node.js)Write 1k files
local~0.13s644ms52ms
nix-packages~0.70s629ms47ms
apple-container~0.93s502ms14ms
  • local — lowest overhead, best for fast iteration
  • nix-packages — +0.6s startup for nix develop; execution speed identical to local
  • apple-container — +0.9s startup; many-file I/O is 3-4x faster (ext4 vs APFS metadata)

See docs/perf.md for full benchmark details.

# Try it yourself
nix run github:mizchi/actrun -- workflow run .github/workflows/ci.yml

Development

just              # check + test
just fmt          # format code
just check        # type check
just test         # run tests
just e2e          # run E2E scenarios
just release-check  # fmt + info + check + test + e2e
just build-js     # build the npm CLI bundle (dist/actrun.js) and library (lib/actrun.js)

Live Compatibility Testing

# One-shot: dispatch, wait, download, compare
just gha-compat-live compat-checkout-artifact.yml

# Step by step
just gha-compat-dispatch compat-checkout-artifact.yml
just gha-compat-download <run-id>
just gha-compat-compare compat-checkout-artifact.yml _build/gha-compat/<run-id>

Architecture

FilePurpose
src/lib.mbtContract types
src/parser.mbtWorkflow YAML parser
src/trigger.mbtPush trigger matcher
src/lowering.mbtBitflow IR lowering, action/reusable workflow expansion
src/executor.mbtNative host executor
src/runtime.mbtGit workspace materialization
src/lint/Expression parser, type checker, dead code detection, workflow visualization
src/cmd/actrun/main.mbtCLI entry point
testdata/Compatibility fixtures

Prior Art

  • actionlint — Static checker for GitHub Actions workflow files. actrun lint is inspired by its rule design and type system.

License

Apache-2.0

ci
github
github-actions
moonbit

mizchi/actrun

GitHub Actions compatible local runner

MoonBit

673

323 commits

updated Oct 3, 2026

See the code

README

actrun

A local GitHub Actions runner built with MoonBit. Run and debug GitHub Actions workflows locally with a gh-compatible CLI.

actrun keeps its release contract as close as possible to existing GitHub Actions semantics. Workflow YAML and action metadata stay on a GitHub-compatible surface, while WASM support is treated as a self-hosted runner optimization. See docs/public-api.md for the contract boundary and ADR 0001 for the rationale.

Install

# npx (no install required)
npx @mizchi/actrun workflow run .github/workflows/ci.yml

# curl (Linux / macOS)
curl -fsSL https://raw.githubusercontent.com/mizchi/actrun/main/install.sh | sh

# Docker
docker run --rm -v "$PWD":/workspace -w /workspace ghcr.io/mizchi/actrun workflow run .github/workflows/ci.yml

# npm global install
npm install -g @mizchi/actrun

# Nix (run without installing)
nix run github:mizchi/actrun -- workflow run .github/workflows/ci.yml

# Nix (install into profile)
nix profile install github:mizchi/actrun

# moon install
moon install mizchi/actrun/cmd/actrun

# Build from source
git clone https://github.com/mizchi/actrun.git && cd actrun
moon build src/cmd/actrun --target native

Nix

Run directly

nix run github:mizchi/actrun -- workflow run .github/workflows/ci.yml

Build from source

nix build github:mizchi/actrun
./result/bin/actrun workflow run .github/workflows/ci.yml

Development shell

With direnv (recommended)

With direnv and nix-direnv:

echo "use flake" > .envrc
direnv allow

Or without direnv:

nix develop

Adding the overlay to your flake.nix

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    actrun.url = "github:mizchi/actrun";
  };

  outputs = { nixpkgs, actrun, ... }:
    let
      system = "aarch64-darwin"; # or "x86_64-linux"
      pkgs = import nixpkgs {
        inherit system;
        overlays = [ actrun.overlays.default ];
      };
    in
    {
      packages.${system}.default = pkgs.actrun;
      devShells.${system}.default = pkgs.mkShell {
        packages = [ pkgs.actrun ];
      };
    };
}

Without flakes

# default.nix provides a ready-to-use derivation
let
  actrun = import (builtins.fetchTarball "https://github.com/mizchi/actrun/archive/main.tar.gz") { };
in
  actrun

Quick Start

# Run a workflow locally
actrun workflow run .github/workflows/ci.yml

# Show execution plan without running
actrun workflow run .github/workflows/ci.yml --dry-run

# Emit flow cache plan JSON for an external orchestrator
actrun workflow run .github/workflows/ci.yml \
  --dry-run \
  --json \
  --flow-cache-store /tmp/flow-cache.json \
  --flow-signature build=sig-build

# Skip actions not needed locally (e.g. setup tools already installed)
actrun workflow run .github/workflows/ci.yml \
  --skip-action actions/checkout \
  --skip-action extractions/setup-just

# Run in isolated worktree
actrun workflow run .github/workflows/ci.yml \
  --workspace-mode worktree

# Generate config file
actrun init

# View results
actrun run view run-1
actrun run logs run-1 --task build/test

Configuration

actrun init generates an actrun.toml in the current directory:

# Workspace mode: local, worktree, tmp, docker
workspace_mode = "local"

# Skip actions not needed locally
local_skip_actions = ["actions/checkout"]

# Trust all third-party actions without prompt
trust_actions = true

# Nix integration: "auto" (force), "off" (disable), or empty (auto-detect)
nix_mode = ""

# Additional nix packages
nix_packages = ["python312", "jq"]

# Container runtime: docker, podman, container, lima, nerdctl
container_runtime = "docker"

# Include uncommitted changes in worktree/tmp workspace
# include_dirty = true

# Default local GitHub context when `--event` is omitted
# [local_context]
# repository = "owner/repo"
# ref_name = "main"
# before_rev = "HEAD^"
# after_rev = "HEAD"
# actor = "your-name"

# Override actions with local commands
# [override."actions/setup-node"]
# run = "echo 'using local node' && node --version"

# Affected file patterns per workflow
# [affected."ci.yml"]
# patterns = ["src/**", "package.json"]

When --event is omitted, actrun auto-detects github.repository, github.ref_name, github.sha, and github.actor from the local git repository when possible. Use [local_context] only when you need to pin or override those values. See Local GitHub Context for precedence and examples.

CLI flags always override actrun.toml settings. See Cheatsheet for quick reference and Advanced Workflow for details.

Flow Cache

Use --flow-cache-store <path> together with one or more --flow-signature <job-or-task>=<fingerprint> flags to exchange task cache state with the embedded bitflow planner.

--dry-run --json includes a flow_cache.plan payload with per-task hit/miss information. A normal run writes successful task fingerprints back to the same store and persists both the plan and writeback result under run.json as flow_cache.plan and flow_cache.writeback.

CLI Reference

Workflow Commands

actrun workflow list                 # List workflows in .github/workflows/
actrun workflow run <workflow.yml>    # Run a workflow locally

Run Commands

actrun run list                      # List past runs
actrun run view <run-id>             # View run summary
actrun run view <run-id> --json      # View run as JSON
actrun run watch <run-id>            # Watch until completion
actrun run logs <run-id>             # View all logs
actrun run logs <run-id> --task <id> # View specific task log
actrun run download <run-id>         # Download all artifacts

Analysis Commands

# Lint: type check expressions and detect dead code
actrun lint                          # Lint all .github/workflows/*.yml
actrun lint .github/workflows/ci.yml # Lint a specific file
actrun lint --ignore W001            # Suppress a rule (repeatable)

# Visualize: render workflow job dependency graph
actrun viz .github/workflows/ci.yml              # ASCII art (terminal)
actrun viz .github/workflows/ci.yml --mermaid    # Mermaid text (for Markdown)
actrun viz .github/workflows/ci.yml --detail     # Mermaid with step subgraphs
actrun viz .github/workflows/ci.yml --svg        # SVG image
actrun viz .github/workflows/ci.yml --svg --theme github-light

Lint Diagnostics

RuleSeverityDescription
undefined-contexterrorUndefined context (e.g. foobar.x)
wrong-arityerrorWrong function arity (e.g. contains('one'))
unknown-functionerrorUnknown function (e.g. myFunc())
unknown-propertywarningUnknown property (e.g. github.nonexistent)
type-mismatchwarningComparing incompatible types
unreachable-stepwarningUnreachable step (if: false)
future-step-referrorReference to future step
undefined-step-referrorReference to undefined step
undefined-needserrorUndefined needs job reference
circular-needserrorCircular needs dependency
unused-outputswarningUnused job outputs
duplicate-step-iderrorDuplicate step IDs in same job
missing-runs-onerrorMissing runs-on
empty-joberrorEmpty job (no steps)
uses-and-runerrorStep has both uses and run
empty-matrixwarningMatrix with empty rows
invalid-useserrorInvalid uses syntax
invalid-globwarningInvalid glob pattern in trigger filter
redundant-conditionwarningAlways-true/false condition
script-injectionwarningScript injection risk (untrusted input in run:)
permissive-permissionswarningOverly permissive permissions
deprecated-commandwarningDeprecated workflow command (::set-output etc.)
missing-prt-permissionswarningpull_request_target without explicit permissions
if-alwayswarningBare always() — prefer success() || failure()
dangerous-checkout-in-prterrorCheckout PR head in pull_request_target
secrets-to-third-partywarningSecrets passed via env to third-party action
missing-timeoutwarningNo timeout-minutes (opt-in: --strict)
mutable-action-refwarningTag ref instead of SHA pin (opt-in: --online)
action-not-founderrorAction ref not found on GitHub (opt-in: --online)

Configure lint behavior in actrun.toml:

[lint]
preset = "default"  # default, strict, oss
ignore_rules = ["unknown-property", "unused-outputs"]
PresetIncludes
defaultAll rules except missing-timeout and online checks
strictdefault + missing-timeout
ossstrict + mutable-action-ref / action-not-found (network)

Visualization Example

$ actrun viz .github/workflows/release.yml

┌───────┐    ┌─────┐    ┌────────┐
│ build │    │ npm │    │ docker │
└───────┘    └─────┘    └────────┘
    │
    └────────────┐
                 │
            ┌─────────┐
            │ release │
            └─────────┘

Artifact & Cache Commands

actrun artifact list <run-id>                          # List artifacts
actrun artifact download <run-id> --name <name>        # Download artifact
actrun cache list                                      # List cache entries
actrun cache prune --key <key>                         # Delete cache entry

Workflow Run Flags

FlagDescription
--dry-runShow execution plan without running
--skip-action <pattern>Skip actions matching pattern (repeatable)
--workspace-mode <mode>worktree (default), local, tmp, docker
--repo <path>Run from a git repository
--event <path>Push event JSON file
--repository <owner/repo>GitHub repository name
--ref <ref>Git ref name
--run-root <path>Run record storage root
--nixForce nix wrapping for run steps
--no-nixDisable nix wrapping even if flake.nix/shell.nix exists
--nix-packages <pkgs>Ad-hoc nix packages (space-separated)
--container-runtime <name>Container runtime: docker, podman, container, lima, nerdctl
--wasm-runner <kind>Wasm runner kind: wasmtime, deno, v8
--affected [base]Only run if files matching patterns changed (see below)
--retryRe-run only failed jobs from the latest run
--include-dirtyInclude uncommitted changes in worktree/tmp workspace
--jsonJSON output for read commands and --dry-run

Affected Runs

Skip workflows when no relevant files have changed. Patterns are resolved in order:

  1. actrun.toml [affected."<workflow>"] patterns
  2. on:push:paths from the workflow file (automatic fallback)
# Compare against last successful run (default)
actrun ci.yml --affected

# Compare against a specific rev
actrun ci.yml --affected HEAD~3
actrun ci.yml --affected abc1234

# Preview what would happen (shows plan even if skipped)
actrun ci.yml --affected HEAD~1 --dry-run

Configure patterns in actrun.toml:

[affected."ci.yml"]
patterns = ["src/**", "package.json"]

[affected.".github/workflows/lint.yml"]
patterns = ["src/**", "*.config.*"]

If actrun.toml has no patterns, on:push:paths from the workflow is used automatically:

on:
  push:
    paths: ["src/**", "*.toml"]  # actrun --affected uses these

Workspace Modes

ModeDescription
localRun in-place in the current directory
worktreeCreate an isolated git worktree for execution (default)
tmpClone to a temp directory via git clone
dockerRun in a Docker container

Container Runtime

actrun supports multiple container runtimes for job container:, services:, and docker:// actions.

RuntimeBinaryNotes
dockerdockerDefault
podmanpodmanDocker-compatible CLI
containercontainerApple container runtime (macOS)
nerdctlnerdctlcontainerd CLI
limalima nerdctlLima VM with nerdctl (wrapper script auto-generated)
# CLI flag
actrun workflow run ci.yml --container-runtime podman

# actrun.toml
container_runtime = "podman"

# Environment variable (also works)
ACTRUN_CONTAINER_RUNTIME=podman actrun workflow run ci.yml

Supported GitHub Actions

Builtin Actions (deterministic emulation)

ActionSupported Inputs
actions/checkout@*path, ref, fetch-depth, clean, sparse-checkout, submodules, lfs, fetch-tags, persist-credentials, set-safe-directory, show-progress
actions/upload-artifact@*name, path, if-no-files-found, overwrite, include-hidden-files
actions/download-artifact@*name, path, pattern, merge-multiple
actions/cache@*key, path, restore-keys, lookup-only, fail-on-cache-miss
actions/cache/save@*key, path
actions/cache/restore@*key, path, restore-keys, lookup-only, fail-on-cache-miss
actions/setup-node@*node-version, node-version-file, cache, registry-url, always-auth, scope

Remote Actions (fetch + execute)

  • GitHub repo node actions with pre/main/post lifecycle
  • GitHub repo docker actions with pre-entrypoint/entrypoint/post-entrypoint lifecycle
  • Composite actions (local and remote)
  • docker://image direct execution

Self-Hosted WASM Optimization

  • A self-hosted runner may prefer a sibling *.wasm file next to a standard node* action runs.main
  • The same action still runs on GitHub Actions through the normal JS fallback path
  • The runtime family is selected with --wasm-runner / ACTRUN_WASM_RUNNER

Protocol extensions such as wasm://... and runs-on: wasi are experimental / internal and are not part of the release contract. See docs/public-api.md for details.

Local-Only Execution Flag

actrun sets ACTRUN_LOCAL=true in the execution environment. Use this in if: conditions to skip steps locally or run steps only locally:

steps:
  # Skipped when running locally (runs on GitHub Actions)
  - uses: actions/checkout@v5
    if: ${{ !env.ACTRUN_LOCAL }}

  # Runs only locally (skipped on GitHub Actions)
  - run: echo "local debug info"
    if: ${{ env.ACTRUN_LOCAL }}

On GitHub Actions, ACTRUN_LOCAL is not set, so !env.ACTRUN_LOCAL evaluates to true and all steps run normally.

Action Overrides

Replace specific uses: action steps with custom run: commands via actrun.toml. This is useful when you have tools installed locally and want to skip the action's setup logic.

[override."actions/setup-node"]
run = "echo 'using local node' && node --version"

When a workflow step matches uses: actions/setup-node@*, actrun replaces it with the specified run: command before execution.

Combine with local_skip_actions for full control:

local_skip_actions = ["actions/checkout"]

[override."actions/setup-node"]
run = "echo 'using local node'"

[override."actions/setup-python"]
run = "python3 --version"

Secrets & Variables

# Provide secrets via environment variables
ACTRUN_SECRET_MY_TOKEN=xxx actrun workflow run ci.yml

# Provide variables
ACTRUN_VAR_MY_VAR=value actrun workflow run ci.yml

Secrets are automatically masked in stdout, stderr, logs, and run store. The ::add-mask:: workflow command is also supported.

Environment Variables

VariableDescription
ACTRUN_SECRET_<NAME>${{ secrets.<name> }}
ACTRUN_VAR_<NAME>${{ vars.<name> }}
ACTRUN_NODE_BINNode.js binary path
ACTRUN_DOCKER_BINDocker binary path
ACTRUN_WASM_RUNNERWasm runner kind: wasmtime, deno, v8
ACTRUN_WASM_BINWasm runtime binary (default: wasmtime)
ACTRUN_GIT_BINGit binary path
ACTRUN_GITHUB_BASE_URLGitHub API base URL
ACTRUN_ARTIFACT_ROOTArtifact storage root
ACTRUN_CACHE_ROOTCache storage root
ACTRUN_GITHUB_ACTION_CACHE_ROOTRemote action cache root
ACTRUN_ACTION_REGISTRY_ROOTCustom registry root
ACTRUN_NIXSet to false to disable nix wrapping

ACTRUN_WASM_RUNNER を指定した場合、default bin は wasmtime / deno / ACTRUN_NODE_BIN (v8) に切り替わります。ACTRUN_WASM_BIN を併用すると、runner kind は固定したまま実行 binary だけ上書きできます。

Nix Integration

actrun automatically detects flake.nix or shell.nix in the workspace root and wraps run: steps in the corresponding nix environment. This lets workflows written for ubuntu-latest run locally with nix-managed toolchains.

Auto-detection

ConditionWrapping
flake.nix existsnix develop --command <shell> <script>
shell.nix existsnix-shell --run '<shell> <script>'
Neither existsNo wrapping (host environment)

Detection requires nix to be installed. If nix is not found, wrapping is silently skipped.

Examples

# Auto-detect flake.nix / shell.nix
actrun workflow run .github/workflows/ci.yml

# Disable nix wrapping
actrun workflow run .github/workflows/ci.yml --no-nix

# Ad-hoc packages without flake.nix
actrun workflow run .github/workflows/ci.yml --nix-packages "python312 jq"

# Disable via environment variable
ACTRUN_NIX=false actrun workflow run .github/workflows/ci.yml

Typical flake.nix for Rust

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
  outputs = { self, nixpkgs }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
      forAllSystems = nixpkgs.lib.genAttrs systems;
    in {
      devShells = forAllSystems (system:
        let pkgs = nixpkgs.legacyPackages.${system};
        in { default = pkgs.mkShell { packages = [ pkgs.rustc pkgs.cargo ]; }; });
    };
}

Typical flake.nix for Python + uv

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
  outputs = { self, nixpkgs }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
      forAllSystems = nixpkgs.lib.genAttrs systems;
    in {
      devShells = forAllSystems (system:
        let pkgs = nixpkgs.legacyPackages.${system};
        in { default = pkgs.mkShell { packages = [ pkgs.python312 pkgs.uv ]; }; });
    };
}

Notes

  • Only run: steps are wrapped. uses: action steps are not affected.
  • Job container: steps skip nix wrapping (container has its own environment).
  • nix develop / nix-shell is invoked per step, so the nix environment is consistent across steps.

Workflow Features

  • Push trigger filter (branches, paths)
  • strategy.matrix (axes, include, exclude, fail-fast, max-parallel)
  • Job/step if conditions (success(), always(), failure(), cancelled())
  • needs dependencies with output/result propagation
  • Reusable workflows (workflow_call) with inputs, outputs, secrets, secrets: inherit, nested expansion
  • Job container and services with Docker networking
  • Expression functions: contains, startsWith, endsWith, fromJSON, toJSON, hashFiles
  • File commands: GITHUB_ENV, GITHUB_PATH, GITHUB_OUTPUT, GITHUB_STEP_SUMMARY
  • Shell support: bash, sh, pwsh, custom templates ({0})
  • step.continue-on-error, steps.*.outcome / steps.*.conclusion

Performance

Benchmark on Apple Silicon (M-series):

ModeStartupCPU (Node.js)Write 1k files
local~0.13s644ms52ms
nix-packages~0.70s629ms47ms
apple-container~0.93s502ms14ms
  • local — lowest overhead, best for fast iteration
  • nix-packages — +0.6s startup for nix develop; execution speed identical to local
  • apple-container — +0.9s startup; many-file I/O is 3-4x faster (ext4 vs APFS metadata)

See docs/perf.md for full benchmark details.

# Try it yourself
nix run github:mizchi/actrun -- workflow run .github/workflows/ci.yml

Development

just              # check + test
just fmt          # format code
just check        # type check
just test         # run tests
just e2e          # run E2E scenarios
just release-check  # fmt + info + check + test + e2e
just build-js     # build the npm CLI bundle (dist/actrun.js) and library (lib/actrun.js)

Live Compatibility Testing

# One-shot: dispatch, wait, download, compare
just gha-compat-live compat-checkout-artifact.yml

# Step by step
just gha-compat-dispatch compat-checkout-artifact.yml
just gha-compat-download <run-id>
just gha-compat-compare compat-checkout-artifact.yml _build/gha-compat/<run-id>

Architecture

FilePurpose
src/lib.mbtContract types
src/parser.mbtWorkflow YAML parser
src/trigger.mbtPush trigger matcher
src/lowering.mbtBitflow IR lowering, action/reusable workflow expansion
src/executor.mbtNative host executor
src/runtime.mbtGit workspace materialization
src/lint/Expression parser, type checker, dead code detection, workflow visualization
src/cmd/actrun/main.mbtCLI entry point
testdata/Compatibility fixtures

Prior Art

  • actionlint — Static checker for GitHub Actions workflow files. actrun lint is inspired by its rule design and type system.

License

Apache-2.0

ci
github
github-actions
moonbit

Project health

Latest release

0.32.0 · Sep 18, 2026

Releases this year

17

Significant stargazers

(top 24 of 31)

kazuya kawaguchi

4,359 followers · starred Apr 2026

Yudai Takada

119 followers · starred Mar 2026

Hongbo Zhang

1,741 followers · starred Mar 2026

IKEDA Sho

664 followers · starred Mar 2026