NAME
App::Actions::Runner::Steps - run the steps of one job in its container
SYNOPSIS
my $steps = App::Actions::Runner::Steps->new(
expression => $expression,
docker => $docker,
images => $images,
containers => $containers,
workflow => $workflow,
project_dir => $project_dir,
temp_path => $temp_path,
job => $job,
container => $container,
network => $network,
facts => $containers->inspect($container),
job_env => $job_env,
context => $context,
);
my $result = $steps->run; # { result => 'success', env => {...} }
Decide what the job did:
my $result = $steps->run;
if ($result->{result} eq 'failure') {
print "job failed\n";
}
print "TOKEN_SET\n" if exists $result->{env}{TOKEN}; # exported with GITHUB_ENV
DESCRIPTION
In short: this module runs the steps of one job, one after another, in the job's container, and passes values between them as GitHub does. It is a part of App::Actions::Runner; you rarely use it on its own.
Executes a job's steps with docker exec in its long-lived container, so filesystem changes persist between steps while each step gets a fresh shell, as on GitHub. Each step gets its own GITHUB_ENV, GITHUB_OUTPUT, GITHUB_PATH, GITHUB_STEP_SUMMARY and GITHUB_STATE files, which are read back after it: exported variables and PATH additions reach the later steps, outputs reach steps.ID.outputs.
A step is a script (run), a docker:// action, a local composite or JavaScript action (uses: ./path), or a well-known action that is a no-op here. Composite actions run their steps through the same machinery, one level deeper. JavaScript actions' pre runs before main, and post runs after the job's last step, in reverse order.
METHODS
new
Creates the executor for one job, after its container has started.
ARGUMENTS
expression,docker,images,containers,workflow- the run's collaborators.project_dir,temp_path- the project and the job's runner directory on the host.job,container,network- the job instance, its container and its network.facts- what "inspect" in App::Actions::Runner::Containers found:path,has_bash,has_node.job_env,context- the job's environment and expression context.quiet,verbose,dry_run,strict_actions- optional flags;user- optional; the container user.
RETURNS
An App::Actions::Runner::Steps object.
SIDE EFFECTS
None.
EXAMPLE
See the "SYNOPSIS".
API SPECIFICATION
INPUT
{
expression => { type => 'object', can => 'interpolate' },
docker => { type => 'object', can => 'container_exec' },
images => { type => 'object', can => 'ensure' },
containers => { type => 'object', can => 'run_action' },
workflow => { type => 'object', can => 'defaults' },
project_dir => { type => 'string', min => 1 },
temp_path => { type => 'string', min => 1 },
job => { type => 'hashref' },
container => { type => 'string', min => 1 },
network => { type => 'string', min => 1 },
facts => { type => 'hashref' },
job_env => { type => 'hashref' },
context => { type => 'hashref' },
user => { type => 'string', optional => 1 },
quiet => { type => 'boolean', optional => 1 },
verbose => { type => 'boolean', optional => 1 },
dry_run => { type => 'boolean', optional => 1 },
strict_actions => { type => 'boolean', optional => 1 },
}
OUTPUT
{ type => 'object', isa => 'App::Actions::Runner::Steps' }
MESSAGES
+---------------------------+------------------------------+-----------------------------+
| Message | Meaning | Resolution |
+---------------------------+------------------------------+-----------------------------+
| Invalid arguments to | A collaborator is missing or | Pass the documented objects |
| ...Steps::new: REASON | an option has the wrong type | |
+---------------------------+------------------------------+-----------------------------+
run
my $result = $steps->run;
Runs every step of the job, then the post steps the JavaScript actions deferred, in reverse order.
RETURNS
{ result => 'success' | 'failure', env => \%job_env }.
SIDE EFFECTS
Runs commands in the container, prints the steps and their output, and writes and removes the step files.
EXAMPLE
my $result = $steps->run;
print "job failed\n" if $result->{result} eq 'failure';
API SPECIFICATION
INPUT
{}
OUTPUT
{
type => 'hashref',
schema => {
result => { type => 'string', memberof => [ 'success', 'failure' ] },
env => { type => 'hashref' },
},
}
MESSAGES
+-----------------------------+------------------------------+-----------------------------+
| Message | Meaning | Resolution |
+-----------------------------+------------------------------+-----------------------------+
| action-run: unable to | A step's env, if or | Fix the expression |
| evaluate environment / if / | continue-on-error does not | |
| continue-on-error ...: E | evaluate | |
| !! step exited with status N| A step failed | Read its output above |
| !! Step failed but | It failed and the job goes on| None needed |
| continue-on-error is set | | |
| action-run: action 'X' | The action cannot run here | Use --strict-actions to make|
| skipped: REASON | | it fail instead |
| Shell 'bash' is not | The image has no bash | Use shell: sh, or an image |
| available in the container | | with bash |
+-----------------------------+------------------------------+-----------------------------+
LIMITATIONS
Remote actions (
uses: owner/repo@ref) are not fetched, and a local action that usesdockeris not built; both are skipped, or fail understrict_actions.preandpostrun only for JavaScript actions; a composite action has neither, as on GitHub, but adockeraction'spre-entrypointandpost-entrypointwould have them and are not run.timeout-minutesis not applied to steps.A step that changes its working directory with
cddoes not change the next step's: each step starts where itsworking-directorysays.
COMMON PITFALLS
Each step starts a new shell. A
cdor anexportin one step does not reach the next. Useworking-directory:, or writeNAME=valueto$GITHUB_ENV.bashmust be in the image forshell: bash. A plain Alpine image has onlysh; the step then fails withShell 'bash' is not available.JavaScript actions need
nodein the image.A failed step stops the job's later steps, except those with
if: failure()orif: always(), and except when the step hascontinue-on-error: true.Some well-known actions do nothing here.
actions/checkout(your project is already mounted), setup-perl (the image already has Perl), cache, artifact and Codecov actions are skipped as successes.Secrets are in the step files. The script of each step is written to the runner folder; it is private to you unless the job runs as another user (see "LIMITATIONS" in App::Actions::Runner).
ENCODING
Step commands, environment values, inputs and outputs are Perl character strings; they may contain any Unicode text, including non-ASCII characters and emoji. Step scripts and the GITHUB_* files are written as UTF-8, with LF line endings, and the files a step writes back (GITHUB_ENV, GITHUB_OUTPUT, ...) are read as UTF-8. What a step prints goes to your terminal as bytes, unchanged.
AUTHOR
Nigel Horne, njh@nigelhorne.com
FORMAL SPECIFICATION
This section describes each routine in the Z notation, a precise mathematical language for describing programs. You do not need to read it to use the module: everything here is also said in plain words in the sections above. It is for readers who want to check exactly what a routine promises.
How to read it: a box names a routine. The lines above the middle rule declare the values it works with. A name ending in ? is an input, and a name ending in ! is an output. ΞState means the routine changes nothing, and ΔState means it can change the state. The lines below the rule are the conditions that are always true when the routine returns.
new
┌─ Steps ───────────────────────────────────────────
│ ctx : CONTEXT
│ jobEnv : NAME ⇸ seq CHAR
│ pathAdds : seq PATH
│ counter : ℕ
│ posts : seq POST
│ depth : ℕ
│ failed : 𝔹
├───────────────────────────────────────────────────
│ depth ≤ 10 ∧ ctx.env = jobEnv
└───────────────────────────────────────────────────
run
┌─ Run ─────────────────────────────────────────────
│ ΔSteps
│ result! : {success, failure}
├───────────────────────────────────────────────────
│ result! = (if failed' then failure else success)
│ failed' ⇔ (∃ s : job.steps • outcome(s) = failure ∧ ¬ continueOnError(s))
│ ∨ (∃ p : posts • failed(p))
└───────────────────────────────────────────────────
STATE DIAGRAM
One call to "run" takes each step through these states:
run()
|
v
+---------------------+
| NEXT STEP |<-------------------------------------------+
+---------------------+ |
| none left -> RUN POSTS |
v |
+---------------------+ if: is false (by default: the job has |
| EVALUATE if: | not failed) -> SKIPPED ------------------>+
+---------------------+ |
| true |
v |
+---------------------+ fresh GITHUB_ENV, GITHUB_OUTPUT, |
| PREPARE STEP FILES | GITHUB_PATH, GITHUB_STATE, |
| (O_EXCL, mode 0600) | GITHUB_STEP_SUMMARY files |
+---------------------+ |
| |
v |
what kind of step? |
+-- run: ------------> script written; docker exec SHELL script |
+-- docker://IMAGE --> Containers->run_action (own container) |
+-- ./local composite-> its steps, through this same machine |
+-- ./local javascript-> pre now; main now; post queued |
+-- known no-op -----> success without running anything |
+-- other remote ----> skipped (strict_actions: failure) |
| |
v |
+---------------------+ exported env -> later steps; |
| READ BACK FILES | PATH additions; outputs -> |
+---------------------+ steps.ID.outputs; summary kept |
| |
v |
status 0? --yes--> SUCCESS ------------------------------------->+
| no |
v |
continue-on-error? --yes--> warned; job still succeeding ------->+
| no |
v |
JOB FAILING: later steps run only if their if: says so --------->+
+---------------------+ queued JavaScript post steps,
| RUN POSTS | last queued first
+---------------------+
|
v
return { result => 'success' | 'failure', env => job env }
LICENSE AND COPYRIGHT
Copyright 2026 Nigel Horne.
This program is released under the GNU General Public License, version 2. See the LICENSE file that comes with this distribution. If you use it, please let me know.