diff --git a/docs/schemas.md b/docs/schemas.md index 35a2d8a..989eac0 100644 --- a/docs/schemas.md +++ b/docs/schemas.md @@ -166,14 +166,13 @@ declare_job( The guest exports `_start`. During `_start`, it may read files from the selected workflow commit with `workflow_file_len` and `read_workflow_file`, parse the format it understands, and call `declare_job` once per job. Version 1 serves -paths under `.sip/workflows/` plus the legacy root `.build.yml` and -`.build.yaml` paths. Missing or disallowed paths return `-1`. `parent` and -`requires` are job names in this first slice; the host translates them to -durable job IDs before writing SQLite rows. `requires` is encoded as +paths under `.sip/workflows/`. Missing or disallowed paths return `-1`. +`parent` and `requires` are job names in this first slice; the host translates +them to durable job IDs before writing SQLite rows. `requires` is encoded as newline-delimited UTF-8 names. The basic module looks for `.sip/workflows/sip.yml`, `.sip/workflows/sip.yaml`, `.sip/workflows/ci.yml`, -`.sip/workflows/ci.yaml`, `.build.yml`, and `.build.yaml`, uses -`unsafe-host-shell` as the executor, and passes the parsed command/script text +and `.sip/workflows/ci.yaml`, uses `unsafe-host-shell` as the executor, and +passes the parsed command/script text as UTF-8. ## WASM Executor ABI diff --git a/examples/wasm-modules/basic/src/lib.rs b/examples/wasm-modules/basic/src/lib.rs index f842c66..936806a 100644 --- a/examples/wasm-modules/basic/src/lib.rs +++ b/examples/wasm-modules/basic/src/lib.rs @@ -38,8 +38,6 @@ fn read_yaml() -> String { ".sip/workflows/sip.yaml", ".sip/workflows/ci.yml", ".sip/workflows/ci.yaml", - ".build.yml", - ".build.yaml", ] { let len = unsafe { workflow_file_len(path.as_ptr(), path.len() as i32) }; if len <= 0 { diff --git a/src/main.rs b/src/main.rs index 67ec9a8..bb119c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -75,7 +75,7 @@ struct StateDirs { tmp: PathBuf, } -#[derive(Debug)] +#[derive(Debug, Clone)] struct DeclaredJob { id: String, name: String, @@ -89,6 +89,8 @@ struct DeclaredJob { struct WorkflowHost { repo: PathBuf, workflow_oid: String, + workflow_execution_id: String, + next_job_index: usize, jobs: Vec, } @@ -361,7 +363,12 @@ fn process_event( if workflow.oid != "none" { if let Some(module_path) = orchestrator_module_path(repo, dirs, name)? { let module_path = TempPath::new(module_path); - let jobs = run_workflow_module(module_path.as_ref(), repo, &workflow)?; + let jobs = run_workflow_module( + module_path.as_ref(), + repo, + &workflow_execution_id, + &workflow, + )?; workflow_ran = true; let jobs = persist_declared_jobs(&db, &workflow_execution_id, &jobs)?; if let Some(executor_path) = executor_module_path()? { @@ -535,6 +542,7 @@ impl Drop for TempPath { fn run_workflow_module( module_path: &Path, repo: &Path, + workflow_execution_id: &str, workflow: &Workflow, ) -> Result, String> { let engine = Engine::default(); @@ -626,6 +634,8 @@ fn run_workflow_module( WorkflowHost { repo: repo.to_path_buf(), workflow_oid: workflow.oid.clone(), + workflow_execution_id: workflow_execution_id.to_string(), + next_job_index: 1, jobs: Vec::new(), }, ); @@ -654,9 +664,6 @@ fn workflow_file_bytes( } fn is_allowed_workflow_file(path: &str) -> bool { - if path == ".build.yml" || path == ".build.yaml" { - return true; - } let path = Path::new(path); let mut components = path.components(); if components.next() != Some(Component::Normal(OsStr::new(".sip"))) { @@ -738,8 +745,12 @@ fn declare_guest_job( Some(value) => value, None => return -1, }; - caller.data_mut().jobs.push(DeclaredJob { - id: String::new(), + let host = caller.data_mut(); + let index = host.next_job_index; + host.next_job_index += 1; + let id = declared_job_id(&host.workflow_execution_id, index, &name, &host.jobs); + host.jobs.push(DeclaredJob { + id, name, parent_name, requires_names: requires, @@ -910,26 +921,20 @@ fn persist_declared_jobs( workflow_execution_id: &str, jobs: &[DeclaredJob], ) -> Result, String> { - let mut jobs_with_ids = Vec::new(); - for (index, job) in jobs.iter().enumerate() { - let mut job = DeclaredJob { - id: format!( - "{}-{}-{}", - workflow_execution_id, - index + 1, - id_fragment(&job.name) - ), - name: job.name.clone(), - parent_name: job.parent_name.clone(), - requires_names: job.requires_names.clone(), - executor: job.executor.clone(), - command: job.command.clone(), - }; - if job.id.len() > 160 { - job.id.truncate(160); + for job in jobs { + if job.id.is_empty() { + return Err(format!("workflow job {} has no assigned id", job.name)); + } + if jobs + .iter() + .filter(|candidate| candidate.id == job.id) + .count() + > 1 + { + return Err(format!("workflow job id {} is not unique", job.id)); } - jobs_with_ids.push(job); } + let jobs_with_ids = jobs.to_vec(); let mut sql = String::from("PRAGMA foreign_keys = ON;\nBEGIN IMMEDIATE;\n"); for job in &jobs_with_ids { @@ -994,6 +999,37 @@ fn persist_declared_jobs( Ok(jobs_with_ids) } +fn declared_job_id( + workflow_execution_id: &str, + index: usize, + name: &str, + existing: &[DeclaredJob], +) -> String { + let mut id = format!("{workflow_execution_id}-{index}-{}", id_fragment(name)); + if id.len() > 160 { + id.truncate(160); + id = id.trim_end_matches('-').to_string(); + } + if !existing.iter().any(|job| job.id == id) { + return id; + } + + let mut suffix = 2; + loop { + let suffix_text = format!("-{suffix}"); + let mut candidate = id.clone(); + if candidate.len() + suffix_text.len() > 160 { + candidate.truncate(160 - suffix_text.len()); + candidate = candidate.trim_end_matches('-').to_string(); + } + candidate.push_str(&suffix_text); + if !existing.iter().any(|job| job.id == candidate) { + return candidate; + } + suffix += 1; + } +} + fn id_fragment(value: &str) -> String { let mut fragment = String::new(); let mut previous_dash = false;