April 13, 2026
2 pull requests merged across 1 repo
bahdotsh/wrkflw
- Composite actions that declare
outputs:withvalue:expressions (e.g.${{ steps.build-msg.outputs.msg }}) now correctly propagate those values back to the calling workflow ${{ steps.<composite-id>.outputs.<key> }}previously resolved to empty string — now resolves to the evaluated value- Output propagation also runs on the early-return failure path so partial outputs are available when a composite step fails
Root cause
execute_composite_action() was running all internal composite steps and tracking their outputs in composite_step_outputs, but never reading the action's outputs: YAML section to evaluate and return those values. The last wire was simply never connected.
Approach
Added propagate_composite_outputs() which:
1. Reads the action's outputs: mapping from the parsed YAML
2. Evaluates each value: expression against the composite's internal step context
3. Writes results to the caller's GITHUB_OUTPUT file
The existing apply_step_environment_updates pipeline picks them up naturally — zero changes to StepResult or process_outcome.
Test plan
- New unit test:
propagate_composite_outputs_writes_to_github_output_file— verifies expression evaluation and file write - New unit test:
propagate_composite_outputs_no_outputs_section_is_noop— verifies no-op when action has no outputs - All 335 executor tests pass (333 existing + 2 new)
- Manual verification:
${{ steps.full-greet.outputs.message }}resolves to"Hi, World!"(was empty before)
- Root cause:
EmulationRuntime::run_containerignored thevolumesparameter and silently rerouted the working directory toGITHUB_WORKSPACE(= the real project directory) when the container path didn't exist on the host.handle_upload_artifactstill usedctx.working_dir(the per-job tempdir). Two workspaces diverged, so upload-artifact reported"No files found matching pattern"for any file a priorrun:step had written. Same class of bug lived inSecureEmulationRuntime, which additionally copied files into a private sandbox workspace that the caller never saw. - Fix: Added
container::resolve_host_working_dir— a pure function that rebases a container-visible path onto its host-side volume source via the longest component-boundary prefix match (usesPath::strip_prefix, so/github/workspace-foois not matched by a/github/workspacemount). Both emulation runtimes now honorvolumes, matching the bind-mount semantics docker/podman already provide. The sandbox runs commands in-place in the caller's workspace; the copy-files-to-a-private-directory layer was not a meaningful security boundary (validation comes from the command whitelist, dangerous-pattern regexes, and env-var filtering, all preserved) and it was the mechanism that broke the run-step/artifact-handler workspace invariant for secure emulation. - No back-compat shims: uncovered container paths now fail loudly with
"not covered by any volume mount"instead of silently running in the wrong place. No GITHUB_WORKSPACE fallback, no implicit project copy.
Reproduces for any workflow with actions/upload-artifact + actions/download-artifact under --runtime emulation without an explicit actions/checkout.
Closes #88.
Test plan
- New end-to-end regression test (
run_step_upload_download_artifact_roundtrip_emulation) drives a realEmulationRuntimethrough run → upload-artifact → download-artifact and asserts the payload round-trips byte-for-byte. Verified it fails onmainbefore the fix with the exact"No files found matching pattern"symptom. - 6 unit tests for
resolve_host_working_dir: exact match, sub-path, longest-prefix-wins, no-match, empty volumes, component-boundary (the/github/workspace-fooedge case). - 2 new tests for
SecureEmulationRuntime: volume rebase produces the correct host cwd, and a missing volume mapping hard-errors with a clear message. - Existing tests unchanged and still passing: 14/14 runtime, 333/333 executor. Deleted the orphaned
sandbox::test_file_filteringtest that only covered the removed copy path. - Manual smoke against a real workflow with
upload-artifact/download-artifactunder--runtime emulationon the reviewer's machine (the automated regression test covers the same code path, but a hands-on run is a good final sanity check).
Notes
- Surfaces a pre-existing, orthogonal wrkflw quirk:
ArtifactStore::uploadfeeds user patterns straight to theglobcrate, which doesn't match files under a trailing(you need/*). The regression test uses an exact filename to dodge this; worth filing a follow-up forupload-artifactglob compatibility. secure_emulation's allowed_read_paths / allowed_write_paths config fields are now unused in this PR but kept as public API for potential future use.