NAME

App::Actions::Action - read the metadata of a local GitHub action

SYNOPSIS

Read an action from its directory, and find out what kind it is:

use App::Actions::Action;

my $action = App::Actions::Action->load_from_dir('.github/actions/setup');
print $action->kind, "\n";    # composite, javascript, docker, ...

List its inputs, and which are required:

for my $name (sort keys %{ $action->inputs }) {
	my $input = $action->inputs->{$name};
	printf "%-10s %s default '%s'\n", $name,
		$input->{required} ? 'required' : 'optional', $input->{default};
}

Read the steps of a composite action:

if ($action->is_composite) {
	print $_->{run} // $_->{uses}, "\n" for @{ $action->steps };
}

Read a metadata file by its name:

my $action = App::Actions::Action->load('.github/actions/setup/action.yml');

DESCRIPTION

A GitHub action is a reusable piece of a workflow. A local action is one kept in the same repository, used with uses: ./path/to/action. Its directory holds a metadata file, action.yml (or action.yaml), which says:

  • its name and description;

  • its inputs (values the calling step passes with with:) and outputs;

  • how it runs (runs:): as a composite action (a list of steps, like a job), as a JavaScript action (a Node.js program), or as a Docker action.

This module reads that file, checks its shape, and gives you the parts. The steps of a composite action are checked by the same code as a workflow's steps ("normalize_step" in App::Actions::Util), so the two follow the same rules.

It only reads the file. Running the action is the job of App::Actions::Runner::Steps.

METHODS

load_from_dir

Finds and parses the metadata file in an action's directory.

ARGUMENTS

  • $dir - the action's directory.

RETURNS

An App::Actions::Action object.

SIDE EFFECTS

Reads the directory and the metadata file.

EXAMPLE

my $action = App::Actions::Action->load_from_dir('.github/actions/setup');
print $action->name, ': ', $action->description, "\n";

API SPECIFICATION

INPUT

{ dir => { type => 'string', min => 1 } }

OUTPUT

{ type => 'object', isa => 'App::Actions::Action' }

MESSAGES

+-----------------------------------+------------------------------+-------------------------------+
| Message                           | Meaning                      | Resolution                    |
+-----------------------------------+------------------------------+-------------------------------+
| No action directory given         | The path was undef or empty  | Pass the directory            |
| Action directory 'D' does not     | No directory at the path     | Check the uses: path          |
| exist                             |                              |                               |
| No action.yml or action.yaml      | The directory has no         | Add the metadata file         |
| found in 'D'                      | metadata                     |                               |
+-----------------------------------+------------------------------+-------------------------------+

The messages of "load" apply to the file that is found.

load

Parses a single metadata file.

ARGUMENTS

  • $file - the path of action.yml or action.yaml.

RETURNS

An App::Actions::Action object.

SIDE EFFECTS

Reads the file.

EXAMPLE

my $action = App::Actions::Action->load('greet/action.yml');
if ($action->is_composite) {
	print scalar @{ $action->steps }, " steps\n";
}

API SPECIFICATION

INPUT

{ file => { type => 'string', min => 1 } }

OUTPUT

{ type => 'object', isa => 'App::Actions::Action' }

MESSAGES

