use std::collections::BTreeMap; use ci_protocol::actions::{ActionsExecution, ActionsWorkflow, NeedResult}; use ci_protocol::runner::{JobPayload, LeaseDto}; use models::ci::{job_token, workflow_job, workflow_run}; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; use serde_json::{Value, json}; use time::OffsetDateTime; use crate::{ CiService, errors::{CiError, CiResult}, }; impl CiService { pub async fn build_actions_job_payload( &self, job: &workflow_job::Model, run: &workflow_run::Model, lease_token: String, ) -> CiResult { let context = job .actions_context .as_ref() .ok_or_else(|| CiError::Internal("Actions context missing".into()))?; let workflow: ActionsWorkflow = serde_json::from_value(context["workflow"].clone())?; let repo = self.repo_by_id(job.repo_id).await?; let space = self.space_of_repo(&repo).await?; let mut secrets = self.actions_job_secrets(job, run, context).await?; let fork = run.event_payload["pull_request"]["head"]["repo"]["fork"] .as_bool() .unwrap_or(false); if fork { secrets.clear(); } let id = uuid::Uuid::now_v7(); let secret = crate::secrets::random_secret(32); let token = format!("ci_{id}.{secret}"); let permissions = &context["permissions"]; job_token::ActiveModel { id: Set(id), job_id: Set(job.id), repo_id: Set(job.repo_id), actor_id: Set(run.trigger_user_id.unwrap_or(space.owner_id)), token_hash: Set(crate::secrets::sha256_hex(secret.as_bytes())), lease_token: Set(lease_token.clone()), contents_read: Set(matches!( permissions["contents"].as_str(), Some("read" | "write") )), contents_write: Set(!fork && permissions["contents"].as_str() == Some("write")), expires_at: Set(OffsetDateTime::now_utc() + time::Duration::seconds(self.config().ci.job_timeout_max_secs as i64 + 600)), } .insert(self.db_write()) .await?; secrets.insert("GITHUB_TOKEN".into(), token.clone()); let needs: BTreeMap = serde_json::from_value(context["needs_results"].clone()).unwrap_or_default(); let timeout = context["instance"]["timeout_seconds"] .as_u64() .unwrap_or(self.config().ci.job_timeout_max_secs) .min(self.config().ci.job_timeout_max_secs) .max(1); let mut github = self.actions_github_context(run).await?; github["job"] = context["job"].clone(); github["token"] = json!(token); let mut env = BTreeMap::from([ ("CI".into(), "true".into()), ("GITDATA_CI".into(), "true".into()), ("GITDATA_RUN_ID".into(), run.id.to_string()), ("GITDATA_JOB_ID".into(), job.id.to_string()), ]); if let Some(environment) = &job.environment { env.insert("GITDATA_ENVIRONMENT".into(), environment.clone()); } let actions = ActionsExecution { workflow, job: context["job"].as_str().unwrap_or(&job.job_key).into(), matrix: context["instance"]["matrix"].clone(), github, event: run.event_payload.clone(), needs, inputs: serde_json::from_value(context["inputs"].clone()).unwrap_or_default(), vars: self.actions_variables(job.repo_id).await?, token, runtime_url: format!("{}/", self.actions_runtime_url()?), runtime_token: self.actions_runtime_token(job, run)?, }; Ok(JobPayload { job_id: job.id, run_id: run.id, repo_id: repo.id, space_id: space.id, run_attempt: job.attempt, job_name: job.name.clone(), clone_url: format!( "{}/{}/{}.git", self.config().git.base_url.trim_end_matches('/'), space.lower_name, repo.name ), commit_sha: run.commit_sha.clone(), git_ref: run.git_ref.clone(), branch: run.branch.clone(), tag: run.tag.clone(), base_sha: run.base_sha.clone(), base_ref: run.base_ref.clone(), pr_number: run.pr_number, event: run.event.as_str().into(), workflow_name: run.workflow_name.clone(), workflow_file: run.workflow_file.clone(), env, secrets, allow_git_token: false, container: None, timeout_secs: timeout, steps: Vec::new(), artifacts: Vec::new(), needs_artifacts: Vec::new(), lease: LeaseDto { token: lease_token, expires_at: job.lease_expires_at.unwrap_or(OffsetDateTime::now_utc()), heartbeat_interval_secs: self.config().ci.heartbeat_interval_secs, attempt: job.attempt, }, actions: Some(actions), }) } async fn actions_job_secrets( &self, job: &workflow_job::Model, run: &workflow_run::Model, context: &Value, ) -> CiResult> { let mut contexts = Vec::new(); let mut cursor = context; while cursor .get("caller_context") .is_some_and(|value| value.is_object()) { contexts.push(cursor.clone()); cursor = &cursor["caller_context"]; if contexts.len() > 10 { return Err(CiError::InvalidArgument( "workflow call depth exceeds 10".into(), )); } } let mut secrets: BTreeMap = self .resolved_secrets(job.repo_id, job.space_id, None) .await? .into_iter() .collect(); for current in contexts.into_iter().rev() { let mapping = ¤t["secret_mapping"]; if mapping.as_str() == Some("inherit") { continue; } if mapping.is_null() { secrets.clear(); continue; } let needs: BTreeMap = serde_json::from_value(current["caller_needs"].clone()).unwrap_or_default(); let mut request = self .actions_request(run, ¤t["caller_context"], &needs, "evaluate") .await?; request["value"] = mapping.clone(); request["secrets"] = json!(secrets); secrets = serde_json::from_value(self.actions_engine(request).await?)?; } if let Some(environment) = &job.environment { if let Some(environment) = self .find_actions_environment(job.space_id, job.repo_id, environment) .await? { let rows = models::ci::ci_secret::Entity::find() .filter(models::ci::ci_secret::Column::EnvironmentId.eq(environment.id)) .all(self.db_write()) .await?; for row in rows { secrets.insert( row.name, crate::secrets::decrypt(self.encryption_key(), &row.value_ciphertext)?, ); } } } Ok(secrets) } }