* 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
Add CLAUDE.md, AGENTS.md, and INDEX.md — all generated by the indxr
MCP tooling to give AI coding assistants a structured way to explore
the codebase without dumping entire files into context.
CLAUDE.md is the detailed version with token cost estimates and a
full tool reference. AGENTS.md is the condensed version. INDEX.md
is an auto-generated codebase index with file summaries and symbol
maps.
While at it, add .indxr-cache/ to .gitignore because nobody needs
that in the repo.