diff --git a/tests/context_projection.rs b/tests/context_projection.rs --- a/tests/context_projection.rs +++ b/tests/context_projection.rs @@ -1,171 +1,199 @@ -#![allow(clippy::panic, clippy::unwrap_used)] - -use std::str::FromStr; -use std::sync::Arc; - -use syncode_runner::control::{ - ActionRepository, ControlVendor, DependencyResult, JobAssignment, JobDefinition, JobId, - JobResult, Needs, Outputs, Secrets, Variables, WorkflowDialect, WorkflowSource, -}; -use syncode_runner::execution::{ - Architecture, ContainerPort, ExecutionPlatform, OperatingSystem, ProviderId, ProviderMetadata, - RuntimeServiceName, SandboxMetadata, ServiceMetadata, ServiceName, ServicePort, -}; -use syncode_runner::github_actions::context::{apply_sandbox_metadata, evaluation_context}; -use syncode_runner::github_actions::expression::{EvaluationStatus, ExpressionProgram, evaluate}; -use syncode_runner::workflow::{ - DynamicObject, JobKey, OutputName, PropertyName, Value, WorkflowCompiler, -}; -use syncode_workflow_github_actions::compiler::GithubActionsCompiler; - -#[test] -fn projects_typed_assignment_plan_and_sandbox_contexts() { - let assignment = assignment(); - let Some(JobDefinition::Workflow(source)) = assignment.definition.as_ref() else { - panic!("this projection is driven from workflow source"); - }; - let hir = GithubActionsCompiler - .compile(source) - .unwrap_or_else(|error| panic!("compile: {error}")); - let mut plans = - syncode_runner::workflow::plans(hir).unwrap_or_else(|error| panic!("plan: {error}")); - let plan = plans.remove(0); - let mut context = - evaluation_context(&assignment, ControlVendor::SynCode, &provider(), plan.job()) - .unwrap_or_else(|error| panic!("context: {error}")); - - assert_condition("inputs.target == 'prod'", &context); - assert_condition("syncode.event.inputs.target == 'prod'", &context); - assert_condition("job.status == 'success'", &context); - assert_condition( - "strategy.fail-fast == false && strategy.max-parallel == 2", - &context, - ); - assert!(matches!( - evaluate("steps", &context), - Ok(Value::Object(values)) if values.is_empty() - )); - assert_condition("syncode.actor == github.actor", &context); - assert_condition("gitea.actor == github.actor", &context); - assert_condition("forgejo.actor == github.actor", &context); - - apply_sandbox_metadata(&mut context, &sandbox_metadata()) - .unwrap_or_else(|error| panic!("sandbox context: {error}")); - assert_condition("job.services.web.id == 'container-1'", &context); - assert_condition("job.services.web.network == 'job-network'", &context); - assert_condition("job.services.web.ports[80] == 49152", &context); - context.set_status(EvaluationStatus::Cancelled); - assert_condition("job.status == 'cancelled'", &context); - assert_condition("job.services.web.id == 'container-1'", &context); -} - -#[test] -fn failed_needs_drive_status_checks_and_outputs() { - let mut assignment = assignment(); - assignment.needs = [( - "build".parse::().unwrap(), - DependencyResult { - outputs: [( - "artifact".parse::().unwrap(), - "bundle.tar".to_owned(), - )] - .into_iter() - .collect::(), - result: JobResult::Failure, - }, - )] - .into_iter() - .collect(); - let Some(JobDefinition::Workflow(source)) = assignment.definition.as_ref() else { - panic!("this projection is driven from workflow source"); - }; - let hir = GithubActionsCompiler.compile(source).unwrap(); - let mut plans = syncode_runner::workflow::plans(hir).unwrap(); - let plan = plans.remove(0); - let context = - evaluation_context(&assignment, ControlVendor::SynCode, &provider(), plan.job()).unwrap(); - - assert_condition("failure()", &context); - assert_condition("always()", &context); - assert_condition("needs.build.result == 'failure'", &context); - assert_condition("needs.build.outputs.artifact == 'bundle.tar'", &context); -} - -fn assignment() -> JobAssignment { - let inputs = object([( - PropertyName::literal("target"), - Value::String("prod".to_owned()), - )]); - let event = object([(PropertyName::literal("inputs"), inputs)]); - JobAssignment { - id: JobId::new(1), - setup_error: None, - resource_key: None, - definition: Some(JobDefinition::Workflow(WorkflowSource::new( - WorkflowDialect::GitHubActions, - WORKFLOW.as_bytes().to_vec(), - ))), - context: Some(DynamicObject::from_iter([( - PropertyName::literal("event"), - event, - )])), - secrets: Secrets::default(), - needs: Needs::default(), - variables: Variables::default(), - action_repository: ActionRepository::ControlPlane, - action_artifacts: None, - actions_runtime: None, - } -} - -fn provider() -> ProviderMetadata { - ProviderMetadata { - id: ProviderId::new("test"), - version: "1".to_owned(), - api_version: "1".to_owned(), - platform: ExecutionPlatform { - operating_system: OperatingSystem::Linux, - architecture: Architecture::Amd64, - }, - } -} - -fn sandbox_metadata() -> SandboxMetadata { - let name = - ServiceName::new("web".to_owned()).unwrap_or_else(|error| panic!("service: {error}")); - let port = ContainerPort::from_str("80/tcp").unwrap_or_else(|error| panic!("port: {error}")); - SandboxMetadata::new(vec![ServiceMetadata::new( - RuntimeServiceName::from(&name), - "container-1".to_owned(), - "job-network".to_owned(), - vec![ServicePort::new(port, 49_152)], - )]) -} - -fn assert_condition( - expression: &str, - context: &syncode_runner::github_actions::expression::EvaluationContext, -) { - let result = expression - .parse::() - .and_then(|program| program.evaluate_condition(context)) - .unwrap_or_else(|error| panic!("expression {expression:?}: {error}")); - assert!(result, "expression {expression:?} was false"); -} - -fn object(values: [(PropertyName, Value); N]) -> Value { - Value::Object(Arc::new(DynamicObject::from_iter(values))) -} - -const WORKFLOW: &str = r#" -jobs: - build: - runs-on: ubuntu-latest - strategy: - fail-fast: false - max-parallel: 2 - matrix: - target: [prod] - steps: - - run: echo ok -"#; +#![allow(clippy::panic, clippy::unwrap_used)] + +use std::str::FromStr; +use std::sync::Arc; + +use syncode_runner::control::{ + ActionRepository, ControlVendor, DependencyResult, JobAssignment, JobDefinition, JobId, + JobResult, Needs, Outputs, SecretValue, Secrets, Variables, WorkflowDialect, WorkflowSource, +}; +use syncode_runner::execution::{ + Architecture, ContainerPort, ExecutionPlatform, OperatingSystem, ProviderId, ProviderMetadata, + RuntimeServiceName, SandboxMetadata, ServiceMetadata, ServiceName, ServicePort, +}; +use syncode_runner::github_actions::context::{apply_sandbox_metadata, evaluation_context}; +use syncode_runner::github_actions::expression::{EvaluationStatus, ExpressionProgram, evaluate}; +use syncode_runner::workflow::{ + DynamicObject, JobKey, OutputName, PropertyName, Value, WorkflowCompiler, +}; +use syncode_workflow_github_actions::compiler::GithubActionsCompiler; + +#[test] +fn projects_typed_assignment_plan_and_sandbox_contexts() { + let assignment = assignment(); + let Some(JobDefinition::Workflow(source)) = assignment.definition.as_ref() else { + panic!("this projection is driven from workflow source"); + }; + let hir = GithubActionsCompiler + .compile(source) + .unwrap_or_else(|error| panic!("compile: {error}")); + let mut plans = + syncode_runner::workflow::plans(hir).unwrap_or_else(|error| panic!("plan: {error}")); + let plan = plans.remove(0); + let mut context = + evaluation_context(&assignment, ControlVendor::SynCode, &provider(), plan.job()) + .unwrap_or_else(|error| panic!("context: {error}")); + + assert_condition("inputs.target == 'prod'", &context); + assert_condition("syncode.event.inputs.target == 'prod'", &context); + assert_condition("job.status == 'success'", &context); + assert_condition( + "strategy.fail-fast == false && strategy.max-parallel == 2", + &context, + ); + assert!(matches!( + evaluate("steps", &context), + Ok(Value::Object(values)) if values.is_empty() + )); + assert_condition("syncode.actor == github.actor", &context); + assert_condition("gitea.actor == github.actor", &context); + assert_condition("forgejo.actor == github.actor", &context); + + apply_sandbox_metadata(&mut context, &sandbox_metadata()) + .unwrap_or_else(|error| panic!("sandbox context: {error}")); + assert_condition("job.services.web.id == 'container-1'", &context); + assert_condition("job.services.web.network == 'job-network'", &context); + assert_condition("job.services.web.ports[80] == 49152", &context); + context.set_status(EvaluationStatus::Cancelled); + assert_condition("job.status == 'cancelled'", &context); + assert_condition("job.services.web.id == 'container-1'", &context); +} + +#[test] +fn failed_needs_drive_status_checks_and_outputs() { + let mut assignment = assignment(); + assignment.needs = [( + "build".parse::().unwrap(), + DependencyResult { + outputs: [( + "artifact".parse::().unwrap(), + "bundle.tar".to_owned(), + )] + .into_iter() + .collect::(), + result: JobResult::Failure, + }, + )] + .into_iter() + .collect(); + let Some(JobDefinition::Workflow(source)) = assignment.definition.as_ref() else { + panic!("this projection is driven from workflow source"); + }; + let hir = GithubActionsCompiler.compile(source).unwrap(); + let mut plans = syncode_runner::workflow::plans(hir).unwrap(); + let plan = plans.remove(0); + let context = + evaluation_context(&assignment, ControlVendor::SynCode, &provider(), plan.job()).unwrap(); + + assert_condition("failure()", &context); + assert_condition("always()", &context); + assert_condition("needs.build.result == 'failure'", &context); + assert_condition("needs.build.outputs.artifact == 'bundle.tar'", &context); +} + +#[test] +fn syncode_projects_the_native_repository_token_as_github_token() { + let mut assignment = assignment(); + assignment.secrets = [ + ( + "SYNCODE_REPOSITORY_TOKEN".parse().unwrap(), + SecretValue::new("native-repository-token"), + ), + ( + "GITHUB_TOKEN".parse().unwrap(), + SecretValue::new("actions-runtime-token"), + ), + ] + .into_iter() + .collect(); + let Some(JobDefinition::Workflow(source)) = assignment.definition.as_ref() else { + panic!("this projection is driven from workflow source"); + }; + let hir = GithubActionsCompiler.compile(source).unwrap(); + let mut plans = syncode_runner::workflow::plans(hir).unwrap(); + let plan = plans.remove(0); + + let context = + evaluation_context(&assignment, ControlVendor::SynCode, &provider(), plan.job()).unwrap(); + + assert_condition("github.token == 'native-repository-token'", &context); +} + +fn assignment() -> JobAssignment { + let inputs = object([( + PropertyName::literal("target"), + Value::String("prod".to_owned()), + )]); + let event = object([(PropertyName::literal("inputs"), inputs)]); + JobAssignment { + id: JobId::new(1), + setup_error: None, + resource_key: None, + definition: Some(JobDefinition::Workflow(WorkflowSource::new( + WorkflowDialect::GitHubActions, + WORKFLOW.as_bytes().to_vec(), + ))), + context: Some(DynamicObject::from_iter([( + PropertyName::literal("event"), + event, + )])), + secrets: Secrets::default(), + needs: Needs::default(), + variables: Variables::default(), + action_repository: ActionRepository::ControlPlane, + action_artifacts: None, + actions_runtime: None, + } +} + +fn provider() -> ProviderMetadata { + ProviderMetadata { + id: ProviderId::new("test"), + version: "1".to_owned(), + api_version: "1".to_owned(), + platform: ExecutionPlatform { + operating_system: OperatingSystem::Linux, + architecture: Architecture::Amd64, + }, + } +} + +fn sandbox_metadata() -> SandboxMetadata { + let name = + ServiceName::new("web".to_owned()).unwrap_or_else(|error| panic!("service: {error}")); + let port = ContainerPort::from_str("80/tcp").unwrap_or_else(|error| panic!("port: {error}")); + SandboxMetadata::new(vec![ServiceMetadata::new( + RuntimeServiceName::from(&name), + "container-1".to_owned(), + "job-network".to_owned(), + vec![ServicePort::new(port, 49_152)], + )]) +} + +fn assert_condition( + expression: &str, + context: &syncode_runner::github_actions::expression::EvaluationContext, +) { + let result = expression + .parse::() + .and_then(|program| program.evaluate_condition(context)) + .unwrap_or_else(|error| panic!("expression {expression:?}: {error}")); + assert!(result, "expression {expression:?} was false"); +} + +fn object(values: [(PropertyName, Value); N]) -> Value { + Value::Object(Arc::new(DynamicObject::from_iter(values))) +} + +const WORKFLOW: &str = r#" +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 2 + matrix: + target: [prod] + steps: + - run: echo ok +"#; diff --git a/crates/runner-workflow-github-actions/src/context/credential.rs b/crates/runner-workflow-github-actions/src/context/credential.rs --- a/crates/runner-workflow-github-actions/src/context/credential.rs +++ b/crates/runner-workflow-github-actions/src/context/credential.rs @@ -1,90 +1,96 @@ -use syncode_runner_domain::{ - ControlVendor, IdentifierError, JobAssignment, SecretName, SecretValue, -}; -use syncode_runner_source::{RepositoryCredential, RepositoryUrl}; - -#[derive(Clone, Copy)] -enum CredentialSecret { - SyncodeRepository, - Gitea, - Github, -} - -pub fn repository_credential( - assignment: &JobAssignment, - vendor: ControlVendor, - origin: RepositoryUrl, -) -> Result, IdentifierError> { - for candidate in CredentialSecret::for_vendor(vendor) { - if let Some(token) = secret(assignment, *candidate)? { - return Ok(Some(RepositoryCredential::new(origin, token.clone()))); - } - } - Ok(None) -} - -pub(crate) fn github_token( - assignment: &JobAssignment, -) -> Result, IdentifierError> { - secret(assignment, CredentialSecret::Github) -} - -fn secret( - assignment: &JobAssignment, - candidate: CredentialSecret, -) -> Result, IdentifierError> { - let name = candidate.as_ref().parse::()?; - Ok(assignment.secrets.get(&name)) -} - -impl CredentialSecret { - const fn for_vendor(vendor: ControlVendor) -> &'static [Self] { - match vendor { - ControlVendor::SynCode => &[Self::SyncodeRepository, Self::Gitea, Self::Github], - ControlVendor::Gitea | ControlVendor::Forgejo => &[Self::Gitea, Self::Github], - ControlVendor::GitHub => &[Self::Github], - ControlVendor::GitLab => &[], - } - } -} - -impl AsRef for CredentialSecret { - fn as_ref(&self) -> &str { - match self { - Self::SyncodeRepository => "SYNCODE_REPOSITORY_TOKEN", - Self::Gitea => "GITEA_TOKEN", - Self::Github => "GITHUB_TOKEN", - } - } -} - -#[cfg(test)] -mod tests { - use syncode_runner_domain::ControlVendor; - - use super::CredentialSecret; - - #[test] - fn syncode_prefers_the_native_repository_token() { - let names = CredentialSecret::for_vendor(ControlVendor::SynCode) - .iter() - .map(AsRef::as_ref) - .collect::>(); - - assert_eq!( - names, - vec!["SYNCODE_REPOSITORY_TOKEN", "GITEA_TOKEN", "GITHUB_TOKEN"] - ); - } - - #[test] - fn forge_controls_do_not_request_syncode_credentials() { - for vendor in [ControlVendor::Gitea, ControlVendor::Forgejo] { - assert!( - CredentialSecret::for_vendor(vendor) - .iter() - .all(|candidate| candidate.as_ref() != "SYNCODE_REPOSITORY_TOKEN") - ); - } - } -} +use syncode_runner_domain::{ + ControlVendor, IdentifierError, JobAssignment, SecretName, SecretValue, +}; +use syncode_runner_source::{RepositoryCredential, RepositoryUrl}; + +#[derive(Clone, Copy)] +enum CredentialSecret { + SyncodeRepository, + Gitea, + Github, +} + +pub fn repository_credential( + assignment: &JobAssignment, + vendor: ControlVendor, + origin: RepositoryUrl, +) -> Result, IdentifierError> { + for candidate in CredentialSecret::for_vendor(vendor) { + if let Some(token) = secret(assignment, *candidate)? { + return Ok(Some(RepositoryCredential::new(origin, token.clone()))); + } + } + Ok(None) +} + +pub(crate) fn github_token( + assignment: &JobAssignment, + vendor: ControlVendor, +) -> Result, IdentifierError> { + for candidate in CredentialSecret::for_vendor(vendor) { + if let Some(token) = secret(assignment, *candidate)? { + return Ok(Some(token)); + } + } + Ok(None) +} + +fn secret( + assignment: &JobAssignment, + candidate: CredentialSecret, +) -> Result, IdentifierError> { + let name = candidate.as_ref().parse::()?; + Ok(assignment.secrets.get(&name)) +} + +impl CredentialSecret { + const fn for_vendor(vendor: ControlVendor) -> &'static [Self] { + match vendor { + ControlVendor::SynCode => &[Self::SyncodeRepository, Self::Gitea, Self::Github], + ControlVendor::Gitea | ControlVendor::Forgejo => &[Self::Gitea, Self::Github], + ControlVendor::GitHub => &[Self::Github], + ControlVendor::GitLab => &[], + } + } +} + +impl AsRef for CredentialSecret { + fn as_ref(&self) -> &str { + match self { + Self::SyncodeRepository => "SYNCODE_REPOSITORY_TOKEN", + Self::Gitea => "GITEA_TOKEN", + Self::Github => "GITHUB_TOKEN", + } + } +} + +#[cfg(test)] +mod tests { + use syncode_runner_domain::ControlVendor; + + use super::CredentialSecret; + + #[test] + fn syncode_prefers_the_native_repository_token() { + let names = CredentialSecret::for_vendor(ControlVendor::SynCode) + .iter() + .map(AsRef::as_ref) + .collect::>(); + + assert_eq!( + names, + vec!["SYNCODE_REPOSITORY_TOKEN", "GITEA_TOKEN", "GITHUB_TOKEN"] + ); + } + + #[test] + fn forge_controls_do_not_request_syncode_credentials() { + for vendor in [ControlVendor::Gitea, ControlVendor::Forgejo] { + assert!( + CredentialSecret::for_vendor(vendor) + .iter() + .all(|candidate| candidate.as_ref() != "SYNCODE_REPOSITORY_TOKEN") + ); + } + } +} diff --git a/crates/runner-workflow-github-actions/src/context/evaluation.rs b/crates/runner-workflow-github-actions/src/context/evaluation.rs --- a/crates/runner-workflow-github-actions/src/context/evaluation.rs +++ b/crates/runner-workflow-github-actions/src/context/evaluation.rs @@ -1,216 +1,216 @@ -use std::sync::Arc; - -use super::ContextProjectionError; -use super::result::GithubJobResult; -use crate::expression::ExpressionProgram; -use crate::expression::{ContextName, EvaluationContext, VendorContextName}; -use syncode_runner_domain::{ - ControlVendor, DynamicObject, JobAssignment, Outputs, PropertyName, Secrets, Value, Variables, -}; -use syncode_runner_execution::{ProviderMetadata, SandboxDirectory}; -use syncode_runner_workflow::{BooleanValue, JobPlan, PositiveIntegerValue}; - -pub fn evaluation_context( - task: &JobAssignment, - vendor: ControlVendor, - provider: &ProviderMetadata, - job: &JobPlan, -) -> Result { - let mut context = EvaluationContext::default(); - let mut github = task.context.clone().unwrap_or_default(); - if let Some(token) = super::github_token(task)? { - github.insert( - PropertyName::literal("token"), - Value::String(token.expose().to_owned()), - ); - } - let inputs = event_inputs(&github); - let github = Value::Object(Arc::new(github)); - context - .values_mut() - .insert(ContextName::Github, github.clone()); - for vendor in vendor_contexts(vendor) { - context - .values_mut() - .insert(ContextName::Vendor(*vendor), github.clone()); - } - context - .values_mut() - .insert(ContextName::Secrets, secret_object(&task.secrets)); - context - .values_mut() - .insert(ContextName::Vars, variables_object(&task.variables)); - context.values_mut().insert( - ContextName::Runner, - object([ - ( - PropertyName::literal("os"), - Value::String(provider.platform.operating_system.to_string()), - ), - ( - PropertyName::literal("arch"), - Value::String(provider.platform.architecture.to_string()), - ), - ( - PropertyName::literal("temp"), - Value::String(SandboxDirectory::RunnerTemp.as_ref().to_owned()), - ), - ( - PropertyName::literal("workspace"), - Value::String(SandboxDirectory::Workspace.as_ref().to_owned()), - ), - ]), - ); - context.values_mut().insert( - ContextName::Needs, - Value::Object(Arc::new( - task.needs - .iter() - .map(|(name, need)| { - ( - PropertyName::from(name), - object([ - ( - PropertyName::literal("outputs"), - outputs_object(&need.outputs), - ), - ( - PropertyName::literal("result"), - Value::String(GithubJobResult::from(need.result).to_string()), - ), - ]), - ) - }) - .collect(), - )), - ); - if task - .needs - .iter() - .any(|(_, need)| matches!(need.result, syncode_runner_domain::JobResult::Cancelled)) - { - context.set_status(crate::expression::EvaluationStatus::Cancelled); - } else if task.needs.iter().any(|(_, need)| { - matches!( - need.result, - syncode_runner_domain::JobResult::Failure | syncode_runner_domain::JobResult::Skipped - ) - }) { - context.set_status(crate::expression::EvaluationStatus::Failure); - } - context.values_mut().insert( - ContextName::Matrix, - Value::Object(Arc::new(job.matrix().clone())), - ); - context - .values_mut() - .insert(ContextName::Steps, object(std::iter::empty())); - context.values_mut().insert(ContextName::Inputs, inputs); - let strategy = strategy_object(job, &context)?; - context.values_mut().insert(ContextName::Strategy, strategy); - Ok(context) -} - -fn event_inputs(github: &DynamicObject) -> Value { - match github.property("event") { - Some(Value::Object(event)) => match event.property("inputs") { - Some(Value::Object(inputs)) => Value::Object(Arc::clone(inputs)), - _ => object(std::iter::empty()), - }, - _ => object(std::iter::empty()), - } -} - -fn strategy_object( - job: &JobPlan, - context: &EvaluationContext, -) -> Result { - let fail_fast = match job.strategy().fail_fast() { - BooleanValue::Literal(value) => *value, - BooleanValue::Expression(program) => program.evaluate_condition(context)?, - }; - let mut strategy = - DynamicObject::from_iter([(PropertyName::literal("fail-fast"), Value::Bool(fail_fast))]); - if let Some(max_parallel) = job.strategy().max_parallel() { - strategy.insert( - PropertyName::literal("max-parallel"), - Value::Number(max_parallel_value(max_parallel, context)? as f64), - ); - } - Ok(Value::Object(Arc::new(strategy))) -} - -fn max_parallel_value( - value: &PositiveIntegerValue, - context: &EvaluationContext, -) -> Result { - let value = match value { - PositiveIntegerValue::Literal(value) => return Ok(*value), - PositiveIntegerValue::Expression(program) => program.evaluate(context)?, - }; - match value { - Value::Number(value) - if value.is_finite() - && value > 0.0 - && value.fract() == 0.0 - && value <= u64::MAX as f64 => - { - Ok(value as u64) - } - Value::String(value) => value - .parse::() - .ok() - .filter(|value| *value > 0) - .ok_or(ContextProjectionError::InvalidMaxParallel), - _ => Err(ContextProjectionError::InvalidMaxParallel), - } -} - -const fn vendor_contexts(vendor: ControlVendor) -> &'static [VendorContextName] { - match vendor { - ControlVendor::SynCode => &[ - VendorContextName::SynCode, - VendorContextName::Gitea, - VendorContextName::Forgejo, - ], - ControlVendor::Gitea => &[VendorContextName::Gitea], - ControlVendor::Forgejo => &[VendorContextName::Forgejo, VendorContextName::Gitea], - ControlVendor::GitHub | ControlVendor::GitLab => &[], - } -} - -fn variables_object(values: &Variables) -> Value { - Value::Object(Arc::new( - values - .iter() - .map(|(name, value)| (PropertyName::from(name), Value::String(value.clone()))) - .collect(), - )) -} - -fn outputs_object(values: &Outputs) -> Value { - Value::Object(Arc::new( - values - .iter() - .map(|(name, value)| (PropertyName::from(name), Value::String(value.clone()))) - .collect(), - )) -} - -fn secret_object(values: &Secrets) -> Value { - Value::Object(Arc::new( - values - .iter() - .map(|(name, value)| { - ( - PropertyName::from(name), - Value::String(value.expose().to_owned()), - ) - }) - .collect(), - )) -} - -fn object(values: impl IntoIterator) -> Value { - Value::Object(Arc::new(DynamicObject::from_iter(values))) -} +use std::sync::Arc; + +use super::ContextProjectionError; +use super::result::GithubJobResult; +use crate::expression::ExpressionProgram; +use crate::expression::{ContextName, EvaluationContext, VendorContextName}; +use syncode_runner_domain::{ + ControlVendor, DynamicObject, JobAssignment, Outputs, PropertyName, Secrets, Value, Variables, +}; +use syncode_runner_execution::{ProviderMetadata, SandboxDirectory}; +use syncode_runner_workflow::{BooleanValue, JobPlan, PositiveIntegerValue}; + +pub fn evaluation_context( + task: &JobAssignment, + vendor: ControlVendor, + provider: &ProviderMetadata, + job: &JobPlan, +) -> Result { + let mut context = EvaluationContext::default(); + let mut github = task.context.clone().unwrap_or_default(); + if let Some(token) = super::github_token(task, vendor)? { + github.insert( + PropertyName::literal("token"), + Value::String(token.expose().to_owned()), + ); + } + let inputs = event_inputs(&github); + let github = Value::Object(Arc::new(github)); + context + .values_mut() + .insert(ContextName::Github, github.clone()); + for vendor in vendor_contexts(vendor) { + context + .values_mut() + .insert(ContextName::Vendor(*vendor), github.clone()); + } + context + .values_mut() + .insert(ContextName::Secrets, secret_object(&task.secrets)); + context + .values_mut() + .insert(ContextName::Vars, variables_object(&task.variables)); + context.values_mut().insert( + ContextName::Runner, + object([ + ( + PropertyName::literal("os"), + Value::String(provider.platform.operating_system.to_string()), + ), + ( + PropertyName::literal("arch"), + Value::String(provider.platform.architecture.to_string()), + ), + ( + PropertyName::literal("temp"), + Value::String(SandboxDirectory::RunnerTemp.as_ref().to_owned()), + ), + ( + PropertyName::literal("workspace"), + Value::String(SandboxDirectory::Workspace.as_ref().to_owned()), + ), + ]), + ); + context.values_mut().insert( + ContextName::Needs, + Value::Object(Arc::new( + task.needs + .iter() + .map(|(name, need)| { + ( + PropertyName::from(name), + object([ + ( + PropertyName::literal("outputs"), + outputs_object(&need.outputs), + ), + ( + PropertyName::literal("result"), + Value::String(GithubJobResult::from(need.result).to_string()), + ), + ]), + ) + }) + .collect(), + )), + ); + if task + .needs + .iter() + .any(|(_, need)| matches!(need.result, syncode_runner_domain::JobResult::Cancelled)) + { + context.set_status(crate::expression::EvaluationStatus::Cancelled); + } else if task.needs.iter().any(|(_, need)| { + matches!( + need.result, + syncode_runner_domain::JobResult::Failure | syncode_runner_domain::JobResult::Skipped + ) + }) { + context.set_status(crate::expression::EvaluationStatus::Failure); + } + context.values_mut().insert( + ContextName::Matrix, + Value::Object(Arc::new(job.matrix().clone())), + ); + context + .values_mut() + .insert(ContextName::Steps, object(std::iter::empty())); + context.values_mut().insert(ContextName::Inputs, inputs); + let strategy = strategy_object(job, &context)?; + context.values_mut().insert(ContextName::Strategy, strategy); + Ok(context) +} + +fn event_inputs(github: &DynamicObject) -> Value { + match github.property("event") { + Some(Value::Object(event)) => match event.property("inputs") { + Some(Value::Object(inputs)) => Value::Object(Arc::clone(inputs)), + _ => object(std::iter::empty()), + }, + _ => object(std::iter::empty()), + } +} + +fn strategy_object( + job: &JobPlan, + context: &EvaluationContext, +) -> Result { + let fail_fast = match job.strategy().fail_fast() { + BooleanValue::Literal(value) => *value, + BooleanValue::Expression(program) => program.evaluate_condition(context)?, + }; + let mut strategy = + DynamicObject::from_iter([(PropertyName::literal("fail-fast"), Value::Bool(fail_fast))]); + if let Some(max_parallel) = job.strategy().max_parallel() { + strategy.insert( + PropertyName::literal("max-parallel"), + Value::Number(max_parallel_value(max_parallel, context)? as f64), + ); + } + Ok(Value::Object(Arc::new(strategy))) +} + +fn max_parallel_value( + value: &PositiveIntegerValue, + context: &EvaluationContext, +) -> Result { + let value = match value { + PositiveIntegerValue::Literal(value) => return Ok(*value), + PositiveIntegerValue::Expression(program) => program.evaluate(context)?, + }; + match value { + Value::Number(value) + if value.is_finite() + && value > 0.0 + && value.fract() == 0.0 + && value <= u64::MAX as f64 => + { + Ok(value as u64) + } + Value::String(value) => value + .parse::() + .ok() + .filter(|value| *value > 0) + .ok_or(ContextProjectionError::InvalidMaxParallel), + _ => Err(ContextProjectionError::InvalidMaxParallel), + } +} + +const fn vendor_contexts(vendor: ControlVendor) -> &'static [VendorContextName] { + match vendor { + ControlVendor::SynCode => &[ + VendorContextName::SynCode, + VendorContextName::Gitea, + VendorContextName::Forgejo, + ], + ControlVendor::Gitea => &[VendorContextName::Gitea], + ControlVendor::Forgejo => &[VendorContextName::Forgejo, VendorContextName::Gitea], + ControlVendor::GitHub | ControlVendor::GitLab => &[], + } +} + +fn variables_object(values: &Variables) -> Value { + Value::Object(Arc::new( + values + .iter() + .map(|(name, value)| (PropertyName::from(name), Value::String(value.clone()))) + .collect(), + )) +} + +fn outputs_object(values: &Outputs) -> Value { + Value::Object(Arc::new( + values + .iter() + .map(|(name, value)| (PropertyName::from(name), Value::String(value.clone()))) + .collect(), + )) +} + +fn secret_object(values: &Secrets) -> Value { + Value::Object(Arc::new( + values + .iter() + .map(|(name, value)| { + ( + PropertyName::from(name), + Value::String(value.expose().to_owned()), + ) + }) + .collect(), + )) +} + +fn object(values: impl IntoIterator) -> Value { + Value::Object(Arc::new(DynamicObject::from_iter(values))) +}