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
nameanddescription;its
inputs(values the calling step passes withwith:) andoutputs;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 ofaction.ymloraction.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
usescan be run, or a JavaScript action'smainexists, is the runner's question.runs.pre,runs.postand their conditions are kept inrunsbut not interpreted here.Input names are not validated against the characters GitHub allows, and a declared input's
deprecationMessageis ignored.The
requiredword list istrue,yesand1; YAML 1.1 spellings such asonandyare 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: worldmeans an optional input whose default isworld, not a required one.requiredis true only fortrue,yesand1(in any case). YAML 1.1 words such asonandycount as false.Defaults are always strings. A YAML default of
trueis the stringtrue, and3is3, as GitHub passes them. A missing default is the empty string, notundef.A description may be
undef, butnameand the action's owndescriptionare''when missing.kindmay be an unknown word. It iscomposite,javascriptordockerfor those kinds, and otherwise whateverruns.usingsaid, in lower case. Checkis_compositeandis_javascriptrather than comparing strings.The returned hashes are the object's own. Do not change what
inputs,outputs,runsorstepsreturn.
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.