* fix(executor): propagate composite action outputs back to caller
It turns out that execute_composite_action() was happily running all
the internal steps of a composite action, correctly tracking their
outputs in composite_step_outputs, and then... just throwing all of
that away. The action.yml `outputs:` section — the whole reason
composite actions *have* a return path — was never read or evaluated.
So ${{ steps.my-composite.outputs.whatever }} always resolved to
empty string. Inputs worked fine. The internal steps ran fine. The
output values were right there in memory. Nobody bothered to connect
the last wire.
Add propagate_composite_outputs() which reads the action's outputs
section after the step loop, evaluates each value expression against
the composite's internal step context, and writes the results to the
caller's GITHUB_OUTPUT file. The existing apply_step_environment_updates
pipeline then picks them up naturally — no changes to StepResult or
process_outcome needed.
Also wire this into the early-return failure path so partial outputs
are still available when a composite step fails.
* fix(executor): pass actual job status to composite output propagation
The previous commit (1119f63) added propagate_composite_outputs()
to resolve composite action outputs and write them to the caller's
GITHUB_OUTPUT file. Good fix, but it hardcoded job_status to
"success" in the ExpressionContext.
It turns out that when a composite step *fails*, record_step_status
has already flipped composite_job_status to "failure" — but the
output propagation on the failure short-circuit path was still
cheerfully telling the expression evaluator everything was fine.
Any output expression using success() or failure() builtins would
evaluate with the wrong answer.
Pass the actual composite_job_status to propagate_composite_outputs
instead of lying about it. While at it, add tests for the failure
path (partial outputs from steps that ran before the failure) and
for referencing steps that never existed (should resolve to empty,
not panic).
* fix(executor): handle multiline values in composite output propagation
It turns out that propagate_composite_outputs was writing *all* values
using simple key=value format, which silently corrupts the
GITHUB_OUTPUT file when a resolved expression contains newlines. The
second line onward becomes unparseable garbage, and downstream steps
quietly get empty outputs.
This is not great, especially since parse_github_kv_file already
supports the heredoc format (key<<EOF\nvalue\nEOF) on the read side.
The writer just never bothered to emit it.
Switch to heredoc format when the value contains '\n', and log a
debug diagnostic when the GITHUB_OUTPUT file can't be opened instead
of silently swallowing the error. Add a round-trip test that verifies
multiline values survive the write-then-parse cycle.
* fix(executor): use unique heredoc delimiter and handle write errors in composite output propagation
The heredoc format for multiline composite outputs was using a
hardcoded "EOF" delimiter. If an output value happened to contain
a line that was literally "EOF", parse_github_kv_file would
prematurely terminate the heredoc and silently truncate the value.
This is not great.
On top of that, every write!() call was discarding its Result with
`let _ = ...`, so a partial I/O failure would produce a corrupt
GITHUB_OUTPUT file that the caller would happily misparse.
Replace the hardcoded delimiter with generate_heredoc_delimiter(),
which starts with "ghadelimiter" and appends _1, _2, etc. until
it finds one that doesn't collide with any line in the value. This
matches what real GitHub Actions does (they use UUIDs, but the
principle is the same). Chain the writes with and_then() so the
first failure logs a debug message and breaks out of the loop.
While at it, add tests for delimiter collision and the delimiter
generator itself.
* .gitignore
WRKFLW
A command-line tool for validating and executing GitHub Actions workflows locally. Test your workflows on your machine before pushing to GitHub.
Features
- TUI interface — interactive terminal UI for browsing, running, and monitoring workflows
- Workflow validation — syntax checks, structural validation, and composite action input cross-checking with CI/CD-friendly exit codes
- Local execution — run workflows using Docker, Podman, or emulation mode (no containers)
- Job selection — run individual jobs with
--jobflag or via TUI job selection mode - Job dependency resolution — automatic ordering based on
needswith parallel execution of independent jobs - Action support — Docker container actions, JavaScript actions, composite actions, and local actions
- Reusable workflows — execute caller jobs via
jobs.<id>.uses(local orowner/repo/path@ref) - GitHub context emulation — environment variables,
GITHUB_OUTPUT,GITHUB_ENV,GITHUB_PATH,GITHUB_STEP_SUMMARY - Matrix builds — full support for
include,exclude,max-parallel, andfail-fast - Secrets management — multiple providers (env, file, Vault, AWS, Azure, GCP) with masking and encryption
- Remote triggering — trigger
workflow_dispatchruns on GitHub or GitLab pipelines - GitLab support — validate and trigger GitLab CI pipelines
Installation
cargo install wrkflw
Or build from source:
git clone https://github.com/bahdotsh/wrkflw.git
cd wrkflw
cargo build --release
Quick Start
# Launch the TUI (auto-detects .github/workflows)
wrkflw
# Validate workflows
wrkflw validate
# Run a workflow
wrkflw run .github/workflows/ci.yml
Usage
Validation
# Validate all workflows in .github/workflows
wrkflw validate
# Validate specific files or directories
wrkflw validate path/to/workflow.yml
wrkflw validate path/to/workflows/
# Validate multiple paths
wrkflw validate flow-1.yml flow-2.yml path/to/workflows/
# GitLab pipelines
wrkflw validate .gitlab-ci.yml --gitlab
# Verbose output
wrkflw validate --verbose path/to/workflow.yml
Exit codes: 0 = all valid, 1 = validation failures, 2 = usage error. Use --no-exit-code to disable.
Execution
# Run with Docker (default)
wrkflw run .github/workflows/ci.yml
# Run with Podman
wrkflw run --runtime podman .github/workflows/ci.yml
# Run in emulation mode (no containers)
wrkflw run --runtime emulation .github/workflows/ci.yml
# Run a specific job
wrkflw run --job build .github/workflows/ci.yml
# List jobs in a workflow
wrkflw run --jobs .github/workflows/ci.yml
# Preserve failed containers for debugging
wrkflw run --preserve-containers-on-failure .github/workflows/ci.yml
TUI
# Open TUI with default directory
wrkflw tui
# Open with specific runtime
wrkflw tui --runtime podman
Controls:
| Key | Action |
|---|---|
Tab / 1-4 |
Switch tabs (Workflows, Execution, Logs, Help) |
Up/Down or j/k |
Navigate |
Space |
Toggle selection |
Enter |
Run / View details |
r |
Run selected workflows |
a / n |
Select all / Deselect all |
e |
Cycle runtime (Docker / Podman / Emulation) |
v |
Toggle Execution / Validation mode |
t |
Trigger remote workflow |
q / Esc |
Quit / Back |
Remote Triggering
Trigger workflow_dispatch events on GitHub or GitLab.
# GitHub (requires GITHUB_TOKEN env var)
wrkflw trigger workflow-name --branch main --input key=value
# GitLab (requires GITLAB_TOKEN env var)
wrkflw trigger-gitlab --branch main --variable key=value
Runtime Modes
| Mode | Description | Best for |
|---|---|---|
| Docker (default) | Full container isolation, closest to GitHub runners | Production, CI/CD |
| Podman | Rootless containers, no daemon required | Security-conscious environments |
| Emulation | Runs directly on host, no containers needed | Quick local testing |
Reusable Workflows
jobs:
call-local:
uses: ./.github/workflows/shared.yml
call-remote:
uses: my-org/my-repo/.github/workflows/shared.yml@v1
with:
foo: bar
secrets:
token: ${{ secrets.MY_TOKEN }}
- Local refs resolve relative to the working directory
- Remote refs are shallow-cloned at the specified
@ref with:entries becomeINPUT_<KEY>env vars;secrets:becomeSECRET_<KEY>
Limitations: outputs from called workflows are not propagated back; secrets: inherit is not supported; private repos for remote uses: are not yet supported.
Secrets Management
WRKFLW supports GitHub Actions-compatible ${{ secrets.* }} syntax with multiple providers:
# Environment variables (simplest)
export GITHUB_TOKEN="ghp_..."
wrkflw run .github/workflows/ci.yml
# File-based secrets (JSON, YAML, or .env format)
# Configure in ~/.wrkflw/secrets.yml
Supported providers: environment variables, file-based, HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Cloud Secret Manager. See the secrets demo for detailed examples.
Limitations
Supported
- Workflow syntax validation with exit codes
- Job dependency resolution and parallel execution
- Matrix builds, environment variables, GitHub context
- Container, JavaScript, composite, and local actions
- Reusable workflows (caller jobs)
- Environment files (
GITHUB_OUTPUT,GITHUB_ENV,GITHUB_PATH,GITHUB_STEP_SUMMARY) - TUI and CLI interfaces
- Container cleanup (even on Ctrl+C)
Not Supported
- GitHub encrypted secrets and fine-grained permissions
actions/cache(no persistent cache between runs)- Artifact upload/download between jobs
- Event triggers other than
workflow_dispatch - Windows and macOS runners
- Job/step timeouts, concurrency, and cancellation
- Service containers in emulation mode
- Reusable workflow output propagation (
needs.<id>.outputs.*)
Project Structure
WRKFLW is organized as a Cargo workspace with focused crates:
| Crate | Purpose |
|---|---|
wrkflw |
CLI binary and library entry point |
wrkflw-executor |
Workflow execution engine |
wrkflw-parser |
Workflow file parsing and schema validation |
wrkflw-evaluator |
Structural evaluation of workflow files |
wrkflw-validators |
Validation rules for jobs, steps, triggers |
wrkflw-runtime |
Container and emulation runtime abstractions |
wrkflw-ui |
Terminal user interface |
wrkflw-models |
Shared data structures |
wrkflw-matrix |
Matrix expansion utilities |
wrkflw-secrets |
Secrets management with multiple providers |
wrkflw-github |
GitHub API integration |
wrkflw-gitlab |
GitLab API integration |
wrkflw-logging |
In-memory logging for TUI/CLI |
wrkflw-utils |
Shared helpers |
License
MIT License - see LICENSE for details.
