Skip to content

feat: run composite actions from a uses reference on the host - #81

Merged
Mearman merged 2 commits into
mainfrom
feat/composite-actions
Oct 11, 2026
Merged

Mearman merged 2 commits into
mainfrom
feat/composite-actions

Conversation

@Mearman

@Mearman Mearman commented Oct 11, 2026

Copy link
Copy Markdown
Member

Adds composite-action resolution and interpretation to @pipewright/pipeline-github-actions: the library half of running a uses: step without a container (#71). The runner and CLI wiring is #76; this changes no consumer yet.

src/action.ts reads what a reference and a composite action's metadata are. parseActionReference takes a uses value into its forms: a local ./path, whose .. segments are kept because only the base it appears in (the workspace for a workflow step, the action's own directory for a composite step) can apply them, and owner/repo, owner/repo@ref and the subpath form, split at the first @ because a ref may contain one while a locator cannot. A docker:// image and the gated same-repository $/ alias are rejected naming what they are. compositeActionSchema validates the metadata file GitHub documents: the declared inputs (GitHub gives an action's inputs no type, unlike a reusable workflow's; every value an action receives is a string), the run and uses steps with the keys each form allows (shell required beside run, which is GitHub's own rule), and branding and runs.outputs carried as data. A step is validated through the branch its present key picks, so one wrong key is named where it is rather than reported against both branches of a union.

Resolution goes through an injected port, never a request of the library's own: ActionResolver maps a reference to an ActionTree, which exposes the directory the host materialised the action at, a reader for its files relative to its own directory (action.yml, then the documented action.yaml alternative), and local for resolving a nested ./ reference against that directory. A CLI host resolves local references against the workspace and remote ones at their ref, serving the repository being run from the checkout, and tests serve in-memory trees. A fetched tree is data: it is read, and nothing in it executes because it was fetched.

expandCompositeAction takes the provider step the importer already builds for a uses: reference, the resolver port and an evaluation context, and returns the expanded neutral steps. Each declared input is mapped from the supplied with or its default: names match without regard to case, an optional input that arrived unset reads as the empty string (both are the runner's own behaviour), and defaults evaluate against the calling context, which is the view the runner evaluates them with, before the child's inputs context exists. The action's own steps then evaluate against a context whose inputs holds the mapped values and whose github.action_path is the host's directory, with each step's own environment merged for its expressions, and each expanded run step carries its evaluated environment plus GITHUB_ACTION_PATH. Two things GitHub tolerates are errors instead, because each means the workflow and the action disagree: a supplied input the action does not declare (GitHub warns and drops it) and a required input with neither a value nor a default (GitHub passes the empty string).

Nested uses recurse. A nested action that is not composite, and a docker:// reference, do not: they stay provider steps carrying the uses and with text as written, which is how the importer carries a workflow's own uses steps, so node actions remain #75's business and docker remains rejected at run time as today. Nesting is bounded at ten actions, derived from the runner's own limit: CompositeActionsMaxDepth = 9 in actions/runner's src/Runner.Common/Constants.cs, enforced as a throw once the depth counted from zero at the workflow's steps passes nine, which allows ten; the expansion counts the action the step names as the first of those ten and rejects an eleventh. A cycle is recognised by canonical identity (repository at ref plus normalised directory, so ./.. climbing out of a nested action back to an ancestor is caught, not just a repeated reference text) and rejected naming the whole chain.

Anything the model has no form for is rejected naming what and where, with the metadata file and a JSON pointer into it: malformed metadata, a step with both or neither of run and uses, a key no composite step has, a shell outside bash and sh (the neutral run step carries no shell and a host of this tool runs commands through one POSIX shell of its own, so another interpreter's text would be mis-run rather than run), and continue-on-error, which would change how a failing step concludes.

test/composite.unit.test.ts covers each reference form and its rejections, a local composite action with defaults and expressions, a same-repository reference served from the checkout, a remote action with one level of nesting, nested non-composite and docker steps staying provider steps, the cycle naming the chain, the bound at ten and eleven, and the metadata rejections by file and pointer. The runner, the CLI and the contract package are untouched.

Fixes #74

…etadata

parseActionReference reads a uses value into its forms: a local ./path,
whose .. segments are kept for the base it appears in to apply, and an
owner/repo, owner/repo@ref or subpath reference. Anything else, a
docker:// image and the gated $/ alias among them, is rejected naming
what the value is. compositeActionSchema validates the metadata of an
action whose runs.using is composite: the declared inputs, the run and
uses steps with the keys GitHub documents for each, and branding and
runs.outputs carried as data. Keys GitHub does not document for a
composite action are rejected by the strict objects.

Sources: GitHub's metadata syntax documentation and the actions/runner
source for what its parser accepts.
expandCompositeAction takes the provider step the importer builds for a
uses: reference, resolves the action through an injected port, and
returns the run steps and nested provider steps the model runs. Every
declared input is mapped from the supplied with or its default, with
input names matched without regard to case, and the template
expressions of with values, defaults and the action's steps are
evaluated through the expression evaluator against a context whose
inputs holds the mapped values and whose github.action_path is the
directory the host resolved the action to. Each expanded run step
carries its own evaluated environment plus GITHUB_ACTION_PATH, and a
nested action that is not composite stays a provider step carrying the
text as written.

Nesting recurses to the ten actions GitHub's own runner allows, and a
cycle is rejected naming the whole reference chain. An undeclared input,
a required input with no value, a shell or continue-on-error the model
has no step form for and metadata that does not match the schema are all
rejected naming what and where, the metadata by file and JSON pointer.

Refs #74; wiring the expansion into the runner is #76.
@Mearman

Mearman commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-10-11T18:32:43.679672Z bc47b6b Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Mearman
Mearman marked this pull request as ready for review October 11, 2026 18:26
@Mearman
Mearman merged commit feda4c4 into main Oct 11, 2026
12 checks passed
@Mearman
Mearman deleted the feat/composite-actions branch October 11, 2026 18:33
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.4.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Run composite actions from a uses reference on the host

1 participant