+-----------------------------------+------------------------------+-------------------------------+
| Message                           | Meaning                      | Resolution                    |
+-----------------------------------+------------------------------+-------------------------------+
| No action metadata file given     | The path was undef or empty  | Pass a path                   |
| Action metadata file 'F' does not | No file at the path          | Check the path                |
| exist                             |                              |                               |
| Unable to parse action metadata   | The YAML is malformed        | Fix the YAML                  |
| 'F': E                            |                              |                               |
| Action metadata 'F' is empty      | No document in the file      | Add the metadata              |
| Action metadata 'F' must contain  | The top level is not a       | Make it a mapping             |
| a YAML mapping                    | mapping                      |                               |
| Action metadata 'F' has no 'runs' | runs is missing              | Add a runs: mapping           |
| mapping                           |                              |                               |
| Action metadata 'F' has no        | runs.using is missing        | Add using: composite (or      |
| 'runs.using'                      |                              | node20, docker)               |
| Composite action 'F' has no       | A composite with no steps    | Add runs.steps                |
| 'runs.steps' list                 |                              |                               |
| JavaScript action 'F' has no      | A node action with no entry  | Add runs.main                 |
| 'runs.main'                       | point                        |                               |
| The 'inputs' of 'F' must be a     | inputs is a list or a scalar | Name each input               |
| mapping                           |                              |                               |
| Input 'N' of 'F' must be a        | An input is a list           | Give it description, default  |
| mapping                           |                              | and required keys             |
| The 'outputs' of 'F' must be a    | outputs is a list or a       | Name each output              |
| mapping                           | scalar                       |                               |
| Output 'N' of 'F' must be a       | An output is a list          | Give it a value key           |
| mapping                           |                              |                               |
+-----------------------------------+------------------------------+-------------------------------+

The step messages of "normalize_step" in App::Actions::Util apply to every composite step.

PSEUDOCODE

read the file as a YAML mapping
refuse metadata without runs, or without runs.using
decide the kind from runs.using:
	composite needs runs.steps, which are normalised like workflow steps
	nodeN needs runs.main
	docker and anything else are recorded as given
read the declared inputs and outputs
return the action with its directory, names, kind and declarations

file, dir, name, description, using, kind, inputs, outputs, runs, steps

my $kind   = $action->kind;      # composite, javascript, docker, or runs.using
my $inputs = $action->inputs;    # { NAME => { description, required, default } }
my $steps  = $action->steps;     # [] unless the action is composite

Read-only accessors. file is the metadata file and dir the directory it is in. name and description are '' when the metadata has none. using is runs.using in lower case. inputs maps each input to description (or undef), required (0 or 1) and default (a string); outputs maps each output to description and value.

API SPECIFICATION

INPUT

{}

OUTPUT

file, dir, name, description, using, kind: { type => 'string' }
inputs, outputs, runs:                     { type => 'hashref' }
steps:                                     { type => 'arrayref' }

MESSAGES

None.

EXAMPLE

for my $name (sort keys %{ $action->inputs }) {
	my $input = $action->inputs->{$name};
	printf "%s%s\n", $name, $input->{required} ? ' (required)' : '';
}

is_composite, is_javascript

print "composite\n" if $action->is_composite;

Return 1 when the action is of that kind, else 0.

API SPECIFICATION

INPUT

{}

OUTPUT

{ type => 'integer', memberof => [ 0, 1 ] }

MESSAGES

None.

EXAMPLE

my $runner = $action->is_javascript ? 'node' : 'sh';

LIMITATIONS

  • Only the metadata is read. Whether a composite step's uses can be run, or a JavaScript action's main exists, is the runner's question.

  • runs.pre, runs.post and their conditions are kept in runs but not interpreted here.

  • Input names are not validated against the characters GitHub allows, and a declared input's deprecationMessage is ignored.

  • The required word list is true, yes and 1; YAML 1.1 spellings such as on and y are read as false.

COMMON PITFALLS

  • Give the directory to "load_from_dir", the file to "load".

  • An input written as a plain value is optional. who: world means an optional input whose default is world, not a required one.

  • required is true only for true, yes and 1 (in any case). YAML 1.1 words such as on and y count as false.

  • Defaults are always strings. A YAML default of true is the string true, and 3 is 3, as GitHub passes them. A missing default is the empty string, not undef.

  • A description may be undef, but name and the action's own description are '' when missing.

  • kind may be an unknown word. It is composite, javascript or docker for those kinds, and otherwise whatever runs.using said, in lower case. Check is_composite and is_javascript rather than comparing strings.

  • The returned hashes are the object's own. Do not change what inputs, outputs, runs or steps return.

ENCODING

