Skip to content
Clone
9301161
197 lines 7.6 KB Rust
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<JobPayload> {
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<String, NeedResult> =
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,
},
trace: run
.event_payload
.get("_gitdata_trace")
.and_then(|value| value.as_str())
.map(str::to_owned),
actions: Some(actions),
})
}
async fn actions_job_secrets(
&self,
job: &workflow_job::Model,
run: &workflow_run::Model,
context: &Value,
) -> CiResult<BTreeMap<String, String>> {
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<String, String> = self
.resolved_secrets(job.repo_id, job.space_id, None)
.await?
.into_iter()
.collect();
for current in contexts.into_iter().rev() {
let mapping = &current["secret_mapping"];
if mapping.as_str() == Some("inherit") {
continue;
}
if mapping.is_null() {
secrets.clear();
continue;
}
let needs: BTreeMap<String, NeedResult> =
serde_json::from_value(current["caller_needs"].clone()).unwrap_or_default();
let mut request = self
.actions_request(run, &current["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
&& 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)
}
}