fix: Project native checkout token #68

Manually merged
day01 merged 1 commits from fix/0.6-project-native-github-token into develop 2026-08-30 17:38:10 +00:00
3 changed files with 37 additions and 3 deletions
Showing only changes of commit 812d64ce2c - Show all commits
+29 -1
View File
@@ -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::<JobKey>().unwrap(),
DependencyResult {
outputs: [(
"artifact".parse::<OutputName>().unwrap(),
"bundle.tar".to_owned(),
)]
.into_iter()
.collect::<Outputs>(),
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::<ExpressionProgram>()
.and_then(|program| program.evaluate_condition(context))
.unwrap_or_else(|error| panic!("expression {expression:?}: {error}"));
assert!(result, "expression {expression:?} was false");
}

fn object<const N: usize>(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::<JobKey>().unwrap(),
DependencyResult {
outputs: [(
"artifact".parse::<OutputName>().unwrap(),
"bundle.tar".to_owned(),
)]
.into_iter()
.collect::<Outputs>(),
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::<ExpressionProgram>()
.and_then(|program| program.evaluate_condition(context))
.unwrap_or_else(|error| panic!("expression {expression:?}: {error}"));
assert!(result, "expression {expression:?} was false");
}

fn object<const N: usize>(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
"#;
@@ -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<Option<RepositoryCredential>, 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<Option<&SecretValue>, IdentifierError> {
secret(assignment, CredentialSecret::Github)
}

fn secret(
assignment: &JobAssignment,
candidate: CredentialSecret,
) -> Result<Option<&SecretValue>, IdentifierError> {
let name = candidate.as_ref().parse::<SecretName>()?;
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<str> 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::<Vec<_>>();

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<Option<RepositoryCredential>, 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<Option<&SecretValue>, 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<Option<&SecretValue>, IdentifierError> {
let name = candidate.as_ref().parse::<SecretName>()?;
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<str> 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::<Vec<_>>();

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")
);
}
}
}
@@ -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<ExpressionProgram>,
) -> Result<EvaluationContext, ContextProjectionError> {
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<ExpressionProgram>,
context: &EvaluationContext,
) -> Result<Value, ContextProjectionError> {
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<ExpressionProgram>,
context: &EvaluationContext,
) -> Result<u64, ContextProjectionError> {
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::<u64>()
.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<Item = (PropertyName, Value)>) -> 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<ExpressionProgram>,
) -> Result<EvaluationContext, ContextProjectionError> {
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<ExpressionProgram>,
context: &EvaluationContext,
) -> Result<Value, ContextProjectionError> {
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<ExpressionProgram>,
context: &EvaluationContext,
) -> Result<u64, ContextProjectionError> {
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::<u64>()
.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<Item = (PropertyName, Value)>) -> Value {
Value::Object(Arc::new(DynamicObject::from_iter(values)))
}