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 uses docker is not built; both are skipped, or fail under strict_actions.

  • pre and post run only for JavaScript actions; a composite action has neither, as on GitHub, but a docker action's pre-entrypoint and post-entrypoint would have them and are not run.

  • timeout-minutes is not applied to steps.

  • A step that changes its working directory with cd does not change the next step's: each step starts where its working-directory says.

COMMON PITFALLS

  • Each step starts a new shell. A cd or an export in one step does not reach the next. Use working-directory:, or write NAME=value to $GITHUB_ENV.

  • bash must be in the image for shell: bash. A plain Alpine image has only sh; the step then fails with Shell 'bash' is not available.

  • JavaScript actions need node in the image.

  • A failed step stops the job's later steps, except those with if: failure() or if: always(), and except when the step has continue-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.