NAME

App::Actions::Runner::Support::Env - the env context that resolves variables when they are read

SYNOPSIS

Resolve a set of variables that refer to each other:

use App::Actions::Expression;
use App::Actions::Runner::Support::Env;

my $expression = App::Actions::Expression->new;
my %raw = (A => 'a', B => '${{ env.A }}b');
my %resolved;
my %context;

my $state = tie my %env, 'App::Actions::Runner::Support::Env', \%raw, \%resolved, sub {
	return $expression->interpolate($raw{ $_[0] }, \%context);
};
$context{env} = \%env;

print $env{B}, "\n";    # ab

Find out what went wrong:

my $ok = eval { my $value = $env{B}; 1 };
print $state->failure->message, "\n" unless $ok;

Clean up (the hash and the resolver refer to each other):

undef $state;
untie %env;

Most code does not use this class directly: it calls "resolve_env" in App::Actions::Runner::Support, which does all of the above.

DESCRIPTION

A workflow's env values may use other variables: B: ${{ env.A }}b. To work out B, the evaluator must first know A.

This class is a tied hash for the env part of an expression context. When the evaluator reads env.A, the hash works out A at that moment, by calling a function you give it, and keeps the result. So the variables can be read in any order.

Each variable's text is worked out exactly once. Its result is never worked out again, so ${{ }} text that came from somewhere else, such as a commit message, stays as text.

If A is read again while A is still being worked out, the variables form a loop (A uses B, B uses A). That is the error runner.env_cycle.

The hash can only be read. It holds the names of the raw variables, so exists, keys and a case-insensitive search all work.

METHODS

TIEHASH

my $state = tie my %env, 'App::Actions::Runner::Support::Env', \%raw, \%resolved, $resolve;

Called by tie.

ARGUMENTS

  • \%raw - the variables' text, name to string.

  • \%resolved - an empty hash; each result is stored here.

  • $resolve - a code reference. It is called with a name and must return that variable's value. It usually interpolates $raw{$name} with a context whose env is this tied hash.

RETURNS

The tie object. Keep it if you want to call "failure".

SIDE EFFECTS

None.

EXAMPLE

my $state = tie my %env, 'App::Actions::Runner::Support::Env', { A => 'x' }, {}, sub { 'x' };

API SPECIFICATION

INPUT

{
	raw      => { type => 'hashref' },
	resolved => { type => 'hashref' },
	resolve  => { type => 'coderef' },
}

OUTPUT

{ type => 'object', isa => 'App::Actions::Runner::Support::Env' }

MESSAGES

None.

FETCH

my $value = $env{NAME};

Returns the value of a variable, working it out the first time.

ARGUMENTS

  • $key - the variable's name, exactly as written (the evaluator does the case-insensitive search by walking the keys).

RETURNS

The value, or undef for a name that is not one of the variables.

SIDE EFFECTS

Stores the value in the \%resolved hash. On a failure, remembers it (see "failure").

EXAMPLE

my $value = $env{B};

API SPECIFICATION

INPUT

{ key => { type => 'string' } }

OUTPUT

{ type => 'string', optional => 1 }

MESSAGES

+-----------------------------+------------------------------+-----------------------------+
| Message                     | Meaning                      | Resolution                  |
+-----------------------------+------------------------------+-----------------------------+
| Environment variable 'X'    | X refers to itself, or to a  | Break the loop: give one of |
| refers to itself, directly  | variable that refers back    | the variables a value that  |
| or through other variables  | to it                        | does not use the others     |
+-----------------------------+------------------------------+-----------------------------+
| Unable to evaluate          | The resolver failed for X    | Fix the expression in the   |
| environment variable 'X': E |                              | workflow's env              |
+-----------------------------+------------------------------+-----------------------------+

EXISTS, FIRSTKEY, NEXTKEY and SCALAR

exists $env{NAME};
my @names = keys %env;
if (%env) { ... }

These see the raw variables, so a name is there before it is worked out. Walking the keys does not work any variable out.

API SPECIFICATION

INPUT

{ key => { type => 'string', optional => 1 } }

OUTPUT

{ type => 'string', optional => 1 }

MESSAGES

None.

failure

my $error = $state->failure;

Returns the first error any read raised, or undef. The evaluator turns an error inside a lookup into plain text; this keeps the original App::Actions::Error object with its code.

API SPECIFICATION

INPUT

{}

OUTPUT

{ type => 'object', isa => 'App::Actions::Error', optional => 1 }

MESSAGES

None.

