Files
wrkflw/Cargo.toml
Gokul 6016887a3b feat(executor): easy GHA emulation fixes for better compatibility (#82)
* feat(executor): add easy GHA emulation fixes for better compatibility

- Expand github.* context with 13 missing env vars (CI, GITHUB_ACTIONS,
  GITHUB_REF_NAME, GITHUB_REF_TYPE, GITHUB_REPOSITORY_OWNER, etc.) and
  improve GITHUB_ACTOR to use git config / $USER instead of hardcoded value
- Enforce timeout-minutes at both job level (default 360m per GHA spec)
  and step level via tokio::time::timeout
- Implement defaults.run.shell and defaults.run.working-directory with
  proper fallback chain: step > job defaults > workflow defaults > bash
- Implement hashFiles() expression function with glob matching, sorted
  file hashing (SHA-256), and integration into the substitution pipeline

* fix(executor): harden hashFiles, working-directory, and shell -e

Three issues from code review, all in the "we got the GHA emulation
*almost* right" category:

1. hashFiles() was returning an empty string when no files matched.
   GHA returns the SHA-256 of empty input (e3b0c44...), not nothing.
   An empty string as a cache key component is the kind of thing
   that silently ruins your day. Also, unreadable files were being
   skipped without a peep — now we at least warn about it.

2. The working-directory default resolution was doing a naive
   Path::join with user-controlled input. If someone writes
   `working-directory: ../../../etc` or an absolute path, join
   happily replaces the base. Inside a container this is *somewhat*
   contained, but in emulation mode it's a real path traversal.
   Normalize the path and reject anything that escapes the
   workspace.

3. The bash -e flag change (correct per GHA spec) was undocumented.
   Scripts that relied on intermediate commands failing without
   aborting the step will now break. Document it in
   BREAKING_CHANGES.md so users aren't left guessing.

* fix(executor): complete the GHA shell invocation and harden hashFiles

The previous commit added `-e` to bash but stopped there, even
though the BREAKING_CHANGES.md *literally documented* the full GHA
invocation as `bash --noprofile --norc -e -o pipefail {0}`. So we
were advertising behavior we weren't actually implementing. This is
not great.

Without `-o pipefail`, piped commands like `false | echo ok` would
silently succeed, which is exactly the kind of divergence that makes
you distrust an emulator. And without `--noprofile --norc`, user
profile scripts can interfere with reproducibility.

While at it, fix hashFiles error handling — it was silently
swallowing read errors and producing a partial hash, which is worse
than failing because you get a *wrong* cache key with no indication
anything went sideways. preprocess_hash_files and
preprocess_expressions now return Result and the engine surfaces
failures as step errors.

Also add the tests that should have been there from the start:
shell invocation flags, working-directory path traversal rejection,
and defaults cascade (step > job > workflow).

* fix(executor): harden hashFiles, timeout, and shell edge cases

The previous round of GHA emulation fixes left a few holes that
would bite you in production:

hashFiles() would happily glob '../../etc/passwd' and hash whatever
it found outside the workspace. It also loaded entire file contents
into memory before hashing, which is *not great* when someone points
it at a large binary artifact. The glob patterns now reject '..'
traversal, and file contents are streamed into the SHA-256 hasher
via io::copy instead.

timeout-minutes accepted any f64 from YAML, including negative
values, NaN, and infinity — all of which make Duration::from_secs_f64
panic. Non-finite and non-positive values now fall back to the GHA
default of 360 minutes.

Unknown shell values were silently accepted with a '-c' fallback.
Now they emit a warning so you at least *know* something is off.

While at it, replaced the hash_files_read_error_returns_err test
that was testing two Ok paths (despite its name) with proper
path-traversal rejection tests.

* fix(executor): fix shadowed timeout_mins and extract sanitization helper

It turns out the job timeout error path was re-reading the *raw*
timeout_minutes value instead of using the already-sanitized one.
If someone set timeout-minutes to NaN or a negative number, the
sanitization would correctly fall back to 360, but the error
message would happily print "Job exceeded timeout of NaN minutes."

Not great.

Extract sanitize_timeout_minutes() so both the job and step
timeout paths use the same logic instead of duplicating the
is_finite/positive/clamp dance. While at it, add proper tests
for NaN, Infinity, negative, zero, and the max clamp — plus a
test that actually exercises the job-level timeout expiry branch,
which previously had zero coverage.
2026-04-02 18:00:41 +05:30

76 lines
2.3 KiB
TOML

[workspace]
members = ["crates/*"]
resolver = "2"
[workspace.package]
version = "0.7.3"
edition = "2021"
description = "A GitHub Actions workflow validator and executor"
documentation = "https://github.com/bahdotsh/wrkflw"
homepage = "https://github.com/bahdotsh/wrkflw"
repository = "https://github.com/bahdotsh/wrkflw"
keywords = ["workflows", "github", "local"]
categories = ["command-line-utilities"]
license = "MIT"
[workspace.dependencies]
# Internal crate dependencies
wrkflw-models = { path = "crates/models", version = "0.7.3" }
wrkflw-evaluator = { path = "crates/evaluator", version = "0.7.3" }
wrkflw-executor = { path = "crates/executor", version = "0.7.3" }
wrkflw-github = { path = "crates/github", version = "0.7.3" }
wrkflw-gitlab = { path = "crates/gitlab", version = "0.7.3" }
wrkflw-logging = { path = "crates/logging", version = "0.7.3" }
wrkflw-matrix = { path = "crates/matrix", version = "0.7.3" }
wrkflw-parser = { path = "crates/parser", version = "0.7.3" }
wrkflw-runtime = { path = "crates/runtime", version = "0.7.3" }
wrkflw-secrets = { path = "crates/secrets", version = "0.7.3" }
wrkflw-ui = { path = "crates/ui", version = "0.7.3" }
wrkflw-utils = { path = "crates/utils", version = "0.7.3" }
wrkflw-validators = { path = "crates/validators", version = "0.7.3" }
# External dependencies
clap = { version = "4.3", features = ["derive"] }
colored = "2.0"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
serde_json = "1.0"
jsonschema = "0.17"
tokio = { version = "1.28", features = ["full"] }
async-trait = "0.1"
bollard = "0.14"
futures-util = "0.3"
futures = "0.3"
chrono = "0.4"
uuid = { version = "1.3", features = ["v4"] }
tempfile = "3.6"
tar = "0.4"
dirs = "5.0"
thiserror = "1.0"
log = "0.4"
which = "4.4"
crossterm = "0.26.1"
ratatui = { version = "0.23.0", features = ["crossterm"] }
once_cell = "1.19.0"
itertools = "0.11.0"
indexmap = { version = "2.0.0", features = ["serde"] }
rayon = "1.7.0"
num_cpus = "1.16.0"
regex = "1.10"
lazy_static = "1.4"
reqwest = { version = "0.11", default-features = false, features = [
"rustls-tls",
"json",
] }
libc = "0.2"
nix = { version = "0.27.1", features = ["fs"] }
urlencoding = "2.1.3"
wiremock = "0.5"
glob = "0.3"
sha2 = "0.10"
shlex = "1.3"
[profile.release]
codegen-units = 1
lto = true