The metadata file is read as UTF-8 (see "ENCODING" in App::Actions::Util). Names, descriptions, defaults and step commands may contain any Unicode text, including non-ASCII characters and emoji, and are returned as Perl character strings. Input names should be ASCII, because they become environment variable names (INPUT_NAME) when the action runs. The path you pass is used as bytes.

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.

load_from_dir

┌─ LoadFromDir ─────────────────────────────────────
│ ΞFileSystem
│ dir?    : optional PATH
│ action! : Action
├───────────────────────────────────────────────────
│ dir? ∈ {⊥, ⟨⟩} ⇒ raises DirRequired
│ dir? ¬in; directories ⇒ raises DirMissing(dir?)
│ let found == ⟨ n : ⟨action.yml, action.yaml⟩ | dir?/n ∈ regularFiles ⟩
│ found = ⟨⟩ ⇒ raises NoMetadata(dir?)
│ found ≠ ⟨⟩ ⇒ action! = Load(dir?/head found)
└───────────────────────────────────────────────────

load

KIND ::= composite | javascript | docker | other⟨⟨STRING⟩⟩

┌─ Load ────────────────────────────────────────────
│ file?   : PATH
│ action! : Action
├───────────────────────────────────────────────────
│ let doc == LoadYamlMapping(file?, action)
│ doc.runs ¬in; MAPPING ⇒ raises NoRuns(file?)
│ doc.runs.using ∈ {⊥, ⟨⟩} ⇒ raises NoUsing(file?)
│ let u == lower(doc.runs.using)
│ u = composite ⇒ action!.kind = composite ∧ doc.runs.steps ∈ seq VALUE
│ u ∈ node⁀DIGITS ⇒ action!.kind = javascript ∧ doc.runs.main ∈ STRING \ {⟨⟩}
│ u = docker ⇒ action!.kind = docker
│ otherwise action!.kind = u
│ action!.dir = dirname(file?)
│ action!.kind ≠ composite ⇒ action!.steps = ⟨⟩
└───────────────────────────────────────────────────

file, dir, name, description, using, kind, inputs, outputs, runs, steps

┌─ Accessor ────────────────────────────────────────
│ ΞAction
│ field? : {file, dir, name, description, using, kind, inputs, outputs, runs, steps}
│ value! : VALUE
├───────────────────────────────────────────────────
│ value! = θAction.field?
└───────────────────────────────────────────────────

is_composite, is_javascript

┌─ IsKind ──────────────────────────────────────────
│ ΞAction
│ k?   : {composite, javascript}
│ yes! : {0, 1}
├───────────────────────────────────────────────────
│ yes! = 1 ⇔ kind = k?
└───────────────────────────────────────────────────

STATE DIAGRAM

An action object never changes after it is made. Loading goes through these steps; each check that fails throws an App::Actions::Error with the code shown:

 load_from_dir($dir)
        |
        v
+------------------+  no $dir -> action.dir_required
| FIND METADATA    |  not a directory -> action.dir_missing
| action.yml, then |  neither file -> action.no_metadata
| action.yaml      |
+------------------+
        |
        v   load($file)
+------------------+  missing / bad YAML / empty / not a mapping
| READ YAML        |  -> action.file_* , action.parse_failed, ...
+------------------+
        |
        v
+------------------+  no runs, or runs.using missing
| READ runs.using  |  -> action.no_runs / action.no_using
+------------------+
        |
        +-- composite ----> steps must be a list; each step checked
        |                   (normalize_step)       -> action.no_steps
        +-- nodeN -------> runs.main must be set  -> action.no_main
        |   (javascript)
        +-- docker -------> kept as it is
        +-- anything else -> kind = that word
        |
        v
+------------------+  inputs/outputs not a mapping, or an entry
| READ inputs and  |  that is neither text nor a mapping
| outputs          |  -> action.inputs_invalid, action.input_invalid, ...
+------------------+
        |
        v
+------------------+
|   LOADED         |  read-only: file, dir, name, description, using,
|   (immutable)    |  kind, inputs, outputs, runs, steps,
+------------------+  is_composite, is_javascript

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.