/* * This file is part of the xPack project (http://xpack.github.io). * Copyright (c) 2021-2026 Liviu Ionescu. All rights reserved. * * Permission to use, copy, modify, and/or distribute this software * for any purpose is hereby granted, under the terms of the MIT license. * * If a copy of the license was not distributed with this file, it can * be obtained from https://opensource.org/license/mit. */ // ---------------------------------------------------------------------------- import assert from 'node:assert' import * as os from 'node:os' import { Logger } from '@xpack/logger' // ---------------------------------------------------------------------------- import { LiquidSubstitutionsVariables, LiquidSubstitutionsStrings, } from '../data/substitutions-variables.js' import { isJsonObject, isString, isJsonArray, } from '../functions/is-something.js' import { performSubstitutions } from '../functions/perform-substitutions.js' import { getErrorMessage, hasLiquidSyntax } from '../functions/utils.js' import { JsonActionContent, JsonActions, JsonActionTemplate, } from '../types/json.js' import { BuildConfiguration } from './build-configurations.js' import { ConfigurationError } from './errors.js' import { LiquidEngine } from './liquid-engine.js' import { TemplateExpander } from './template-expander.js' // ============================================================================ /** * Configuration parameters for constructing an actions collection instance. * * @remarks * This interface defines the required configuration for creating an * instance of {@link Actions}. Most properties are mandatory except for * the optional inheritedActionsMap and * buildConfiguration parameters. * * The parameters provide the actions collection with access to the Liquid * templating engine, substitution variables hierarchy, action definitions * from the package manifest, optional inherited actions from a parent * package, optional build configuration context, and the logger for * diagnostic output. */ export interface ActionsConstructorParameters { /** * The Liquid templating engine for variable substitution. */ engine: LiquidEngine /** * The variables available for substitution in action definitions. */ substitutionsVariables: LiquidSubstitutionsVariables /** * The JSON object containing action definitions, or undefined if there are * no actions. */ jsonActions: JsonActions | undefined /** * Optional map of actions inherited from a parent package. */ inheritedActionsMap?: Map /** * Optional build configuration this actions collection belongs to. */ buildConfiguration?: BuildConfiguration /** * The logger instance for output and diagnostics. */ log: Logger } /** * A collection of xpm actions for a build configuration or * the entire package. * * @remarks * This class manages a collection of named actions, each containing one or * more commands to be executed. Actions can belong to a package or a build * configuration and support template-based definitions with matrix expansion * to generate multiple actions from a single template. * * The collection always exists, even as empty if no actions are defined. * * Action lifecycle phases: * *
    *
  1. Construction: Basic setup with optional inheritance from parent * package.
  2. *
  3. Initialisation: Template name expansion without content * evaluation.
  4. *
  5. Retrieval: On-demand instantiation when accessed via * get().
  6. *
  7. Action Initialisation: Liquid template evaluation and * substitution.
  8. *
