2026-02-11 09:44:01 -06:00
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
2025-11-07 14:18:52 -06:00
using System ;
using System.Collections.Generic ;
using System.Globalization ;
using System.Linq ;
using GitHub.Actions.Expressions ;
using GitHub.Actions.Expressions.Data ;
using GitHub.Actions.Expressions.Sdk ;
using GitHub.Actions.Expressions.Sdk.Functions ;
using GitHub.Actions.WorkflowParser.ObjectTemplating ;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens ;
namespace GitHub.Actions.WorkflowParser.Conversion
{
internal static class WorkflowTemplateConverter
{
/// <summary>
/// Constructs the <c ref="WorkflowTemplate" />. Errors are stored to both <c ref="TemplateContext.Errors" /> and <c ref="WorkflowTemplate.Errors" />.
/// </summary>
internal static WorkflowTemplate ConvertToWorkflow (
TemplateContext context ,
TemplateToken workflow )
{
var result = new WorkflowTemplate ( ) ;
result . FileTable . AddRange ( context . GetFileTable ( ) ) ;
// Note, the "finally" block appends context.Errors to result
try
{
if ( workflow = = null | | context . Errors . Count > 0 )
{
return result ;
}
var workflowMapping = workflow . AssertMapping ( "root" ) ;
foreach ( var workflowPair in workflowMapping )
{
var workflowKey = workflowPair . Key . AssertString ( "root key" ) ;
switch ( workflowKey . Value )
{
case WorkflowTemplateConstants . On :
var inputTypes = ConvertToOnWorkflowDispatchInputTypes ( workflowPair . Value ) ;
2026-02-11 09:44:01 -06:00
foreach ( var item in inputTypes )
2025-11-07 14:18:52 -06:00
{
result . InputTypes . TryAdd ( item . Key , item . Value ) ;
}
break ;
case WorkflowTemplateConstants . Description :
case WorkflowTemplateConstants . Name :
case WorkflowTemplateConstants . RunName :
break ;
case WorkflowTemplateConstants . Defaults :
result . Defaults = workflowPair . Value ;
break ;
case WorkflowTemplateConstants . Env :
result . Env = workflowPair . Value ;
break ;
case WorkflowTemplateConstants . Concurrency :
ConvertToConcurrency ( context , workflowPair . Value , isEarlyValidation : true ) ;
result . Concurrency = workflowPair . Value ;
break ;
case WorkflowTemplateConstants . Jobs :
result . Jobs . AddRange ( ConvertToJobs ( context , workflowPair . Value ) ) ;
break ;
case WorkflowTemplateConstants . Permissions :
result . Permissions = ConvertToPermissions ( context , workflowPair . Value ) ;
break ;
default :
workflowKey . AssertUnexpectedValue ( "root key" ) ; // throws
break ;
}
}
// Propagate explicit permissions
if ( result . Permissions ! = null )
{
foreach ( var job in result . Jobs )
{
if ( job . Permissions = = null )
{
job . Permissions = result . Permissions ;
}
}
}
}
catch ( Exception ex )
{
context . Errors . Add ( ex ) ;
}
finally
{
if ( context . Errors . Count > 0 )
{
foreach ( var error in context . Errors )
{
result . Errors . Add ( new WorkflowValidationError ( error . Code , error . Message ) ) ;
}
}
}
return result ;
}
internal static void ConvertToReferencedWorkflow (
TemplateContext context ,
TemplateToken referencedWorkflow ,
ReusableWorkflowJob workflowJob ,
String permissionsPolicy ,
bool isTrusted )
{
// Explicit max permissions for the reusable workflow or reusable workflow chain.
// Present only when the caller (or higher ancestor) has defined the maximum allowed permissions in YAML.
var explicitMaxPermissions = workflowJob . Permissions ;
var workflowMapping = referencedWorkflow . AssertMapping ( "root" ) ;
foreach ( var workflowPair in workflowMapping )
{
var workflowKey = workflowPair . Key . AssertString ( "root key" ) ;
switch ( workflowKey . Value )
{
case WorkflowTemplateConstants . On :
ConvertToOnTrigger ( context , workflowPair . Value , workflowJob ) ;
break ;
case WorkflowTemplateConstants . Description :
case WorkflowTemplateConstants . Name :
case WorkflowTemplateConstants . RunName :
break ;
case WorkflowTemplateConstants . Defaults :
workflowJob . Defaults = workflowPair . Value ;
break ;
case WorkflowTemplateConstants . Env :
workflowJob . Env = workflowPair . Value ;
break ;
case WorkflowTemplateConstants . Concurrency :
ConvertToConcurrency ( context , workflowPair . Value , true ) ;
workflowJob . EmbeddedConcurrency = workflowPair . Value ;
break ;
case WorkflowTemplateConstants . Jobs :
workflowJob . Jobs . AddRange ( ConvertToJobs ( context , workflowPair . Value ) ) ;
break ;
case WorkflowTemplateConstants . Permissions :
var embeddedRootPermissions = ConvertToPermissions ( context , workflowPair . Value ) ;
PermissionsHelper . ValidateEmbeddedPermissions (
context ,
workflowJob ,
embeddedJob : null ,
requested : embeddedRootPermissions ,
explicitMax : explicitMaxPermissions ,
permissionsPolicy ,
isTrusted ) ;
workflowJob . Permissions = embeddedRootPermissions ;
break ;
default :
workflowKey . AssertUnexpectedValue ( "root key" ) ; // throws
break ;
}
}
// Validate requested permissions or propagate explicit permissions
foreach ( var embeddedJob in workflowJob . Jobs )
{
if ( embeddedJob . Permissions ! = null )
{
// Validate requested permissions
PermissionsHelper . ValidateEmbeddedPermissions (
context ,
workflowJob ,
embeddedJob ,
requested : embeddedJob . Permissions ,
explicitMax : explicitMaxPermissions ,
permissionsPolicy ,
isTrusted ) ;
}
else if ( workflowJob . Permissions ! = null )
{
// Propagate explicit permissions
embeddedJob . Permissions = workflowJob . Permissions ;
}
}
}
internal static IDictionary < String , String > ConvertToOnWorkflowDispatchInputTypes ( TemplateToken onToken )
{
var result = new Dictionary < String , String > ( ) ;
if ( onToken . Type ! = TokenType . Mapping )
{
return result ;
}
var triggerMapping = onToken . AssertMapping ( $"workflow {WorkflowTemplateConstants.On} value" ) ;
var dispatchTrigger = triggerMapping . FirstOrDefault ( x = >
string . Equals ( ( x . Key as StringToken ) . Value , WorkflowTemplateConstants . WorkflowDispatch , StringComparison . Ordinal ) ) . Value ;
if ( dispatchTrigger = = null | | dispatchTrigger is NullToken )
{
return result ;
}
var wfDispatchDefinitions = dispatchTrigger . AssertMapping ( $"workflow {WorkflowTemplateConstants.On} value {WorkflowTemplateConstants.WorkflowDispatch}" ) ;
var inputDefinitionsToken = wfDispatchDefinitions . FirstOrDefault ( x = >
string . Equals ( ( x . Key as StringToken ) . Value , WorkflowTemplateConstants . Inputs , StringComparison . Ordinal ) ) . Value ;
if ( inputDefinitionsToken = = null | | inputDefinitionsToken is NullToken )
{
return result ;
}
var inputs = inputDefinitionsToken . AssertMapping ( $"{WorkflowTemplateConstants.On}-{WorkflowTemplateConstants.WorkflowDispatch}-{WorkflowTemplateConstants.Inputs}" ) ;
var inputDefinitions = inputs ?
. ToDictionary (
x = > x . Key . AssertString ( "inputs key" ) . Value ,
x = > x . Value ,
StringComparer . OrdinalIgnoreCase
) ;
foreach ( var definedItem in inputDefinitions )
{
string definedKey = definedItem . Key ;
if ( definedItem . Value is NullToken )
{
result . Add ( definedKey , WorkflowTemplateConstants . TypeString ) ;
continue ;
}
var definedInputSpec = definedItem . Value . AssertMapping ( $"input {definedKey}" ) . ToDictionary (
x = > x . Key . AssertString ( $"input {definedKey} key" ) . Value ,
x = > x . Value ,
StringComparer . OrdinalIgnoreCase ) ;
if ( definedInputSpec . TryGetValue ( WorkflowTemplateConstants . Type , out TemplateToken inputTypeToken ) & &
inputTypeToken is StringToken inputTypeStringToken )
{
result . Add ( definedKey , inputTypeStringToken . Value ) ;
}
else
{
result . Add ( definedKey , WorkflowTemplateConstants . TypeString ) ;
}
}
return result ;
}
internal static void ConvertToOnTrigger (
TemplateContext context ,
TemplateToken onToken ,
ReusableWorkflowJob parentWorkflowJob )
{
switch ( onToken . Type )
{
// check for on: workflow_call
case TokenType . String :
var result = onToken . AssertString ( $"Reference workflow {WorkflowTemplateConstants.On} value" ) ;
if ( result . Value = = WorkflowTemplateConstants . WorkflowCall )
{
ValidateWorkflowJobTrigger ( context , parentWorkflowJob ) ;
return ;
}
break ;
// check for on: [push, workflow_call]
case TokenType . Sequence :
var triggers = onToken . AssertSequence ( $"Reference workflow {WorkflowTemplateConstants.On} value" ) ;
foreach ( var triggerItem in triggers )
{
var triggerString = triggerItem . AssertString ( $"Reference workflow {WorkflowTemplateConstants.On} value {triggerItem}" ) . Value ;
if ( triggerString = = WorkflowTemplateConstants . WorkflowCall )
{
ValidateWorkflowJobTrigger ( context , parentWorkflowJob ) ;
return ;
}
}
break ;
// check for on: workflow_call Mapping
case TokenType . Mapping :
var triggerMapping = onToken . AssertMapping ( $"Reference workflow {WorkflowTemplateConstants.On} value" ) ;
foreach ( var triggerItem in triggerMapping )
{
var triggerString = triggerItem . Key . AssertString ( $"Reference workflow {WorkflowTemplateConstants.On} value {triggerItem.Key}" ) . Value ;
if ( triggerString = = WorkflowTemplateConstants . WorkflowCall )
{
if ( triggerItem . Value is NullToken )
{
ValidateWorkflowJobTrigger ( context , parentWorkflowJob ) ;
return ;
}
var wfCallDefinitions = triggerItem . Value . AssertMapping ( $"Reference workflow {WorkflowTemplateConstants.On} value {triggerItem.Key}" ) ;
foreach ( var wfCallDefinitionItem in wfCallDefinitions )
{
var wfCallDefinitionItemKey = wfCallDefinitionItem . Key . AssertString ( $"{WorkflowTemplateConstants.On}-{WorkflowTemplateConstants.WorkflowCall}-{wfCallDefinitionItem.Key.ToString()}" ) . Value ;
if ( wfCallDefinitionItemKey = = WorkflowTemplateConstants . Inputs )
{
parentWorkflowJob . InputDefinitions = wfCallDefinitionItem . Value . AssertMapping ( $"{WorkflowTemplateConstants.On}-{WorkflowTemplateConstants.WorkflowCall}-{wfCallDefinitionItem.Key.ToString()}" ) ;
}
else if ( wfCallDefinitionItemKey = = WorkflowTemplateConstants . Secrets )
{
parentWorkflowJob . SecretDefinitions = wfCallDefinitionItem . Value . AssertMapping ( $"{WorkflowTemplateConstants.On}-{WorkflowTemplateConstants.WorkflowCall}-{wfCallDefinitionItem.Key.ToString()}" ) ;
}
else if ( wfCallDefinitionItemKey = = WorkflowTemplateConstants . Outputs )
{
parentWorkflowJob . Outputs = wfCallDefinitionItem . Value . AssertMapping ( $"{WorkflowTemplateConstants.On}-{WorkflowTemplateConstants.WorkflowCall}-{wfCallDefinitionItem.Key.ToString()}" ) ;
}
}
ValidateWorkflowJobTrigger ( context , parentWorkflowJob ) ;
return ;
}
}
break ;
default :
break ;
}
context . Error ( onToken , $"{WorkflowTemplateConstants.WorkflowCall} key is not defined in the referenced workflow." ) ;
return ;
}
internal static Boolean ConvertToIfResult (
TemplateContext context ,
TemplateToken ifResult )
{
var expression = ifResult . Traverse ( ) . FirstOrDefault ( x = > x is ExpressionToken ) ;
if ( expression ! = null )
{
throw new ArgumentException ( $"Unexpected type '{expression.GetType().Name}' encountered while reading 'if'." ) ;
}
var evaluationResult = EvaluationResult . CreateIntermediateResult ( null , ifResult ) ;
return evaluationResult . IsTruthy ;
}
internal static String ConvertToJobName (
TemplateContext context ,
TemplateToken name ,
Boolean isEarlyValidation = false )
{
var result = default ( String ) ;
// Expression
if ( isEarlyValidation & & name is ExpressionToken )
{
return result ;
}
// String
var nameString = name . AssertString ( $"job {WorkflowTemplateConstants.Name}" ) ;
result = nameString . Value ;
return result ;
}
internal static Snapshot ConvertToSnapshot (
TemplateContext context ,
TemplateToken snapshotToken ,
Boolean isEarlyValidation = false )
{
String imageName = null ;
string defaultVersion = "1.*" ;
String version = null ;
var condition = new BasicExpressionToken ( null , null , null , $"{WorkflowTemplateConstants.Success}()" ) ;
if ( isEarlyValidation & & snapshotToken is ExpressionToken )
{
return default ;
}
// String
if ( snapshotToken is StringToken snapshotStringToken )
{
imageName = snapshotStringToken . Value ;
}
// Mapping
else if ( snapshotToken is MappingToken snapshotMappingToken )
{
foreach ( var snapshotProperty in snapshotMappingToken )
{
var propertyName = snapshotProperty . Key . AssertString ( $"job {WorkflowTemplateConstants.Snapshot} key" ) ;
var propertyValue = snapshotProperty . Value ;
switch ( propertyName . Value )
{
case WorkflowTemplateConstants . ImageName :
if ( isEarlyValidation & & propertyValue is ExpressionToken )
{
return default ;
}
imageName = propertyValue . AssertString ( $"job {WorkflowTemplateConstants.Snapshot} {WorkflowTemplateConstants.ImageName}" ) . Value ;
break ;
case WorkflowTemplateConstants . If :
condition = ConvertToIfCondition ( context , propertyValue , IfKind . Snapshot ) ;
break ;
case WorkflowTemplateConstants . CustomImageVersion :
if ( isEarlyValidation & & propertyValue is ExpressionToken )
{
return default ;
}
var versionValue = propertyValue . AssertString ( $"job {WorkflowTemplateConstants.Snapshot} {WorkflowTemplateConstants.CustomImageVersion}" ) . Value ;
if ( versionValue ! = null & & ! IsSnapshotImageVersionValid ( versionValue ) )
{
context . Error ( snapshotToken , "Expected format '{major-version}.*' Actual '" + versionValue + "'" ) ;
return null ;
}
version = versionValue ;
break ;
default :
propertyName . AssertUnexpectedValue ( $"job {WorkflowTemplateConstants.Snapshot} key" ) ;
break ;
}
}
}
// ImageName is a required property (schema validation)
if ( imageName = = null )
{
context . Error ( snapshotToken , $"job {WorkflowTemplateConstants.Snapshot} {WorkflowTemplateConstants.ImageName} is required." ) ;
return null ;
}
2026-02-11 09:44:01 -06:00
2025-11-07 14:18:52 -06:00
return new Snapshot
{
ImageName = imageName ,
If = condition ,
Version = version ? ? defaultVersion
} ;
}
private static bool IsSnapshotImageVersionValid ( string versionString )
{
var versionSegments = versionString . Split ( "." ) ;
2026-02-11 09:44:01 -06:00
if ( versionSegments . Length ! = 2 | |
2025-11-07 14:18:52 -06:00
! versionSegments [ 1 ] . Equals ( "*" ) | |
! Int32 . TryParse ( versionSegments [ 0 ] , NumberStyles . None , CultureInfo . InvariantCulture , result : out int parsedMajor ) | |
parsedMajor < 0 )
{
return false ;
}
return true ;
}
internal static RunsOn ConvertToRunsOn (
TemplateContext context ,
TemplateToken runsOn ,
Boolean isEarlyValidation = false )
{
var result = new RunsOn ( ) ;
ConvertToRunsOnLabels ( context , runsOn , result , isEarlyValidation ) ;
// Mapping
if ( runsOn is MappingToken runsOnMapping )
{
foreach ( var runsOnToken in runsOnMapping )
{
var propertyName = runsOnToken . Key . AssertString ( $"job {WorkflowTemplateConstants.RunsOn} property name" ) ;
switch ( propertyName . Value )
{
case WorkflowTemplateConstants . Group :
// Expression
if ( isEarlyValidation & & runsOnToken . Value is ExpressionToken )
{
continue ;
}
// String
var groupName = runsOnToken . Value . AssertString ( $"job {WorkflowTemplateConstants.RunsOn} {WorkflowTemplateConstants.Group} name" ) . Value ;
var names = groupName . Split ( WorkflowTemplateConstants . Slash ) ;
if ( names . Length = = 2 )
{
if ( string . IsNullOrEmpty ( names [ 1 ] ) )
{
context . Error ( runsOnToken . Value , $"Invalid {WorkflowTemplateConstants.RunsOn} {WorkflowTemplateConstants.Group} name '{groupName}'." ) ;
}
else if ( ! string . Equals ( names [ 0 ] , WorkflowTemplateConstants . Org ) & & ! string . Equals ( names [ 0 ] , WorkflowTemplateConstants . Organization ) & &
! string . Equals ( names [ 0 ] , WorkflowTemplateConstants . Ent ) & & ! string . Equals ( names [ 0 ] , WorkflowTemplateConstants . Enterprise ) )
{
context . Error ( runsOnToken . Value , $"Invalid {WorkflowTemplateConstants.RunsOn} {WorkflowTemplateConstants.Group} name '{groupName}'. Please use 'organization/' or 'enterprise/' prefix to target a single runner group." ) ;
}
else
{
result . RunnerGroup = groupName ;
}
}
else if ( names . Length > 2 )
{
context . Error ( runsOnToken . Value , $"Invalid {WorkflowTemplateConstants.RunsOn} {WorkflowTemplateConstants.Group} name '{groupName}'. Please use 'organization/' or 'enterprise/' prefix to target a single runner group." ) ;
}
else
{
result . RunnerGroup = groupName ;
}
break ;
case WorkflowTemplateConstants . Labels :
ConvertToRunsOnLabels ( context , runsOnToken . Value , result , isEarlyValidation ) ;
break ;
}
}
}
return result ;
}
internal static void ConvertToRunsOnLabels (
TemplateContext context ,
TemplateToken runsOnLabelsToken ,
RunsOn runsOn ,
Boolean isEarlyValidation = false )
{
// Expression
if ( isEarlyValidation & & runsOnLabelsToken is ExpressionToken )
{
return ;
}
// String
if ( runsOnLabelsToken is StringToken runsOnLabelsString )
{
runsOn . Labels . Add ( runsOnLabelsString . Value ) ;
}
// Sequence<String>
else if ( runsOnLabelsToken is SequenceToken runsOnLabelsSequence )
{
foreach ( var runsOnLabelToken in runsOnLabelsSequence )
{
// Expression
if ( isEarlyValidation & & runsOnLabelToken is ExpressionToken )
{
continue ;
}
// String
var label = runsOnLabelToken . AssertString ( $"job {WorkflowTemplateConstants.RunsOn} {WorkflowTemplateConstants.Labels} sequence item" ) ;
runsOn . Labels . Add ( label . Value ) ;
}
}
}
internal static Int32 ? ConvertToJobTimeout (
TemplateContext context ,
TemplateToken token ,
Boolean isEarlyValidation = false )
{
if ( isEarlyValidation & & token is ExpressionToken )
{
return null ;
}
var numberToken = token . AssertNumber ( $"job {WorkflowTemplateConstants.TimeoutMinutes}" ) ;
return ( Int32 ) numberToken . Value ;
}
internal static Int32 ? ConvertToJobCancelTimeout (
TemplateContext context ,
TemplateToken token ,
Boolean isEarlyValidation = false )
{
if ( isEarlyValidation & & token is ExpressionToken )
{
return null ;
}
var numberToken = token . AssertNumber ( $"job {WorkflowTemplateConstants.CancelTimeoutMinutes}" ) ;
return ( Int32 ) numberToken . Value ;
}
internal static Boolean ? ConvertToJobContinueOnError (
TemplateContext context ,
TemplateToken token ,
Boolean isEarlyValidation = false )
{
if ( isEarlyValidation & & token is ExpressionToken )
{
return null ;
}
var booleanToken = token . AssertBoolean ( $"job {WorkflowTemplateConstants.ContinueOnError}" ) ;
return booleanToken . Value ;
}
internal static Boolean ? ConvertToStepContinueOnError (
TemplateContext context ,
TemplateToken token ,
Boolean isEarlyValidation = false )
{
if ( isEarlyValidation & & token is ExpressionToken )
{
return null ;
}
var booleanToken = token . AssertBoolean ( $"step {WorkflowTemplateConstants.ContinueOnError}" ) ;
return booleanToken . Value ;
}
internal static String ConvertToStepName (
TemplateContext context ,
TemplateToken token ,
Boolean isEarlyValidation = false )
{
if ( isEarlyValidation & & token is ExpressionToken )
{
return null ;
}
var stringToken = token . AssertString ( $"step {WorkflowTemplateConstants.Name}" ) ;
return stringToken . Value ;
}
internal static GroupPermitSetting ConvertToConcurrency (
TemplateContext context ,
TemplateToken concurrency ,
Boolean isEarlyValidation = false )
{
var result = new GroupPermitSetting ( "" ) ;
// Expression
if ( isEarlyValidation & & concurrency is ExpressionToken )
{
return result ;
}
// String
if ( concurrency is StringToken concurrencyString )
{
result . Group = concurrencyString . Value ;
}
// Mapping
else
{
var concurrencyMapping = concurrency . AssertMapping ( $"{WorkflowTemplateConstants.Concurrency}" ) ;
foreach ( var concurrencyProperty in concurrencyMapping )
{
var propertyName = concurrencyProperty . Key . AssertString ( $"{WorkflowTemplateConstants.Concurrency} key" ) ;
// Expression
if ( isEarlyValidation & & ( concurrencyProperty . Value is ExpressionToken | | concurrencyProperty . Key is ExpressionToken ) )
{
continue ;
}
switch ( propertyName . Value )
{
case WorkflowTemplateConstants . Group :
// Literal
var group = concurrencyProperty . Value . AssertString ( $"{WorkflowTemplateConstants.Group} key" ) ;
result . Group = group . Value ;
break ;
case WorkflowTemplateConstants . CancelInProgress :
// Literal
var cancelInProgress = concurrencyProperty . Value . AssertBoolean ( $"{WorkflowTemplateConstants.CancelInProgress} key" ) ;
result . CancelInProgress = cancelInProgress . Value ;
break ;
default :
propertyName . AssertUnexpectedValue ( $"{WorkflowTemplateConstants.Concurrency} key" ) ; // throws
break ;
}
}
}
if ( ! isEarlyValidation & & String . IsNullOrEmpty ( result . Group ) )
{
context . Error ( concurrency , "Concurrency group name cannot be empty" ) ;
}
if ( result . Group ? . Length > 400 )
{
context . Error ( concurrency , "Concurrency group name must be less than 400 characters" ) ;
}
return result ;
}
internal static ActionsEnvironmentReference ConvertToActionEnvironmentReference (
TemplateContext context ,
TemplateToken environment ,
bool isEarlyValidation = false )
{
// Expression
if ( isEarlyValidation & & environment is ExpressionToken )
{
return null ;
}
// String
else if ( environment is StringToken nameString )
{
return String . IsNullOrEmpty ( nameString . Value ) ? null : new ActionsEnvironmentReference ( nameString . Value ) ;
}
// Mapping
else
{
var environmentMapping = environment . AssertMapping ( $"job {WorkflowTemplateConstants.Environment}" ) ;
if ( isEarlyValidation )
{
// Skip early validation if any expressions other than "url" (expanded by the runner)
var urlToken = environmentMapping
. Where ( x = > x . Key is StringToken key & & string . Equals ( key . Value , WorkflowTemplateConstants . Url , StringComparison . Ordinal ) )
. Select ( x = > x . Value )
. SingleOrDefault ( ) ;
if ( isEarlyValidation & & environmentMapping . Traverse ( ) . Any ( x = > x is ExpressionToken & & x ! = urlToken ) )
{
return null ;
}
}
var name = default ( String ) ;
var url = default ( TemplateToken ) ;
foreach ( var environmentProp in environmentMapping )
{
var propertyName = environmentProp . Key . AssertString ( $"job {WorkflowTemplateConstants.Environment} key" ) ;
var propertyValue = environmentProp . Value ;
switch ( propertyName . Value )
{
// Name is a required property (schema validation)
case WorkflowTemplateConstants . Name :
name = propertyValue . AssertString ( $"job {WorkflowTemplateConstants.Environment} {WorkflowTemplateConstants.Name}" ) . Value ;
break ;
case WorkflowTemplateConstants . Url :
url = propertyValue ;
break ;
default :
propertyName . AssertUnexpectedValue ( $"job {WorkflowTemplateConstants.Environment} key" ) ; // throws
break ;
}
}
if ( ! String . IsNullOrEmpty ( name ) )
{
return new ActionsEnvironmentReference ( name ) { Url = url } ;
}
else
{
return null ;
}
}
}
internal static Dictionary < String , String > ConvertToStepEnvironment (
TemplateContext context ,
TemplateToken environment ,
StringComparer keyComparer ,
Boolean isEarlyValidation = false )
{
var result = new Dictionary < String , String > ( keyComparer ) ;
// Expression
if ( isEarlyValidation & & environment is ExpressionToken )
{
return result ;
}
// Mapping
var mapping = environment . AssertMapping ( "environment" ) ;
foreach ( var pair in mapping )
{
// Expression key
if ( isEarlyValidation & & pair . Key is ExpressionToken )
{
continue ;
}
// String key
var key = pair . Key . AssertString ( "environment key" ) ;
// Expression value
if ( isEarlyValidation & & pair . Value is ExpressionToken )
{
continue ;
}
// String value
var value = pair . Value . AssertString ( "environment value" ) ;
result [ key . Value ] = value . Value ;
}
return result ;
}
internal static Dictionary < String , String > ConvertToStepInputs (
TemplateContext context ,
TemplateToken inputs ,
Boolean isEarlyValidation = false )
{
var result = new Dictionary < String , String > ( StringComparer . OrdinalIgnoreCase ) ;
// Expression
if ( isEarlyValidation & & inputs is ExpressionToken )
{
return result ;
}
// Mapping
var mapping = inputs . AssertMapping ( "inputs" ) ;
foreach ( var pair in mapping )
{
// Expression key
if ( isEarlyValidation & & pair . Key is ExpressionToken )
{
continue ;
}
// Literal key
var key = pair . Key . AssertString ( "inputs key" ) ;
// Expression value
if ( isEarlyValidation & & pair . Value is ExpressionToken )
{
continue ;
}
// Literal value
var value = pair . Value . AssertString ( "inputs value" ) ;
result [ key . Value ] = value . Value ;
}
return result ;
}
internal static Int32 ? ConvertToStepTimeout (
TemplateContext context ,
TemplateToken token ,
Boolean isEarlyValidation = false )
{
if ( isEarlyValidation & & token is ExpressionToken )
{
return null ;
}
var numberToken = token . AssertNumber ( $"step {WorkflowTemplateConstants.TimeoutMinutes}" ) ;
return ( Int32 ) numberToken . Value ;
}
internal static Strategy ConvertToStrategy (
TemplateContext context ,
TemplateToken token ,
String jobFactoryName ,
Boolean isEarlyValidation = false )
{
var result = new Strategy ( ) ;
// Expression
if ( isEarlyValidation & & token is ExpressionToken )
{
return result ;
}
var strategyMapping = token . AssertMapping ( WorkflowTemplateConstants . Strategy ) ;
var matrixBuilder = default ( MatrixBuilder ) ;
var hasExpressions = false ;
foreach ( var strategyPair in strategyMapping )
{
// Expression key
if ( isEarlyValidation & & strategyPair . Key is ExpressionToken )
{
hasExpressions = true ;
continue ;
}
// Literal key
var strategyKey = strategyPair . Key . AssertString ( "strategy key" ) ;
switch ( strategyKey . Value )
{
// Fail-Fast
case WorkflowTemplateConstants . FailFast :
if ( isEarlyValidation & & strategyPair . Value is ExpressionToken )
{
hasExpressions = true ;
continue ;
}
var failFastBooleanToken = strategyPair . Value . AssertBoolean ( $"strategy {WorkflowTemplateConstants.FailFast}" ) ;
result . FailFast = failFastBooleanToken . Value ;
break ;
// Max-Parallel
case WorkflowTemplateConstants . MaxParallel :
if ( isEarlyValidation & & strategyPair . Value is ExpressionToken )
{
hasExpressions = true ;
continue ;
}
var maxParallelNumberToken = strategyPair . Value . AssertNumber ( $"strategy {WorkflowTemplateConstants.MaxParallel}" ) ;
result . MaxParallel = ( Int32 ) maxParallelNumberToken . Value ;
break ;
// Matrix
case WorkflowTemplateConstants . Matrix :
// Expression
if ( isEarlyValidation & & strategyPair . Value is ExpressionToken )
{
hasExpressions = true ;
continue ;
}
var matrix = strategyPair . Value . AssertMapping ( "matrix" ) ;
hasExpressions = hasExpressions | | matrix . Traverse ( ) . Any ( x = > x is ExpressionToken ) ;
matrixBuilder = new MatrixBuilder ( context , jobFactoryName ) ;
var hasCrossProductVector = false ;
var hasIncludeVector = false ;
foreach ( var matrixPair in matrix )
{
// Expression key
if ( isEarlyValidation & & matrixPair . Key is ExpressionToken )
{
hasCrossProductVector = true ; // For early validation, treat as if a vector is defined
continue ;
}
var matrixKey = matrixPair . Key . AssertString ( "matrix key" ) ;
switch ( matrixKey . Value )
{
case WorkflowTemplateConstants . Include :
if ( isEarlyValidation & & matrixPair . Value . Traverse ( ) . Any ( x = > x is ExpressionToken ) )
{
hasIncludeVector = true ; // For early validation, treat as OK
continue ;
}
var includeSequence = matrixPair . Value . AssertSequence ( "matrix includes" ) ;
hasIncludeVector = includeSequence . Count > 0 ;
matrixBuilder . Include ( includeSequence ) ;
break ;
case WorkflowTemplateConstants . Exclude :
if ( isEarlyValidation & & matrixPair . Value . Traverse ( ) . Any ( x = > x is ExpressionToken ) )
{
continue ;
}
var excludeSequence = matrixPair . Value . AssertSequence ( "matrix excludes" ) ;
matrixBuilder . Exclude ( excludeSequence ) ;
break ;
default :
hasCrossProductVector = true ;
if ( isEarlyValidation & & matrixPair . Value . Traverse ( ) . Any ( x = > x is ExpressionToken ) )
{
continue ;
}
var vectorName = matrixKey . Value ;
var vectorSequence = matrixPair . Value . AssertSequence ( "matrix vector value" ) ;
if ( vectorSequence . Count = = 0 )
{
context . Error ( vectorSequence , $"Matrix vector '{vectorName}' does not contain any values" ) ;
}
else
{
matrixBuilder . AddVector ( vectorName , vectorSequence ) ;
}
break ;
}
}
if ( ! hasCrossProductVector & & ! hasIncludeVector )
{
context . Error ( matrix , $"Matrix must define at least one vector" ) ;
}
break ;
default :
strategyKey . AssertUnexpectedValue ( "strategy key" ) ; // throws
break ;
}
}
if ( hasExpressions )
{
return result ;
}
if ( matrixBuilder ! = null )
{
result . Configurations . AddRange ( matrixBuilder . Build ( ) ) ;
}
for ( var i = 0 ; i < result . Configurations . Count ; i + + )
{
var configuration = result . Configurations [ i ] ;
var strategy = new DictionaryExpressionData ( )
{
{
"fail-fast" ,
new BooleanExpressionData ( result . FailFast )
} ,
{
"job-index" ,
new NumberExpressionData ( i )
} ,
{
"job-total" ,
new NumberExpressionData ( result . Configurations . Count )
}
} ;
if ( result . MaxParallel > 0 )
{
strategy . Add (
"max-parallel" ,
new NumberExpressionData ( result . MaxParallel )
) ;
}
else
{
strategy . Add (
"max-parallel" ,
new NumberExpressionData ( result . Configurations . Count )
) ;
}
configuration . ExpressionData . Add ( WorkflowTemplateConstants . Strategy , strategy ) ;
context . Memory . AddBytes ( WorkflowTemplateConstants . Strategy ) ;
context . Memory . AddBytes ( strategy , traverse : true ) ;
if ( ! configuration . ExpressionData . ContainsKey ( WorkflowTemplateConstants . Matrix ) )
{
configuration . ExpressionData . Add ( WorkflowTemplateConstants . Matrix , null ) ;
context . Memory . AddBytes ( WorkflowTemplateConstants . Matrix ) ;
}
}
return result ;
}
internal static ContainerRegistryCredentials ConvertToContainerCredentials ( TemplateToken token )
{
var credentials = token . AssertMapping ( WorkflowTemplateConstants . Credentials ) ;
var result = new ContainerRegistryCredentials ( ) ;
foreach ( var credentialProperty in credentials )
{
var propertyName = credentialProperty . Key . AssertString ( $"{WorkflowTemplateConstants.Credentials} key" ) ;
switch ( propertyName . Value )
{
case WorkflowTemplateConstants . Username :
result . Username = credentialProperty . Value . AssertString ( WorkflowTemplateConstants . Username ) . Value ;
break ;
case WorkflowTemplateConstants . Password :
result . Password = credentialProperty . Value . AssertString ( WorkflowTemplateConstants . Password ) . Value ;
break ;
default :
propertyName . AssertUnexpectedValue ( $"{WorkflowTemplateConstants.Credentials} key {propertyName}" ) ;
break ;
}
}
return result ;
}
internal static JobContainer ConvertToJobContainer (
TemplateContext context ,
TemplateToken value ,
2026-03-02 23:38:16 -06:00
bool isEarlyValidation = false ,
bool isServiceContainer = false )
2025-11-07 14:18:52 -06:00
{
var result = new JobContainer ( ) ;
if ( isEarlyValidation & & value . Traverse ( ) . Any ( x = > x is ExpressionToken ) )
{
return result ;
}
if ( value is StringToken containerLiteral )
{
2026-03-02 23:38:16 -06:00
// Trim "docker://"
var trimmedImage = containerLiteral . Value ;
var hasDockerPrefix = containerLiteral . Value . StartsWith ( WorkflowTemplateConstants . DockerUriPrefix , StringComparison . Ordinal ) ;
if ( hasDockerPrefix )
{
trimmedImage = trimmedImage . Substring ( WorkflowTemplateConstants . DockerUriPrefix . Length ) ;
}
// Empty shorthand after trimming "docker://" ?
if ( String . IsNullOrEmpty ( trimmedImage ) )
2025-11-07 14:18:52 -06:00
{
2026-03-02 23:38:16 -06:00
// Error at parse-time for:
// 1. container: 'docker://'
// 2. services.foo: ''
// 3. services.foo: 'docker://'
//
// Do not error for:
// 1. container: ''
if ( isEarlyValidation & & ( hasDockerPrefix | | isServiceContainer ) )
{
context . Error ( value , "Container image cannot be empty" ) ;
}
// Short-circuit
2025-11-07 14:18:52 -06:00
return null ;
}
2026-03-02 23:38:16 -06:00
// Store original, trimmed further below
2025-11-07 14:18:52 -06:00
result . Image = containerLiteral . Value ;
}
else
{
var containerMapping = value . AssertMapping ( $"{WorkflowTemplateConstants.Container}" ) ;
foreach ( var containerPropertyPair in containerMapping )
{
var propertyName = containerPropertyPair . Key . AssertString ( $"{WorkflowTemplateConstants.Container} key" ) ;
switch ( propertyName . Value )
{
case WorkflowTemplateConstants . Image :
result . Image = containerPropertyPair . Value . AssertString ( $"{WorkflowTemplateConstants.Container} {propertyName}" ) . Value ;
break ;
case WorkflowTemplateConstants . Env :
var env = containerPropertyPair . Value . AssertMapping ( $"{WorkflowTemplateConstants.Container} {propertyName}" ) ;
var envDict = new Dictionary < String , String > ( env . Count ) ;
foreach ( var envPair in env )
{
var envKey = envPair . Key . ToString ( ) ;
var envValue = envPair . Value . AssertString ( $"{WorkflowTemplateConstants.Container} {propertyName} {envPair.Key.ToString()}" ) . Value ;
envDict . Add ( envKey , envValue ) ;
}
result . Environment = envDict ;
break ;
case WorkflowTemplateConstants . Options :
result . Options = containerPropertyPair . Value . AssertString ( $"{WorkflowTemplateConstants.Container} {propertyName}" ) . Value ;
break ;
case WorkflowTemplateConstants . Ports :
var ports = containerPropertyPair . Value . AssertSequence ( $"{WorkflowTemplateConstants.Container} {propertyName}" ) ;
var portList = new List < String > ( ports . Count ) ;
foreach ( var portItem in ports )
{
var portString = portItem . AssertString ( $"{WorkflowTemplateConstants.Container} {propertyName} {portItem.ToString()}" ) . Value ;
portList . Add ( portString ) ;
}
result . Ports = portList ;
break ;
case WorkflowTemplateConstants . Volumes :
var volumes = containerPropertyPair . Value . AssertSequence ( $"{WorkflowTemplateConstants.Container} {propertyName}" ) ;
var volumeList = new List < String > ( volumes . Count ) ;
foreach ( var volumeItem in volumes )
{
var volumeString = volumeItem . AssertString ( $"{WorkflowTemplateConstants.Container} {propertyName} {volumeItem.ToString()}" ) . Value ;
volumeList . Add ( volumeString ) ;
}
result . Volumes = volumeList ;
break ;
case WorkflowTemplateConstants . Credentials :
result . Credentials = ConvertToContainerCredentials ( containerPropertyPair . Value ) ;
break ;
default :
propertyName . AssertUnexpectedValue ( $"{WorkflowTemplateConstants.Container} key" ) ;
break ;
}
}
}
2026-03-02 23:38:16 -06:00
// Trim "docker://"
var hadDockerPrefix = false ;
if ( ! String . IsNullOrEmpty ( result . Image ) & & result . Image . StartsWith ( WorkflowTemplateConstants . DockerUriPrefix , StringComparison . Ordinal ) )
{
hadDockerPrefix = true ;
result . Image = result . Image . Substring ( WorkflowTemplateConstants . DockerUriPrefix . Length ) ;
}
2025-11-07 14:18:52 -06:00
if ( String . IsNullOrEmpty ( result . Image ) )
{
2026-03-02 23:38:16 -06:00
// Error at parse-time for:
// 1. container: {image: 'docker://'}
// 2. services.foo: {image: ''}
// 3. services.foo: {image: 'docker://'}
//
// Do not error for:
// 1. container: {image: ''}
if ( isEarlyValidation & & ( hadDockerPrefix | | isServiceContainer ) )
2026-02-11 09:44:01 -06:00
{
context . Error ( value , "Container image cannot be empty" ) ;
}
2025-11-07 14:18:52 -06:00
return null ;
}
return result ;
}
internal static List < KeyValuePair < String , JobContainer > > ConvertToJobServiceContainers (
TemplateContext context ,
TemplateToken services ,
bool isEarlyValidation = false )
{
var result = new List < KeyValuePair < String , JobContainer > > ( ) ;
if ( isEarlyValidation & & services . Traverse ( ) . Any ( x = > x is ExpressionToken ) )
{
return result ;
}
var servicesMapping = services . AssertMapping ( "services" ) ;
foreach ( var servicePair in servicesMapping )
{
var networkAlias = servicePair . Key . AssertString ( "services key" ) . Value ;
2026-03-02 23:38:16 -06:00
var container = ConvertToJobContainer ( context , servicePair . Value , isEarlyValidation , isServiceContainer : true ) ;
2025-11-07 14:18:52 -06:00
result . Add ( new KeyValuePair < String , JobContainer > ( networkAlias , container ) ) ;
}
return result ;
}
private static IList < IJob > ConvertToJobs (
TemplateContext context ,
TemplateToken workflow )
{
var result = new List < IJob > ( ) ;
var jobsMapping = workflow . AssertMapping ( WorkflowTemplateConstants . Jobs ) ;
var ready = new Queue < NodeInfo > ( ) ;
var allUnsatisfied = new List < NodeInfo > ( ) ;
var jobCountValidator = context . GetJobCountValidator ( ) ;
foreach ( var jobsPair in jobsMapping )
{
var jobId = jobsPair . Key . AssertString ( $"{WorkflowTemplateConstants.Jobs} key" ) ;
jobCountValidator . Increment ( jobId ) ;
var jobDefinition = jobsPair . Value . AssertMapping ( $"{WorkflowTemplateConstants.Jobs} value" ) ;
var idBuilder = new IdBuilder ( ) ;
var job = ConvertToJob ( context , jobId , jobDefinition , idBuilder ) ;
result . Add ( job ) ;
var nodeInfo = new NodeInfo
{
Name = job . Id ! . Value ,
Needs = new List < StringToken > ( job . Needs ? ? new List < StringToken > ( ) ) ,
} ;
if ( nodeInfo . Needs . Count = = 0 )
{
ready . Enqueue ( nodeInfo ) ;
}
else
{
allUnsatisfied . Add ( nodeInfo ) ;
}
}
if ( context . Errors . Count ! = 0 )
{
return result ;
}
if ( ready . Count = = 0 )
{
context . Error ( jobsMapping , "The workflow must contain at least one job with no dependencies." ) ;
return result ;
}
while ( ready . Count > 0 )
{
var current = ready . Dequeue ( ) ;
// Figure out which nodes would start after current completes
for ( var i = allUnsatisfied . Count - 1 ; i > = 0 ; i - - )
{
var unsatisfied = allUnsatisfied [ i ] ;
for ( var j = unsatisfied . Needs . Count - 1 ; j > = 0 ; j - - )
{
if ( String . Equals ( unsatisfied . Needs [ j ] . Value , current . Name , StringComparison . OrdinalIgnoreCase ) )
{
unsatisfied . Needs . RemoveAt ( j ) ;
if ( unsatisfied . Needs . Count = = 0 )
{
ready . Enqueue ( unsatisfied ) ;
allUnsatisfied . RemoveAt ( i ) ;
}
}
}
}
}
// Check whether some jobs will never execute
if ( allUnsatisfied . Count > 0 )
{
var names = result . ToHashSet ( x = > x . Id ! . Value , StringComparer . OrdinalIgnoreCase ) ;
foreach ( var unsatisfied in allUnsatisfied )
{
foreach ( var need in unsatisfied . Needs )
{
if ( names . Contains ( need . Value ) )
{
context . Error ( need , $"Job '{unsatisfied.Name}' depends on job '{need.Value}' which creates a cycle in the dependency graph." ) ;
}
else
{
context . Error ( need , $"Job '{unsatisfied.Name}' depends on unknown job '{need.Value}'." ) ;
}
}
}
}
return result ;
}
private static IJob ConvertToJob (
TemplateContext context ,
StringToken jobId ,
MappingToken jobDefinition ,
IdBuilder idBuilder )
{
if ( ! idBuilder . TryAddKnownId ( jobId . Value , out var error ) )
{
context . Error ( jobId , error ) ;
}
var condition = new BasicExpressionToken ( null , null , null , $"{WorkflowTemplateConstants.Success}()" ) ;
var continueOnError = default ( ScalarToken ) ;
var env = default ( TemplateToken ) ;
var name = default ( ScalarToken ) ;
var jobTarget = default ( TemplateToken ) ;
var steps = new List < IStep > ( ) ;
var strategy = default ( TemplateToken ) ;
var jobContainer = default ( TemplateToken ) ;
var jobServiceContainers = default ( TemplateToken ) ;
var concurrency = default ( TemplateToken ) ;
var actionsEnvironment = default ( TemplateToken ) ;
var defaults = default ( TemplateToken ) ;
var permissions = default ( Permissions ) ;
var outputs = default ( TemplateToken ) ;
var jobTimeout = default ( ScalarToken ) ;
var jobCancelTimeout = default ( ScalarToken ) ;
var snapshot = default ( TemplateToken ) ;
var needs = new List < StringToken > ( ) ;
var workflowJobRef = default ( StringToken ) ;
var workflowJobInputs = default ( MappingToken ) ;
var workflowJobSecrets = default ( MappingToken ) ;
var workflowJobSecretsInherited = false ;
foreach ( var jobProperty in jobDefinition )
{
var propertyName = jobProperty . Key . AssertString ( $"job property name" ) ;
switch ( propertyName . Value )
{
case WorkflowTemplateConstants . ContinueOnError :
ConvertToJobContinueOnError ( context , jobProperty . Value , isEarlyValidation : true ) ; // Validate early if possible
continueOnError = jobProperty . Value . AssertScalar ( $"job {WorkflowTemplateConstants.ContinueOnError}" ) ;
break ;
case WorkflowTemplateConstants . If :
condition = ConvertToIfCondition ( context , jobProperty . Value , IfKind . Job ) ;
break ;
case WorkflowTemplateConstants . Name :
name = jobProperty . Value . AssertScalar ( $"job {WorkflowTemplateConstants.Name}" ) ;
ConvertToJobName ( context , name , isEarlyValidation : true ) ; // Validate early if possible
break ;
case WorkflowTemplateConstants . Needs :
if ( jobProperty . Value is StringToken needsLiteral )
{
needs . Add ( needsLiteral ) ;
}
else
{
var needsSeq = jobProperty . Value . AssertSequence ( $"job {WorkflowTemplateConstants.Needs}" ) ;
foreach ( var needsItem in needsSeq )
{
var need = needsItem . AssertString ( $"job {WorkflowTemplateConstants.Needs} item" ) ;
needs . Add ( need ) ;
}
}
break ;
case WorkflowTemplateConstants . RunsOn :
ConvertToRunsOn ( context , jobProperty . Value , isEarlyValidation : true ) ; // Validate early if possible
jobTarget = jobProperty . Value ;
break ;
case WorkflowTemplateConstants . Snapshot :
if ( ! context . GetFeatures ( ) . Snapshot )
{
context . Error ( jobProperty . Key , $"The key '{WorkflowTemplateConstants.Snapshot}' is not allowed" ) ;
break ;
}
ConvertToSnapshot ( context , jobProperty . Value , isEarlyValidation : true ) ; // Validate early if possible
snapshot = jobProperty . Value ;
break ;
case WorkflowTemplateConstants . Steps :
steps . AddRange ( ConvertToSteps ( context , jobProperty . Value ) ) ;
break ;
case WorkflowTemplateConstants . Strategy :
ConvertToStrategy ( context , jobProperty . Value , null , isEarlyValidation : true ) ; // Validate early if possible
strategy = jobProperty . Value ;
break ;
case WorkflowTemplateConstants . TimeoutMinutes :
ConvertToJobTimeout ( context , jobProperty . Value , isEarlyValidation : true ) ; // Validate early if possible
jobTimeout = jobProperty . Value as ScalarToken ;
break ;
case WorkflowTemplateConstants . CancelTimeoutMinutes :
ConvertToJobCancelTimeout ( context , jobProperty . Value , isEarlyValidation : true ) ; // Validate early if possible
jobCancelTimeout = jobProperty . Value as ScalarToken ;
break ;
case WorkflowTemplateConstants . Container :
ConvertToJobContainer ( context , jobProperty . Value , isEarlyValidation : true ) ; // Validate early if possible
jobContainer = jobProperty . Value ;
break ;
case WorkflowTemplateConstants . Services :
ConvertToJobServiceContainers ( context , jobProperty . Value , isEarlyValidation : true ) ; // Validate early if possible
jobServiceContainers = jobProperty . Value ;
break ;
case WorkflowTemplateConstants . Concurrency :
ConvertToConcurrency ( context , jobProperty . Value , isEarlyValidation : true ) ; // Validate early if possible
concurrency = jobProperty . Value ;
break ;
case WorkflowTemplateConstants . Env :
env = jobProperty . Value ;
break ;
case WorkflowTemplateConstants . Environment :
ConvertToActionEnvironmentReference ( context , jobProperty . Value , isEarlyValidation : true ) ; // Validate early if possible
actionsEnvironment = jobProperty . Value ;
break ;
case WorkflowTemplateConstants . Outputs :
outputs = jobProperty . Value ;
break ;
case WorkflowTemplateConstants . Defaults :
defaults = jobProperty . Value ;
break ;
case WorkflowTemplateConstants . Permissions :
permissions = ConvertToPermissions ( context , jobProperty . Value ) ;
break ;
case WorkflowTemplateConstants . Uses :
workflowJobRef = jobProperty . Value . AssertString ( $"job {WorkflowTemplateConstants.Uses} value" ) ;
break ;
case WorkflowTemplateConstants . With :
workflowJobInputs = jobProperty . Value . AssertMapping ( $"{WorkflowTemplateConstants.Uses}-{WorkflowTemplateConstants.With} value" ) ;
break ;
case WorkflowTemplateConstants . Secrets :
// String in case inherit is used
if ( jobProperty . Value is StringToken sToken
& & sToken . Value = = WorkflowTemplateConstants . Inherit )
{
workflowJobSecretsInherited = true ;
}
else
{
workflowJobSecrets = jobProperty . Value . AssertMapping ( $"{WorkflowTemplateConstants.Uses}-{WorkflowTemplateConstants.Secrets} value" ) ;
}
break ;
default :
propertyName . AssertUnexpectedValue ( "job key" ) ; // throws
break ;
}
}
if ( workflowJobRef ! = null )
{
var workflowJob = new ReusableWorkflowJob
{
Id = jobId ,
Name = name ,
If = condition ,
Ref = workflowJobRef ,
InputValues = workflowJobInputs ,
SecretValues = workflowJobSecrets ,
InheritSecrets = workflowJobSecretsInherited ,
Permissions = permissions ,
Concurrency = concurrency ,
Strategy = strategy ,
} ;
if ( workflowJob . Name is null | | workflowJob . Name is StringToken nameStr & & String . IsNullOrEmpty ( nameStr . Value ) )
{
workflowJob . Name = workflowJob . Id ;
}
workflowJob . Needs . AddRange ( needs ) ;
return workflowJob ;
}
else
{
var result = new Job
{
Id = jobId ,
Name = name ,
ContinueOnError = continueOnError ,
If = condition ,
RunsOn = jobTarget ,
Strategy = strategy ,
TimeoutMinutes = jobTimeout ,
CancelTimeoutMinutes = jobCancelTimeout ,
Container = jobContainer ,
Services = jobServiceContainers ,
Concurrency = concurrency ,
Env = env ,
Environment = actionsEnvironment ,
Outputs = outputs ,
Defaults = defaults ,
Permissions = permissions ,
Snapshot = snapshot ,
} ;
if ( result . Name is null | | result . Name is StringToken nameStr & & String . IsNullOrEmpty ( nameStr . Value ) )
{
result . Name = result . Id ;
}
result . Needs . AddRange ( needs ) ;
result . Steps . AddRange ( steps ) ;
return result ;
}
}
internal static IDictionary < String , String > ConvertToWorkflowJobSecrets (
TemplateContext context ,
TemplateToken secrets )
{
var result = new Dictionary < String , String > ( StringComparer . OrdinalIgnoreCase ) ;
var mapping = secrets . AssertMapping ( "workflow job secrets" ) ;
foreach ( var pair in mapping )
{
var key = pair . Key . AssertString ( "workflow job secret key" ) . Value ;
var value = pair . Value . AssertString ( "workflow job secret value" ) . Value ;
if ( ! String . IsNullOrEmpty ( value ) )
{
result [ key ] = value ;
}
}
return result ;
}
// Public because used by runner for composite actions
public static List < IStep > ConvertToSteps (
TemplateContext context ,
TemplateToken steps )
{
var stepsSequence = steps . AssertSequence ( $"job {WorkflowTemplateConstants.Steps}" ) ;
var idBuilder = new IdBuilder ( ) ;
var result = stepsSequence . Select ( x = > ConvertToStep ( context , x , idBuilder ) ) . ToList ( ) ;
// Generate default IDs when empty
foreach ( IStep step in result )
{
if ( ! string . IsNullOrEmpty ( step . Id ) )
{
continue ;
}
var id = default ( string ) ;
if ( step is ActionStep action )
{
if ( action . Uses ! . Value . StartsWith ( WorkflowTemplateConstants . DockerUriPrefix , StringComparison . Ordinal ) )
{
id = action . Uses ! . Value . Substring ( WorkflowTemplateConstants . DockerUriPrefix . Length ) ;
}
else if ( action . Uses ! . Value . StartsWith ( "./" ) | | action . Uses ! . Value . StartsWith ( ".\\" ) )
{
id = WorkflowConstants . SelfAlias ;
}
else
{
var usesSegments = action . Uses ! . Value . Split ( '@' ) ;
var pathSegments = usesSegments [ 0 ] . Split ( new [ ] { '/' , '\\' } , StringSplitOptions . RemoveEmptyEntries ) ;
var gitRef = usesSegments . Length = = 2 ? usesSegments [ 1 ] : String . Empty ;
if ( usesSegments . Length = = 2 & &
pathSegments . Length > = 2 & &
! string . IsNullOrEmpty ( pathSegments [ 0 ] ) & &
! string . IsNullOrEmpty ( pathSegments [ 1 ] ) & &
! string . IsNullOrEmpty ( gitRef ) )
{
id = $"{pathSegments[0]}/{pathSegments[1]}" ;
}
}
}
if ( string . IsNullOrEmpty ( id ) )
{
id = "run" ;
}
idBuilder . AppendSegment ( $"__{id}" ) ;
// Allow reserved prefix "__" for default IDs
step . Id = idBuilder . Build ( allowReservedPrefix : true ) ;
}
return result ;
}
private static IStep ConvertToStep (
TemplateContext context ,
TemplateToken stepsItem ,
IdBuilder idBuilder )
{
var step = stepsItem . AssertMapping ( $"{WorkflowTemplateConstants.Steps} item" ) ;
var continueOnError = default ( ScalarToken ) ;
var env = default ( TemplateToken ) ;
var id = default ( StringToken ) ;
var ifCondition = default ( BasicExpressionToken ) ;
var ifToken = default ( ScalarToken ) ;
var name = default ( ScalarToken ) ;
var run = default ( ScalarToken ) ;
var timeoutMinutes = default ( ScalarToken ) ;
var uses = default ( StringToken ) ;
var with = default ( TemplateToken ) ;
var workingDir = default ( ScalarToken ) ;
var shell = default ( ScalarToken ) ;
foreach ( var stepProperty in step )
{
var propertyName = stepProperty . Key . AssertString ( $"{WorkflowTemplateConstants.Steps} item key" ) ;
switch ( propertyName . Value )
{
case WorkflowTemplateConstants . ContinueOnError :
ConvertToStepContinueOnError ( context , stepProperty . Value , isEarlyValidation : true ) ; // Validate early if possible
continueOnError = stepProperty . Value . AssertScalar ( $"{WorkflowTemplateConstants.Steps} {WorkflowTemplateConstants.ContinueOnError}" ) ;
break ;
case WorkflowTemplateConstants . Env :
ConvertToStepEnvironment ( context , stepProperty . Value , StringComparer . Ordinal , isEarlyValidation : true ) ; // Validate early if possible
env = stepProperty . Value ;
break ;
case WorkflowTemplateConstants . Id :
id = stepProperty . Value . AssertString ( $"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.Id}" ) ;
if ( ! String . IsNullOrEmpty ( id . Value ) & &
! idBuilder . TryAddKnownId ( id . Value , out var error ) )
{
context . Error ( id , error ) ;
}
break ;
case WorkflowTemplateConstants . If :
ifToken = stepProperty . Value . AssertScalar ( $"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.If}" ) ;
break ;
case WorkflowTemplateConstants . Name :
name = stepProperty . Value . AssertScalar ( $"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.Name}" ) ;
break ;
case WorkflowTemplateConstants . Run :
run = stepProperty . Value . AssertScalar ( $"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.Run}" ) ;
break ;
case WorkflowTemplateConstants . Shell :
shell = stepProperty . Value . AssertScalar ( $"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.Shell}" ) ;
break ;
case WorkflowTemplateConstants . TimeoutMinutes :
ConvertToStepTimeout ( context , stepProperty . Value , isEarlyValidation : true ) ; // Validate early if possible
timeoutMinutes = stepProperty . Value . AssertScalar ( $"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.TimeoutMinutes}" ) ;
break ;
case WorkflowTemplateConstants . Uses :
uses = stepProperty . Value . AssertString ( $"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.Uses}" ) ;
break ;
case WorkflowTemplateConstants . With :
ConvertToStepInputs ( context , stepProperty . Value , isEarlyValidation : true ) ; // Validate early if possible
with = stepProperty . Value ;
break ;
case WorkflowTemplateConstants . WorkingDirectory :
workingDir = stepProperty . Value . AssertScalar ( $"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.WorkingDirectory}" ) ;
break ;
default :
propertyName . AssertUnexpectedValue ( $"{WorkflowTemplateConstants.Steps} item key" ) ; // throws
break ;
}
}
// Fixup the if-condition
ifCondition = ConvertToIfCondition ( context , ifToken , IfKind . Step ) ;
if ( run ! = null )
{
return new RunStep
{
Id = id ? . Value ,
ContinueOnError = continueOnError ,
Name = name ,
If = ifCondition ,
TimeoutMinutes = timeoutMinutes ,
Env = env ,
WorkingDirectory = workingDir ,
Shell = shell ,
Run = run ,
} ;
}
else
{
uses . AssertString ( $"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.Uses}" ) ;
var result = new ActionStep
{
Id = id ? . Value ,
ContinueOnError = continueOnError ,
Name = name ,
If = ifCondition ,
TimeoutMinutes = timeoutMinutes ,
Env = env ,
Uses = uses ,
With = with ,
} ;
if ( ! uses . Value . StartsWith ( WorkflowTemplateConstants . DockerUriPrefix , StringComparison . Ordinal ) & &
! uses . Value . StartsWith ( "./" ) & &
! uses . Value . StartsWith ( ".\\" ) )
{
var usesSegments = uses . Value . Split ( '@' ) ;
var pathSegments = usesSegments [ 0 ] . Split ( new [ ] { '/' , '\\' } , StringSplitOptions . RemoveEmptyEntries ) ;
var gitRef = usesSegments . Length = = 2 ? usesSegments [ 1 ] : String . Empty ;
if ( usesSegments . Length ! = 2 | |
pathSegments . Length < 2 | |
String . IsNullOrEmpty ( pathSegments [ 0 ] ) | |
String . IsNullOrEmpty ( pathSegments [ 1 ] ) | |
String . IsNullOrEmpty ( gitRef ) )
{
context . Error ( uses , $"Expected format {{org}}/{{repo}}[/path]@ref. Actual '{uses.Value}'" ) ;
}
}
return result ;
}
}
/// <summary>
/// When empty, default to "success()".
/// When a status function is not referenced, format as "success() && <CONDITION>".
/// </summary>
private static BasicExpressionToken ConvertToIfCondition (
TemplateContext context ,
TemplateToken token ,
IfKind ifKind )
{
String condition ;
if ( token is null )
{
condition = null ;
}
else if ( token is BasicExpressionToken expressionToken )
{
condition = expressionToken . Expression ;
}
else
{
var stringToken = token . AssertString ( $"{ifKind} {WorkflowTemplateConstants.If}" ) ;
condition = stringToken . Value ;
}
if ( String . IsNullOrWhiteSpace ( condition ) )
{
return new BasicExpressionToken ( token ? . FileId , token ? . Line , token ? . Column , $"{WorkflowTemplateConstants.Success}()" ) ;
}
var expressionParser = new ExpressionParser ( ) ;
var functions = default ( IFunctionInfo [ ] ) ;
var namedValues = default ( INamedValueInfo [ ] ) ;
switch ( ifKind )
{
case IfKind . Job :
namedValues = s_jobIfNamedValues ;
functions = s_jobConditionFunctions ;
break ;
case IfKind . Step :
namedValues = s_stepNamedValues ;
functions = s_stepConditionFunctions ;
break ;
case IfKind . Snapshot :
namedValues = s_snapshotIfNamedValues ;
functions = s_snapshotConditionFunctions ;
break ;
default :
throw new ArgumentException ( $"Unexpected IfKind Enum value '{ifKind}' encountered while translating the token '{token}' to an IfCondition." ) ;
}
var node = default ( ExpressionNode ) ;
try
{
2025-12-17 10:57:05 -05:00
node = expressionParser . CreateTree ( condition , null , namedValues , functions , allowCaseFunction : context . AllowCaseFunction ) as ExpressionNode ;
2025-11-07 14:18:52 -06:00
}
catch ( Exception ex )
{
context . Error ( token , ex ) ;
return null ;
}
if ( node = = null )
{
return new BasicExpressionToken ( token ? . FileId , token ? . Line , token ? . Column , $"{WorkflowTemplateConstants.Success}()" ) ;
}
var hasStatusFunction = node . Traverse ( ) . Any ( x = >
{
if ( x is Function function )
{
return String . Equals ( function . Name , WorkflowTemplateConstants . Always , StringComparison . OrdinalIgnoreCase ) | |
String . Equals ( function . Name , WorkflowTemplateConstants . Cancelled , StringComparison . OrdinalIgnoreCase ) | |
String . Equals ( function . Name , WorkflowTemplateConstants . Failure , StringComparison . OrdinalIgnoreCase ) | |
String . Equals ( function . Name , WorkflowTemplateConstants . Success , StringComparison . OrdinalIgnoreCase ) ;
}
return false ;
} ) ;
var finalCondition = hasStatusFunction ? condition : $"{WorkflowTemplateConstants.Success}() && ({condition})" ;
return new BasicExpressionToken ( token ? . FileId , token ? . Line , token ? . Column , finalCondition ) ;
}
private static Permissions ConvertToPermissions ( TemplateContext context , TemplateToken token )
{
if ( token is StringToken )
{
var permissionLevel = PermissionLevel . NoAccess ;
var permissionsStr = token . AssertString ( "permissions" ) ;
switch ( permissionsStr . Value )
{
case "read-all" :
permissionLevel = PermissionLevel . Read ;
break ;
case "write-all" :
permissionLevel = PermissionLevel . Write ;
break ;
default :
permissionsStr . AssertUnexpectedValue ( permissionsStr . Value ) ;
break ;
}
return new Permissions ( permissionLevel , includeIdToken : true , includeAttestations : true , includeModels : context . GetFeatures ( ) . AllowModelsPermission ) ;
}
var mapping = token . AssertMapping ( "permissions" ) ;
var permissions = new Permissions ( ) ;
foreach ( var pair in mapping )
{
var key = pair . Key . AssertString ( "permissions.key" ) ;
var permissionLevel = ConvertToPermissionLevel ( context , pair . Value ) ;
switch ( key . Value )
{
case "actions" :
permissions . Actions = permissionLevel ;
break ;
2026-02-11 09:44:01 -06:00
case "artifact-metadata" :
permissions . ArtifactMetadata = permissionLevel ;
break ;
2025-11-07 14:18:52 -06:00
case "attestations" :
permissions . Attestations = permissionLevel ;
break ;
case "checks" :
permissions . Checks = permissionLevel ;
break ;
case "contents" :
permissions . Contents = permissionLevel ;
break ;
case "deployments" :
permissions . Deployments = permissionLevel ;
break ;
case "issues" :
permissions . Issues = permissionLevel ;
break ;
case "discussions" :
permissions . Discussions = permissionLevel ;
break ;
case "packages" :
permissions . Packages = permissionLevel ;
break ;
case "pages" :
permissions . Pages = permissionLevel ;
break ;
case "pull-requests" :
permissions . PullRequests = permissionLevel ;
break ;
case "repository-projects" :
permissions . RepositoryProjects = permissionLevel ;
break ;
case "statuses" :
permissions . Statuses = permissionLevel ;
break ;
case "security-events" :
permissions . SecurityEvents = permissionLevel ;
break ;
case "id-token" :
if ( context . GetFeatures ( ) . IdToken )
{
permissions . IdToken = permissionLevel ;
}
else
{
context . Error ( key , $"The key 'id-token' is not allowed" ) ;
}
break ;
case "models" :
if ( context . GetFeatures ( ) . AllowModelsPermission )
{
if ( permissionLevel = = PermissionLevel . Write )
{
permissions . Models = PermissionLevel . Read ;
}
else
{
permissions . Models = permissionLevel ;
}
}
else
{
context . Error ( key , $"The permission 'models' is not allowed" ) ;
}
break ;
default :
break ;
}
}
return permissions ;
}
private static PermissionLevel ConvertToPermissionLevel (
TemplateContext context ,
TemplateToken token )
{
var level = token . AssertString ( "permissions.value" ) ;
switch ( level . Value )
{
case "none" :
return PermissionLevel . NoAccess ;
case "read" :
return PermissionLevel . Read ;
case "write" :
return PermissionLevel . Write ;
default :
level . AssertUnexpectedValue ( level . Value ) ;
return PermissionLevel . NoAccess ;
}
}
private static void ValidateWorkflowJobTrigger (
TemplateContext context ,
ReusableWorkflowJob workflowJob )
{
ConvertToWorkflowJobInputs ( context , workflowJob . InputDefinitions , workflowJob . InputValues , workflowJob , isEarlyValidation : true ) ;
ValidateWorkflowJobSecrets ( context , workflowJob . SecretDefinitions , workflowJob . SecretValues , workflowJob ) ;
}
private static ExpressionData ConvertToInputValueDefinedType (
TemplateContext context ,
string key ,
StringToken definedType ,
TemplateToken token ,
Boolean isEarlyValidation = false )
{
var inputType = default ( string ) ;
switch ( definedType . Value )
{
case WorkflowTemplateConstants . TypeBoolean :
inputType = WorkflowTemplateConstants . BooleanNeedsContext ;
break ;
case WorkflowTemplateConstants . TypeNumber :
inputType = WorkflowTemplateConstants . NumberNeedsContext ;
break ;
case WorkflowTemplateConstants . TypeString :
inputType = WorkflowTemplateConstants . StringNeedsContext ;
break ;
default :
// The schema for worflow_call.inputs only allows boolean, string, or number.
// We should have failed earlier if we receive any other type.
throw new ArgumentException ( $"Unexpected defined type '{definedType.Value}' when converting input value for '{key}'" ) ;
}
// Leverage the templating library to coerce or error
//
// During early validation, we're not actually evaluating any expressions with this call.
// Any allowed contexts (i.e. "github"/"inputs") have not been added yet, so the TemplateEvaluator
// will not unravel any expressions.
//
// During runtime, the expressions have already been expanded.
var result = TemplateEvaluator . Evaluate (
context ,
inputType ,
token ,
context . Memory . CalculateBytes ( token ) , // Remove the size of the template token that is being replaced
token . FileId
) ;
if ( isEarlyValidation & & token . Traverse ( ) . Any ( x = > x is ExpressionToken ) )
{
return null ;
}
return result . ToExpressionData ( ) ;
}
internal static IDictionary < String , String > ConvertToWorkflowJobOutputs ( TemplateToken workflowJobOutputDefinitions )
{
var result = new Dictionary < String , String > ( StringComparer . OrdinalIgnoreCase ) ;
var outputs = workflowJobOutputDefinitions
. AssertMapping ( "workflow job output definitions" )
. ToDictionary (
x = > x . Key . AssertString ( "outputs key" ) . Value ,
x = > x . Value ,
StringComparer . OrdinalIgnoreCase
) ;
foreach ( var definition in outputs )
{
var spec = definition . Value . AssertMapping ( "workfow job output spec" ) . ToDictionary (
x = > x . Key . AssertString ( "outputs spec key" ) . Value ,
x = > x . Value ,
StringComparer . OrdinalIgnoreCase
) ;
var value = spec [ "value" ] . AssertString ( "workfow job output value" ) . Value ;
result . Add ( definition . Key , value ) ;
}
return result ;
}
internal static DictionaryExpressionData ConvertToWorkflowJobInputs (
TemplateContext context ,
TemplateToken workflowJobInputDefinitions ,
TemplateToken workflowJobInputValues ,
ReusableWorkflowJob workflowJob ,
Boolean isEarlyValidation = false )
{
var result = default ( DictionaryExpressionData ) ;
var inputDefinitions = workflowJobInputDefinitions ?
. AssertMapping ( "workflow job input definitions" )
. ToDictionary (
x = > x . Key . AssertString ( "inputs key" ) . Value ,
x = > x . Value ,
StringComparer . OrdinalIgnoreCase
) ;
var inputValues = workflowJobInputValues ?
. AssertMapping ( "workflow job input values" )
. ToDictionary (
x = > x . Key . AssertString ( "with key" ) . Value ,
x = > x . Value ,
StringComparer . OrdinalIgnoreCase
) ;
if ( inputDefinitions ! = null )
{
result = new DictionaryExpressionData ( ) ;
foreach ( var definedItem in inputDefinitions )
{
string definedKey = definedItem . Key ;
var definedInputSpec = definedItem . Value . AssertMapping ( $"input {definedKey}" ) . ToDictionary (
x = > x . Key . AssertString ( $"input {definedKey} key" ) . Value ,
x = > x . Value ,
StringComparer . OrdinalIgnoreCase ) ;
var inputSpecType = definedInputSpec [ WorkflowTemplateConstants . Type ] . AssertString ( $"inputs {definedKey} type" ) ; // must exist, per schema
// if default provided, check with the defined type
if ( definedInputSpec . TryGetValue ( WorkflowTemplateConstants . Default , out TemplateToken defaultValue ) )
{
var value = ConvertToInputValueDefinedType ( context , definedKey , inputSpecType , defaultValue , isEarlyValidation ) ;
if ( ! isEarlyValidation )
{
result . Add ( definedKey , value ) ;
}
}
else if ( ! isEarlyValidation )
{
result . Add ( definedKey , GetDefaultValueByType ( inputSpecType ) . ToExpressionData ( ) ) ;
}
// if input provided, check with defined type and continue
if ( inputValues ! = null & & inputValues . TryGetValue ( definedKey , out TemplateToken inputValue ) )
{
var value = ConvertToInputValueDefinedType ( context , definedKey , inputSpecType , inputValue , isEarlyValidation ) ;
if ( ! isEarlyValidation )
{
result [ definedKey ] = value ;
}
continue ;
}
// if input required but not provided, error out
if ( isEarlyValidation
& & definedInputSpec . TryGetValue ( WorkflowTemplateConstants . Required , out TemplateToken requiredToken )
& & requiredToken . AssertBoolean ( WorkflowTemplateConstants . Required ) . Value )
{
context . Error ( workflowJob . Ref , $"Input {definedKey} is required, but not provided while calling." ) ;
continue ;
}
}
}
// Validating if any undefined inputs are provided
ValidateUndefinedParameters ( context , inputDefinitions , inputValues , "input" ) ;
return result ;
}
private static void ValidateWorkflowJobSecrets (
TemplateContext context ,
MappingToken workflowJobSecretDefinitions ,
MappingToken workflowJobSecretValues ,
ReusableWorkflowJob workflowJob )
{
// if the secrets are inherited from the caller, we do not have any workflowJob.SecretValues (i.e. explicit mapping)
// Inherited org/repo/env secrets will be stored in context variables and will be validated there
if ( workflowJob . InheritSecrets )
{
return ;
}
var secretDefinitions = workflowJobSecretDefinitions ? . ToDictionary (
x = > x . Key . AssertString ( "secrets key" ) . Value ,
x = > x . Value , StringComparer . OrdinalIgnoreCase ) ;
var secretValues = workflowJobSecretValues ? . ToDictionary (
x = > x . Key . AssertString ( "secrets key" ) . Value ,
x = > x . Value , StringComparer . OrdinalIgnoreCase ) ;
if ( secretDefinitions ! = null )
{
foreach ( var definedItem in secretDefinitions )
{
if ( definedItem . Value is NullToken nullToken )
{
continue ;
}
var definedKey = definedItem . Key . ToString ( ) ;
var definedSecretSpec = definedItem . Value . AssertMapping ( $"secret {definedKey}" ) . ToDictionary (
x = > x . Key . AssertString ( $"secret {definedKey} key" ) . Value ,
x = > x . Value , StringComparer . OrdinalIgnoreCase ) ;
// if secret provided, continue
if ( secretValues ! = null & & secretValues . TryGetValue ( definedKey , out TemplateToken secretValue ) )
{
continue ;
}
// if secret required but not provided, error out
if ( definedSecretSpec . TryGetValue ( WorkflowTemplateConstants . Required , out TemplateToken requiredToken )
& & requiredToken . AssertBoolean ( WorkflowTemplateConstants . Required ) . Value )
{
context . Error ( workflowJob . Ref , $"Secret {definedKey} is required, but not provided while calling." ) ;
}
}
}
// Validating if any undefined secrets are provided
ValidateUndefinedParameters ( context , secretDefinitions , secretValues , WorkflowTemplateConstants . Secret ) ;
}
private static void ValidateUndefinedParameters (
TemplateContext context ,
Dictionary < string , TemplateToken > definitions ,
Dictionary < string , TemplateToken > providedValues ,
string parameterType )
{
if ( providedValues ! = null )
{
foreach ( var providedValue in providedValues )
{
var providedKey = providedValue . Key ;
if ( definitions = = null | | ! definitions . TryGetValue ( providedKey , out TemplateToken value ) )
{
context . Error ( providedValue . Value , $"Invalid {parameterType}, {providedKey} is not defined in the referenced workflow." ) ;
}
}
}
}
private static TemplateToken GetDefaultValueByType ( StringToken type )
{
return type . Value switch
{
WorkflowTemplateConstants . TypeString = > new StringToken ( type . FileId , type . Line , type . Column , string . Empty ) ,
WorkflowTemplateConstants . TypeBoolean = > new BooleanToken ( type . FileId , type . Line , type . Column , false ) ,
WorkflowTemplateConstants . TypeNumber = > new NumberToken ( type . FileId , type . Line , type . Column , 0.0 ) ,
_ = > null ,
} ;
}
private sealed class NodeInfo
{
public String Name { get ; set ; }
public List < StringToken > Needs { get ; set ; }
}
private enum IfKind
{
Job = 0 ,
Step = 1 ,
Snapshot = 2
}
private static readonly INamedValueInfo [ ] s_jobIfNamedValues = new INamedValueInfo [ ]
{
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . GitHub ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Vars ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Inputs ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Needs ) ,
} ;
private static readonly INamedValueInfo [ ] s_stepNamedValues = new INamedValueInfo [ ]
{
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . GitHub ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Vars ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Inputs ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Needs ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Strategy ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Matrix ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Steps ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Job ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Runner ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Env ) ,
} ;
private static readonly INamedValueInfo [ ] s_snapshotIfNamedValues = new INamedValueInfo [ ]
{
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . GitHub ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Vars ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Inputs ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Needs ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Strategy ) ,
new NamedValueInfo < NoOperationNamedValue > ( WorkflowTemplateConstants . Matrix ) ,
} ;
private static readonly IFunctionInfo [ ] s_jobConditionFunctions = new IFunctionInfo [ ]
{
new FunctionInfo < NoOperation > ( WorkflowTemplateConstants . Always , 0 , 0 ) ,
new FunctionInfo < NoOperation > ( WorkflowTemplateConstants . Failure , 0 , Int32 . MaxValue ) ,
new FunctionInfo < NoOperation > ( WorkflowTemplateConstants . Cancelled , 0 , 0 ) ,
new FunctionInfo < NoOperation > ( WorkflowTemplateConstants . Success , 0 , Int32 . MaxValue ) ,
} ;
private static readonly IFunctionInfo [ ] s_stepConditionFunctions = new IFunctionInfo [ ]
{
new FunctionInfo < NoOperation > ( WorkflowTemplateConstants . Always , 0 , 0 ) ,
new FunctionInfo < NoOperation > ( WorkflowTemplateConstants . Cancelled , 0 , 0 ) ,
new FunctionInfo < NoOperation > ( WorkflowTemplateConstants . Failure , 0 , 0 ) ,
new FunctionInfo < NoOperation > ( WorkflowTemplateConstants . Success , 0 , 0 ) ,
new FunctionInfo < NoOperation > ( WorkflowTemplateConstants . HashFiles , 1 , Byte . MaxValue ) ,
} ;
private static readonly IFunctionInfo [ ] s_snapshotConditionFunctions = null ;
}
}