fix: Project native checkout token #68
@@ -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)))
|
||||
}
|
||||
Reference in New Issue
Block a user