* * This multi-phase approach ensures efficient resource usage by deferring * expensive operations until actions are actually needed. */ export class Actions { // -------------------------------------------------------------------------- // Public Members. /** * The logger instance for output and diagnostics. * * @remarks * This logger is used throughout the lifecycle of actions collection to * provide trace-level diagnostics for debugging template expansion, action * instantiation, and variable substitution. It enables visibility into the * lazy evaluation process without impacting runtime performance when tracing * is disabled. */ readonly log: Logger /** * The Liquid templating engine for variable substitution. * * @remarks * This engine instance is shared across all actions in the collection and * configured with custom filters for platform detection, path manipulation, * and xpm-specific operations. It's used during both template action name * expansion and later during individual action command substitution, * ensuring consistent template processing throughout the action lifecycle. */ readonly engine: LiquidEngine /** * The variables available for substitution in action definitions. * * @remarks * This comprehensive variable hierarchy provides context for template * evaluation, including package metadata, build configuration properties, * environment variables, platform detection, and path utilities. * * The hierarchy structure: * *
    *
  1. Base variables: env, os, * path (always available).
  2. *
  3. Package variables: name, version, * dependencies, * devDependencies.
  4. *
  5. Configuration variables: build folder paths, compiler * settings.
  6. *
  7. Properties: custom key-value pairs from package or * configuration.
  8. *
  9. Matrix: parameter combinations for template-generated * actions (added per action during initialisation).
  10. *
* * These variables are accessible in Liquid templates using dot notation * (e.g., `{{ package.name }}`, * `{{ configuration.buildFolderRelativePath }}`). */ readonly substitutionsVariables: LiquidSubstitutionsVariables /** * The JSON object containing action definitions from the package manifest. * * @remarks * This object holds the raw action definitions as they appear in the * `package.json` `xpack.actions` section or within a build configuration's * actions. Action definitions can be: * *
    *
  1. Simple strings: Single command to execute.
  2. *
  3. String arrays: Multiple commands executed sequentially.
  4. *
  5. Template objects: With matrix and * template properties for * generating multiple actions from a single definition.
  6. *
* * Template action names (containing `{{` markers) trigger matrix expansion * during initialisation, creating concrete actions from the Cartesian * product of matrix parameter values. */ readonly jsonActions: JsonActions /** * The build configuration this actions collection belongs to, if any. * * @remarks * This optional reference establishes the hierarchical relationship between * actions and build configurations, affecting variable substitution scope * and action inheritance. * * When defined: * *
    *
  1. Actions inherit configuration-specific variables (build folder paths, * compiler settings, toolchain properties).
  2. *
  3. Actions belong to a specific configuration namespace rather than the * package root.
  4. *
  5. Logging and diagnostics include the configuration name for * context.
  6. *
* * When `undefined`: * *
    *
  1. Actions belong to the package root (xpack.actions in * package.json).
  2. *
  3. Only package-level and global variables are available for * substitution.
  4. *
*/ readonly buildConfiguration: BuildConfiguration | undefined // -------------------------------------------------------------------------- // Protected Members. /** * Map of action names to their corresponding action instances. * * @remarks * This map serves as the primary action registry, populated during * collection initialisation with entries for all discovered actions. * * Key characteristics: * *
    *
  1. Known only after Actions.initialise() * completes.
  2. *
  3. Possibly empty if there are no actions defined.
  4. *
  5. Values can be undefined to indicate an action * exists but hasn't * been instantiated yet (lazy loading).
  6. *
  7. For template actions, contains one entry per expanded combination, * not the original template definition.
  8. *
* * Actions transition from `undefined` to instantiated when first accessed * via {@link Actions.get}, implementing the lazy evaluation * pattern. */ protected readonly _actionsMap: Map = new Map< string, Action | undefined >() /** * Set of all action names for quick lookup. * * @remarks * This set provides O(1) existence checks for action names, enabling * efficient validation during template expansion and duplicate detection. * * Key characteristics: * *
    *
  1. Known only after Actions.initialise() * completes.
  2. *
  3. Contains all action names including those generated from * templates.
  4. *
  5. Used to detect duplicate action names that might arise from template * expansion conflicts or explicit duplicates in * package.json.
  6. *
* * This redundant storage (alongside `_actionsMap`) is justified by the * performance benefit for name existence checks, especially in packages * with many actions. */ protected readonly _namesSet: Set = new Set() /** * Map of expanded action names to their original JSON action names. * * @remarks * This reverse mapping enables retrieving the original action definition * from `jsonActions` when lazy-loading action instances. * * Mapping behavior: * *
    *
  1. For regular actions: Maps action name to itself (identity * mapping).
  2. *
  3. For template actions: Maps each generated action name back to * the original template name (e.g., * test-x64test-\{\{ matrix.arch \}\}).
  4. *
  5. Enables Actions.get() to locate the correct JSON * definition when instantiating an action on demand.
  6. *
* * This indirection is essential for the lazy evaluation pattern, allowing * deferred instantiation while maintaining the connection to original * definitions. */ protected readonly _jsonActionsNamesMap: Map = new Map< string, string >() /** * Flag indicating whether the actions collection has been initialised. * * @remarks * This flag prevents redundant initialisation and ensures idempotent * behavior when {@link Actions.initialise} is called multiple * times. * * State transitions: * *
    *
  1. Initially false after construction.
  2. *
  3. Set to true after successful template expansion and * action name * registration.
  4. *
  5. Checked at the beginning of Actions.initialise() to * return early if already initialised.
  6. *
* * This pattern supports safe repeated calls during complex initialisation * sequences without duplicating work or corrupting internal state. */ protected _isInitialised = false /** * Cached array of all action names in the collection. * * @remarks * This array provides O(1) access to action names without repeatedly * creating new arrays from the map keys, improving performance when the * names are accessed multiple times. * * Key characteristics: * *
    *
  1. Empty initially after construction.
  2. *
  3. Populated during Actions.initialise() after all * action names * are determined.
  4. *
  5. Contains all action names including those generated from * templates.
  6. *
  7. Returned by the names getter for efficient repeated * access.
  8. *
* * This cached approach avoids the overhead of calling * `Array.from(map.keys())` on every access whilst still * providing a clean getter interface. */ protected _names: string[] = [] // -------------------------------------------------------------------------- // Constructor and async initialiser. /** * Constructs an actions collection instance. * * @remarks * The constructor performs partial initialisation. Complete initialisation * requires calling the `Actions.initialise()` method. * * @param log - The logger instance for output and diagnostics. */ constructor({ engine, substitutionsVariables, jsonActions, inheritedActionsMap, buildConfiguration, log, }: ActionsConstructorParameters) { assert(log, 'log is required') assert(engine, 'engine is required') assert(substitutionsVariables, 'substitutionsVariables is required') if (buildConfiguration !== undefined) { log.trace(`${Actions.name}()` + ` @${buildConfiguration.name}`) } else { log.trace(`${Actions.name}()`) } this.log = log this.engine = engine this.substitutionsVariables = substitutionsVariables this.jsonActions = jsonActions ?? {} if (buildConfiguration !== undefined) { this.buildConfiguration = buildConfiguration } // If there are inherited actions, add them to the map. // They might be overridden by the current definitions. if (inheritedActionsMap !== undefined) { for (const [ inheritedActionName, inheritedAction, ] of inheritedActionsMap) { // Make copies of the actions, do not alter the inherited ones. const action = new Action({ actionName: inheritedActionName, jsonAction: inheritedAction.jsonAction, parentActions: this, }) this._actionsMap.set(inheritedActionName, action) } } // The rest of the initialisation is done in the async initialiser. } /** * Completes the async initialisation of the actions collection. * * @remarks * This method implements the first step of lazy evaluation. It processes * all action definitions by expanding template action names based on matrix * parameters, but does not evaluate the action content or perform Liquid * substitutions. The actual template evaluation and variable substitution * occur later when individual actions are initialised via * {@link Action.initialise}, and only for actions that are * actually used. This approach avoids unnecessary operations on unused * actions. The method also validates that all expanded action names are * unique. * * @returns A promise that resolves to `true` if initialisation was * performed, or `false` if already initialised. * * @throws {@link ConfigurationError} * If duplicate action names are detected or if template expansion fails. */ async initialise(): Promise { const log = this.log if (this._isInitialised) { if (this.buildConfiguration !== undefined) { log.trace( `${Actions.name}.initialise()` + ` @${this.buildConfiguration.name} again` ) } else { log.trace(`${Actions.name}.initialise() again`) } return false } if (this.buildConfiguration !== undefined) { log.trace( `${Actions.name}.initialise()` + ` @${this.buildConfiguration.name}` ) } else { log.trace(`${Actions.name}.initialise()`) } for (const [actionName, jsonAction] of Object.entries(this.jsonActions)) { if (hasLiquidSyntax(actionName)) { await this._processTemplate({ actionName, jsonActionTemplate: jsonAction as JsonActionTemplate, }) } else { if (this._namesSet.has(actionName)) { throw new ConfigurationError( `action name "${actionName}" already defined` ) } else { this._actionsMap.set(actionName, undefined) this._jsonActionsNamesMap.set(actionName, actionName) this._namesSet.add(actionName) } } } const names = Array.from(this._actionsMap.keys()) this._names = names this.log.trace(`${Actions.name}.initialise() =>`, names) this._isInitialised = true return true } // -------------------------------------------------------------------------- // Public Methods. /** * The number of actions in the collection. * * @remarks * This value is known only after `initialise()`. * * This getter provides direct access to the collection size, enabling * callers to check for emptiness or iterate with knowledge of the * collection's extent. * * @returns The number of actions in the collection. */ get size(): number { assert( this._isInitialised, 'Actions collection must be initialised before accessing size' ) return this._actionsMap.size } /** * Indicates whether the actions collection is empty. * * @remarks * This value is known only after `initialise()`. * * @returns `true` if there are no actions, `false` otherwise. */ get isEmpty(): boolean { assert( this._isInitialised, 'Actions collection must be initialised before accessing isEmpty' ) return this._actionsMap.size === 0 } /** * The names of all actions in the collection. * * @remarks * This value is known only after `initialise()`. * * This getter returns the cached array of action names for efficient * repeated access without recreating the array. * * @returns An array of action names. */ get names(): string[] { assert( this._isInitialised, 'Actions collection must be initialised before accessing names' ) return this._names } /** * Checks whether an action with the specified name exists. * * @remarks * This value is known only after `initialise()`. * * @param actionName - The name of the action to check. * @returns `true` if the action exists, `false` otherwise. */ has(actionName: string): boolean { assert( this._isInitialised, 'Actions collection must be initialised before accessing has()' ) return this._actionsMap.has(actionName) } /** * Retrieves an action by name, creating it if not yet instantiated. * * @remarks * This method implements lazy evaluation to avoid unnecessary operations. * Actions are instantiated on demand but remain uninitialised until actually * used. The two-step process works as follows: * *
    *
  1. During collection initialisation * (Actions.initialise()), * only the matrix of options is evaluated for each template, expanding * only the action names without processing their content.
  2. *
  3. Later, when an action is accessed via this method and subsequently * initialised (Action.initialise()), the template is * fully evaluated and Liquid substitutions are performed on the * commands.
  4. *
* * This approach ensures that only actions that are actually used incur the * cost of template evaluation and variable substitution. * * @param actionName - The name of the action to retrieve. * @returns The action instance. * * @throws {@link ConfigurationError} * If an action with that name does not exist. */ get(actionName: string): Action { assert( this._isInitialised, 'Actions collection must be initialised before accessing get()' ) const log = this.log log.trace(`${Actions.name}.get(${actionName})`) let action = this._actionsMap.get(actionName) if (action === undefined) { const jsonActionName = this._jsonActionsNamesMap.get(actionName) if (jsonActionName === undefined) { throw new ConfigurationError(`action "${actionName}" does not exist`) } // Safety net: This fallback to empty string is defensive programming. // The jsonActions[jsonActionName] should always be defined because // _jsonActionsNamesMap is populated from the jsonActions keys during // initialisation. The ?? '' provides protection against unexpected // runtime inconsistencies between the map and the object. /* c8 ignore start - safety net, action names are not undefined. */ const jsonAction: JsonActionContent = (this.jsonActions[jsonActionName] ?? '') as JsonActionContent /* c8 ignore stop */ action = new Action({ actionName, jsonAction, parentActions: this, }) this._actionsMap.set(actionName, action) } return action } // -------------------------------------------------------------------------- // Private Methods. /** * Processes a template action by expanding it and registering the generated * actions. * * @remarks * This helper method is called during collection initialisation for each * action whose name contains template syntax (\{\{ markers). * * Processing steps: * *
    *
  1. Calls _expandTemplateActions() to generate all action * instances from the template's matrix parameters.
  2. *
  3. Validates that each expanded action name is unique and does not * conflict with existing actions.
  4. *
  5. Registers each expanded action in the internal maps: *
      *
    • _actionsMap: Maps name to action instance.
    • *
    • _jsonActionsNamesMap: Maps expanded name back to * original template name.
    • *
    • _namesSet: Tracks all registered names for * duplicate detection.
    • *
    *
  6. *
* * @param actionName - The template action name containing Liquid variables. * @param jsonActionTemplate - The JSON template definition containing matrix * parameters and an action template. * @returns A promise that resolves when processing is complete. * * @throws {@link ConfigurationError} * If duplicate action names are detected during expansion or if template * expansion fails. */ protected async _processTemplate({ actionName, jsonActionTemplate, }: { actionName: string jsonActionTemplate: JsonActionTemplate }): Promise { // Expand template and generate multiple actions. try { const expandedActionsMap = await this._expandTemplateActions({ actionName, jsonActionTemplate, }) for (const [expandedActionName, expandedAction] of expandedActionsMap) { if (this._namesSet.has(expandedActionName)) { throw new ConfigurationError( `duplicate action name "${expandedActionName}" ` + `could not be generated from template.` ) } else { this._actionsMap.set(expandedActionName, expandedAction) this._jsonActionsNamesMap.set(expandedActionName, actionName) this._namesSet.add(expandedActionName) } } } catch (error) { const message = getErrorMessage(error) + ` in action "${actionName}"` throw new ConfigurationError(message) } } /** * Expands a template action into multiple concrete actions. * * @remarks * This method uses the {@link TemplateExpander} to compute the Cartesian * product of all matrix parameter values and creates a separate action for * each combination, substituting matrix values into both the action name * and command templates. * * Processing steps: * *
    *
  1. Validates matrix and template structure.
  2. *
  3. Delegates to TemplateExpander for matrix processing and * name expansion.
  4. *
  5. Creates action instances via factory callback for each * combination.
  6. *
* * Matrix variables are scoped to individual actions and accessible via * the `matrix` namespace during action command evaluation. * * @param actionName - The template action name containing Liquid variables. * @param jsonActionTemplate - The JSON action template definition containing * matrix parameters and a template. * @returns A promise that resolves to a map of expanded action names to * their corresponding action instances. * * @throws {@link ConfigurationError} * If the matrix structure is invalid, template format is incorrect, or * substitution fails. */ protected async _expandTemplateActions({ actionName, jsonActionTemplate, }: { actionName: string jsonActionTemplate: JsonActionTemplate }): Promise> { const log = this.log log.trace(`${Actions.name}.#expandTemplateActions(${actionName})`) // Validate template structure // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (jsonActionTemplate.matrix == undefined) { throw new ConfigurationError(`action "${actionName}" has no matrix`) } if (!isJsonObject(jsonActionTemplate.matrix)) { throw new ConfigurationError( `action "${actionName}" matrix is not an object` ) } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (jsonActionTemplate.template == undefined) { throw new ConfigurationError(`action "${actionName}" has no template`) } if ( !isString(jsonActionTemplate.template) && !isJsonArray(jsonActionTemplate.template) ) { throw new ConfigurationError( `action "${actionName}" template is not a string or array` ) } // Use TemplateExpander for matrix processing and expansion const expander = new TemplateExpander({ engine: this.engine, substitutionsVariables: this.substitutionsVariables, log: this.log, }) return await expander.expandTemplate({ templateName: actionName, matrix: jsonActionTemplate.matrix, templateContent: jsonActionTemplate.template, templateType: 'action', instanceFactory: ( expandedName: string, combination: Record, templateContent: JsonActionContent ) => new Action({ actionName: expandedName, jsonAction: templateContent, parentActions: this, matrixParameters: { ...combination }, }), }) } } // ============================================================================ /** * Configuration parameters for constructing an action instance. * * @remarks * This interface defines the required configuration for creating an * instance of {@link Action}. Most properties are mandatory except for * the optional matrixParameters, which is only needed for * template-generated actions that were created from matrix expansion. * * The parameters provide the action with its identity (name), command * definitions, access to the parent collection for shared resources, and * optional matrix parameter values for template-generated actions. */ export interface ActionConstructorParameters { /** * The name of the action. */ actionName: string /** * The JSON definition of the action commands. */ jsonAction: JsonActionContent /** * The parent actions collection this action belongs to. */ parentActions: Actions /** * Optional matrix parameter values for template-generated actions. */ matrixParameters?: LiquidSubstitutionsStrings } /** * An individual xpm action containing commands to be executed. * * @remarks * Actions are lazily initialised, with variable substitution performed * only when the action is first retrieved and initialised. This allows for * efficient handling of large numbers of actions generated * from templates. * * An action can exist in three states: * *
    *
  1. Undefined: Name is known but instance not yet created.
  2. *
  3. Instantiated: Object exists but commands not yet evaluated.
  4. *
  5. Initialised: Commands fully evaluated with Liquid * substitutions.
  6. *
* * This design minimizes memory usage and computation for actions that are * defined but never executed, which is common when using matrix templates * to generate platform-specific or configuration-specific actions. */ export class Action { // -------------------------------------------------------------------------- // Public Members. /** * The name of the action. * * @remarks * This is the final, expanded action name used for identification and * execution. For template-generated actions, this is the concrete name * after matrix substitution (e.g., `test-x64` rather than * `test-{{ matrix.arch }}`). * * The name is used for: * *
    *
  1. User-facing identification when listing or executing actions.
  2. *
  3. Logging and diagnostic output to track action lifecycle.
  4. *
  5. Creating copies of inherited actions with preserved names.
  6. *
* * Names must be unique within the actions collection, enforced during * {@link Actions.initialise}. */ readonly name: string /** * The JSON definition of the action commands. * * @remarks * This holds the raw command definition as it appears in `package.json`, * before variable substitution. The format can be: * *
    *
  1. Simple string: Single command line.
  2. *
  3. String array: Multiple commands for sequential execution.
  4. *
* * The definition is preserved in its original form to enable: * *
    *
  1. Creating copies of inherited actions with identical definitions.
  2. *
  3. Deferred template evaluation during * Action.initialise().
  4. *
  5. Re-evaluation if needed with different variable contexts.
  6. *
* * This immutable storage ensures actions can be safely copied and * initialised multiple times without side effects. */ readonly jsonAction: JsonActionContent /** * The parent actions collection this action belongs to. * * @remarks * This reference maintains the hierarchical relationship between individual * actions and their containing collection, providing essential context for * action initialisation and execution. * * The parent collection provides access to: * *
    *
  1. Liquid templating engine for variable substitution.
  2. *
  3. Substitution variables hierarchy (package metadata, configuration, * environment, platform detection).
  4. *
  5. Logger instance for diagnostic output.
  6. *
  7. Build configuration context when actions belong to a specific * configuration rather than the package root.
  8. *
* * This design enables actions to access shared resources without duplicating * them, while maintaining proper scoping for template evaluation. During * initialisation, the action combines parent-level substitution variables * with its own matrix parameters to create a complete context for Liquid * template processing. */ readonly parentActions: Actions /** * The matrix parameter values for template-generated actions. * * @remarks * For template-generated actions, this object contains the specific matrix * parameter values that produced this action instance from the template. * * Usage pattern: * *
    *
  1. Undefined for regular (non-template) actions.
  2. *
  3. For template actions, contains key-value pairs from the matrix * combination (e.g., * \{ arch: 'x64', platform: 'linux' \}).
  4. *
  5. Merged into substitution variables during * Action.initialise(), making values accessible via the * matrix namespace in command templates.
  6. *
  7. Enables the same command template to generate different concrete * commands for each matrix combination.
  8. *
* * Example: A template with `{{ matrix.arch }}` becomes `x64` when this * action's matrix parameters include `{ arch: 'x64' }`. */ protected readonly _matrixParameters?: LiquidSubstitutionsStrings /** * The array of command strings after variable substitution. * * @remarks * This array contains the fully evaluated command lines ready for * execution, with all Liquid template variables substituted. * * Lifecycle states: * *
    *
  1. Undefined initially and until Action.initialise() * is called.
  2. *
  3. Populated during initialisation by evaluating * jsonAction with the * Liquid engine and complete variable context.
  4. *
  5. Array-based JSON definitions are joined, substituted, then split back * into individual command lines.
  6. *
  7. Each string represents one command line to be executed * sequentially.
  8. *
* * Attempting to access via the `commands` getter before initialisation * will trigger an assertion error, enforcing correct usage order. */ protected _commands?: string[] /** * Flag indicating whether the action has been initialised. * * @remarks * This flag ensures idempotent initialization and prevents redundant * template evaluation when {@link Action.initialise} is called * multiple times. * * State transitions: * *
    *
  1. Initially false after construction.
  2. *
  3. Set to true after successful command substitution and * evaluation.
  4. *
  5. Checked at the start of Action.initialise() to * return early if already initialised.
  6. *
* * This pattern allows safe repeated calls during complex initialization * sequences or when actions are accessed multiple times, avoiding the * computational cost of re-evaluating templates unnecessarily. */ protected _isInitialised = false // -------------------------------------------------------------------------- // Constructor and async initialiser. /** * Constructs an action instance. * * @remarks * The constructor performs partial initialisation. Variable substitution * requires calling the {@link Action.initialise} method. * * @param actionName - The name of the action. * @param jsonAction - The JSON definition of the action commands. * @param parentActions - The parent actions collection this action belongs * to. * @param matrixParameters - Optional matrix parameter values for * template-generated actions. */ constructor({ actionName, jsonAction, parentActions, matrixParameters, }: ActionConstructorParameters) { assert(actionName, 'actionName is required') // assert(jsonAction) // Can be an empty string. assert(parentActions, 'parentActions is required') const log = parentActions.log log.trace(`${Action.name}(${actionName})`) this.name = actionName this.jsonAction = jsonAction this.parentActions = parentActions if (matrixParameters !== undefined) { this._matrixParameters = matrixParameters } } /** * Completes the async initialisation of the action. * * @remarks * This method performs variable substitution on the action commands using * the Liquid templating engine and the available substitution variables, * including any matrix parameters for template-generated actions. * * The substitution context includes: * *
    *
  1. All package-level substitution variables (configuration, package * metadata, platform detection, etc.).
  2. *
  3. Build configuration variables if this action belongs to a * configuration.
  4. *
  5. Matrix parameters for template-generated actions, accessible via * the matrix namespace (e.g., * \{\{ matrix.arch \}\}).
  6. *
* * Array-based command definitions are joined with newlines before * substitution, then split back into individual commands after processing. * This allows commands to span multiple array elements while maintaining * clean formatting in the package manifest. * * @returns A promise that resolves to `true` if initialisation was * performed, or `false` if already initialised. * * @throws {@link ConfigurationError} * If command substitution fails. */ async initialise(): Promise { const log = this.parentActions.log if (this._isInitialised) { log.trace(`${Action.name}.initialise(${this.name}) again`) return false } log.trace(`${Action.name}.initialise(${this.name})`) // Silently accept empty or non-existing actions. const jsonAction = this.jsonAction const inputCommands = Array.isArray(jsonAction) ? jsonAction.join(os.EOL) : jsonAction let substitutedCommands if (hasLiquidSyntax(inputCommands)) { try { substitutedCommands = await performSubstitutions({ input: inputCommands, engine: this.parentActions.engine, substitutionsVariables: { ...this.parentActions.substitutionsVariables, matrix: this._matrixParameters ?? {}, }, log, }) } catch (error) { const message = getErrorMessage(error) + ` in action "${this.name}" commands substitution` throw new ConfigurationError(message) } } else { substitutedCommands = inputCommands } this._commands = substitutedCommands .replace(new RegExp(os.EOL + '$'), '') .split(os.EOL) log.trace(`${Action.name}.initialise() =>`, this.name) log.trace(this.name, 'commands =>', this._commands) this._isInitialised = true return true } // -------------------------------------------------------------------------- // Public Methods. /** * Retrieves the array of command strings for this action. * * @remarks * The action must be initialised via {@link Action.initialise} * before accessing this property. Attempting to access commands from an * uninitialised action will result in an assertion error. * * @returns The array of command strings after variable substitution. */ get commands(): string[] { assert( this._isInitialised, 'Action must be initialised before accessing commands' ) assert(this._commands, 'Action _commands not initialised') return this._commands } } // ----------------------------------------------------------------------------