fail

$state->fail('runner.env_cycle', { name => 'A' });

Raises a catalogue error and remembers it as the first failure. It never returns.

ARGUMENTS

  • $message - the message key.

  • \%args - its placeholders.

API SPECIFICATION

INPUT

{
	message => { type => 'string' },
	args    => { type => 'hashref' },
}

OUTPUT

{ type => 'undef' }

MESSAGES

The message it is given.

COMMON PITFALLS

  • Untie the hash when you are done. The context holds the hash, the hash holds the resolver, and the resolver usually holds the context. That loop is only broken by untie. Drop the object tie returned first, or untie warns that a reference still exists.

  • The hash is read-only. Storing or deleting a key dies, because the class has no STORE or DELETE.

  • Once a read fails, later failures report the first one. This is what makes a chain report its cause, not its last link. Use a new tie for a new set of variables.

ENCODING

Names and values are Perl character strings and may hold any text, including non-ASCII characters and emoji. Nothing is encoded or decoded here.

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.

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. ΔEnv means the routine may change the state; ΞEnv means it does not. The lines below the rule are always true when the routine returns.

Env ≙ [ raw : NAME ⇸ TEXT; resolved : NAME ⇸ TEXT;
        busy : ℙ NAME; failure : optional ERROR;
        resolve : NAME → TEXT ]

TIEHASH

┌─ TieHash ─────────────────────────────────────────
│ Env'
│ raw? : NAME ⇸ TEXT; resolve? : NAME → TEXT
├───────────────────────────────────────────────────
│ raw' = raw? ∧ resolve' = resolve?
│ resolved' = ∅ ∧ busy' = ∅ ∧ failure' = ⊥
└───────────────────────────────────────────────────

FETCH

┌─ Fetch ───────────────────────────────────────────
│ ΔEnv
│ key? : NAME
│ v!   : optional TEXT
├───────────────────────────────────────────────────
│ key? ∈ dom resolved ⇒ v! = resolved(key?) ∧ θEnv' = θEnv
│ key? ¬in; dom raw ⇒ v! = ⊥ ∧ θEnv' = θEnv
│ key? ∈ busy ⇒ raises Cycle(key?) ∧ failure' = failure ∨ Cycle(key?)
│ otherwise, with busy ∪ {key?} while resolve(key?) runs:
│     resolve(key?) = t ⇒ v! = t ∧ resolved' = resolved ⊕ {key? ↦ t}
│     resolve(key?) fails ∧ failure ≠ ⊥ ⇒ raises failure
│     resolve(key?) fails ∧ failure = ⊥ ⇒ raises Failed(key?)
│         ∧ failure' = Failed(key?)
│ busy' = busy
└───────────────────────────────────────────────────

EXISTS, FIRSTKEY, NEXTKEY and SCALAR

┌─ Keys ────────────────────────────────────────────
│ ΞEnv
├───────────────────────────────────────────────────
│ exists(k) ⇔ k ∈ dom raw
│ keys = dom raw
└───────────────────────────────────────────────────

failure

┌─ Failure ─────────────────────────────────────────
│ ΞEnv
│ r! : optional ERROR
├───────────────────────────────────────────────────
│ r! = failure
└───────────────────────────────────────────────────

fail

┌─ Fail ────────────────────────────────────────────
│ ΔEnv
│ message? : KEY; args? : NAME ⇸ TEXT
├───────────────────────────────────────────────────
│ failure' = Error(message?, args?)
│ raises failure'
└───────────────────────────────────────────────────

STATE DIAGRAM

Each variable is in one of three states. A read of the variable moves it:

                      $env{A}
+----------------+ (first read)  +-----------------+
|  RAW           |-------------->|  BUSY           |
|  text only     |               |  resolve('A')   |
+----------------+               |  is running     |
                                 +-----------------+
                                   |      |      |
              resolve returned t   |      |      |  $env{A} read again
                                   v      |      v  (a cycle)
                     +-----------------+  |  +------------------------+
                     |  RESOLVED       |  |  | fail: runner.env_cycle |
                     |  resolved{A}=t  |  |  | failure := the error   |
                     +-----------------+  |  +------------------------+
                       |                  |
      later reads      |                  | resolve died
      return t at once |                  v
                       v       +---------------------------------+
                    $env{A}    | failure set? raise it again     |
                               | else fail: runner.env_failed    |
                               +---------------------------------+

After a failure, A goes back to RAW (BUSY is undone on the way out),
and failure keeps the first error until the hash is untied.

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.