diff --git a/README.md b/README.md index f796155..051cf77 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,18 @@ The current durable record formats are defined in [docs/schemas.md](docs/schemas - `hooks/pre-receive.rc` rejects unprivileged writes to `refs/ci/*` and `refs/workflows/*`. - `hooks/post-receive.rc` records accepted branch and tag updates in - `$GIT_DIR/sip/queue`. + `$GIT_DIR/sip/queue`. When `refs/sip/orchestrator` exists, it also runs the + worker during the push and reports when a workflow ran. - `bin/sip-worker.rc` launches the Rust worker, which consumes queued events, peels refs to commit IDs, detects `.sip/workflows`, and writes manifest blobs to `refs/ci/runs/`, plus latest status refs under `refs/ci/status/heads/*` or `refs/ci/status/tags/*`. - `bin/sip-install-hooks.rc` installs the hooks into a target repository. - The worker initializes `$GIT_DIR/sip/logs/jobs.sqlite` with workflow - execution, job, dependency, and FTS5 log tables. When `SIP_WORKFLOW_MODULE` - points at a WASM module, the worker gives that module the selected workflow - YAML and records declared jobs and dependencies. + execution, job, dependency, and FTS5 log tables. When + `refs/sip/orchestrator` points at a commit containing `workflow.wasm` or + `workflow.wat`, the worker gives that module the selected workflow YAML and + records declared jobs and dependencies. - `examples/wasm-modules/basic` is a Rust guest module for the first host ABI. It reads a small YAML subset with either a `jobs:` map or `tasks:` list. - `examples/wasm-modules/unsafe-host-shell` is the first executor module. It @@ -46,13 +48,16 @@ To exercise the WASM declaration scaffold: ```sh cargo build --manifest-path examples/wasm-modules/basic/Cargo.toml --target wasm32-unknown-unknown +blob=$(git -C /path/to/bare.git hash-object -w examples/wasm-modules/basic/target/wasm32-unknown-unknown/debug/sip_basic_workflow.wasm) +tree=$(printf '100644 blob %s\tworkflow.wasm\n' "$blob" | git -C /path/to/bare.git mktree) +commit=$(printf 'orchestrator\n' | git -C /path/to/bare.git commit-tree "$tree") +git -C /path/to/bare.git update-ref refs/sip/orchestrator "$commit" ``` To execute declared jobs with the unsafe host shell executor: ```sh cargo build --manifest-path examples/wasm-modules/unsafe-host-shell/Cargo.toml --target wasm32-unknown-unknown -SIP_WORKFLOW_MODULE=examples/wasm-modules/basic/target/wasm32-unknown-unknown/debug/sip_basic_workflow.wasm \ SIP_EXECUTOR_MODULE=examples/wasm-modules/unsafe-host-shell/target/wasm32-unknown-unknown/debug/sip_unsafe_host_shell.wasm \ bin/sip-worker.rc /path/to/bare.git ``` diff --git a/bin/sip-install-hooks.rc b/bin/sip-install-hooks.rc index 6a2dc39..54b6ed2 100755 --- a/bin/sip-install-hooks.rc +++ b/bin/sip-install-hooks.rc @@ -21,7 +21,7 @@ if(! ~ $gitdir /*) #*/ hookdir=$gitdir^/hooks mkdir -p $hookdir cp $root^/hooks/pre-receive.rc $hookdir^/pre-receive -cp $root^/hooks/post-receive.rc $hookdir^/post-receive +awk -v root=$root '{ gsub("@SIP_ROOT@", root); print }' $root^/hooks/post-receive.rc >$hookdir^/post-receive chmod +x $hookdir^/pre-receive $hookdir^/post-receive echo installed sip hooks in $hookdir diff --git a/docs/schemas.md b/docs/schemas.md index a21c965..e6cad38 100644 --- a/docs/schemas.md +++ b/docs/schemas.md @@ -71,8 +71,9 @@ job_log_db=sip/logs/jobs.sqlite ``` `checkout_oid` is a peeled commit ID for non-delete events. It is `none` for -delete events or for objects that cannot be resolved to a commit. Workflow -fields are `none` unless a workflow source is selected. +delete events. Non-delete events whose new object cannot be resolved to a commit +are moved to `sip/failed`. Workflow fields are `none` unless a workflow source +is selected. `workflow_execution_id` is the durable ID for the workflow execution associated with the run. Version 1 derives it from the run ID as `wf-`. `job_log_db` points at the SQLite database under the repository's Git directory @@ -82,7 +83,9 @@ logs. Workflow source policy: - `refs/workflows/default` is the default protected workflow ref. Workers may - read a different protected ref when `SIP_WORKFLOW_REF` is set. + read a different protected ref when `SIP_WORKFLOW_REF` is set to a valid ref + under `refs/workflows/*`. Other values are ignored for protected workflow + selection. - A protected workflow ref wins over pushed worktree workflows when it exists and contains `.sip/workflows`. - Pushed worktree workflows are selected only when no protected workflow is @@ -124,12 +127,14 @@ The worker creates this database before processing queued events. It contains: - `job_logs_fts`: FTS5 index over job log content. Version 1 records workflow execution rows when run refs are created. If -`SIP_WORKFLOW_MODULE` points at a WASM module and the selected workflow commit -contains `.sip/workflows/sip.yml`, `.sip/workflows/ci.yml`, `.build.yml`, or -the matching `.yaml` variants, the worker passes that YAML to the module and -records jobs declared from its `_start` function. If `SIP_EXECUTOR_MODULE` -points at an executor WASM module, the worker executes ready jobs through that -module and stores output lines in `job_logs`. +`refs/sip/orchestrator` points at a commit whose tree contains `workflow.wasm` +or `workflow.wat`, and the selected workflow commit contains +`.sip/workflows/sip.yml`, `.sip/workflows/ci.yml`, `.build.yml`, or the matching +`.yaml` variants, the worker passes that YAML to the module and records jobs +declared from its `_start` function. If the orchestrator ref does not exist, no +workflow module runs. If `SIP_EXECUTOR_MODULE` points at an executor WASM +module, the worker executes ready jobs through that module and stores output +lines in `job_logs`. ## WASM Job Declaration ABI diff --git a/examples/run-test-repo.rc b/examples/run-test-repo.rc index 8f70267..91e77ad 100755 --- a/examples/run-test-repo.rc +++ b/examples/run-test-repo.rc @@ -33,12 +33,15 @@ git -C $src commit -m 'example jobs' >/dev/null || fail could not commit example git -C $src branch -M main $root^/bin/sip-install-hooks.rc $remote >/dev/null || fail could not install hooks -git -C $src remote add origin $remote -SIP_ACTOR=example git -C $src push origin main >/dev/null >[2]/dev/null || fail could not push example repo +workflow_module=$root^/examples/wasm-modules/basic/target/wasm32-unknown-unknown/debug/sip_basic_workflow.wasm +blob=`{git -C $remote hash-object -w $workflow_module} +tree=`{printf '100644 blob %s\tworkflow.wasm\n' $blob | git -C $remote mktree} +commit=`{printf 'orchestrator\n' | git -C $remote commit-tree $tree} +git -C $remote update-ref refs/sip/orchestrator $commit || fail could not install orchestrator module -SIP_WORKFLOW_MODULE=$root^/examples/wasm-modules/basic/target/wasm32-unknown-unknown/debug/sip_basic_workflow.wasm \ -SIP_EXECUTOR_MODULE=$root^/examples/wasm-modules/unsafe-host-shell/target/wasm32-unknown-unknown/debug/sip_unsafe_host_shell.wasm \ - $root^/bin/sip-worker.rc $remote >/dev/null || fail worker failed +git -C $src remote add origin $remote +SIP_ACTOR=example SIP_EXECUTOR_MODULE=$root^/examples/wasm-modules/unsafe-host-shell/target/wasm32-unknown-unknown/debug/sip_unsafe_host_shell.wasm \ + git -C $src push origin main >/dev/null >[2]/dev/null || fail could not push example repo echo repo: $remote echo jobs: diff --git a/hooks/post-receive.rc b/hooks/post-receive.rc index b106104..10d5838 100755 --- a/hooks/post-receive.rc +++ b/hooks/post-receive.rc @@ -1,6 +1,7 @@ #!/usr/bin/env rc gitdir=`{git rev-parse --git-dir} +sip_root='@SIP_ROOT@' queue=$gitdir^/sip/queue if(! mkdir -p $queue) { echo sip: could not create event queue $queue >[1=2] @@ -8,9 +9,7 @@ if(! mkdir -p $queue) { } queued_at=`{date +%s} -# Keep hooks low latency: record branch/tag events and let sip-worker.rc do the -# slower scheduling and refs/ci mutation work after the push has returned. -awk -v queue=$queue -v queued_at=$queued_at ' +if(! awk -v queue=$queue -v queued_at=$queued_at ' function zero_oid(oid) { return oid ~ "^0+$" } @@ -59,4 +58,11 @@ $3 ~ "^refs/(heads|tags)/" { exit 1 } } -' +') { + exit 1 +} + +if(git show-ref --verify --quiet refs/sip/orchestrator) { + if(! $sip_root^/bin/sip-worker.rc $gitdir) + exit 1 +} diff --git a/hooks/pre-receive.rc b/hooks/pre-receive.rc index be19bb6..6474096 100755 --- a/hooks/pre-receive.rc +++ b/hooks/pre-receive.rc @@ -1,8 +1,9 @@ #!/usr/bin/env rc -# Reject writes to CI-controlled namespaces from git-receive-pack. Server-side -# admin tools that need to maintain these refs must update them outside the -# client receive path. +# Reject writes to CI-controlled namespaces from git-receive-pack. The +# orchestrator module is the only accepted refs/sip/* receive-path ref. +# Server-side admin tools that need to maintain restricted refs must update +# them outside the client receive path. awk ' function reject(message) { print message > "/dev/stderr" @@ -22,10 +23,13 @@ function reject(message) { if (ref ~ "^refs/(ci|workflows)/") reject("sip: updates to " ref " are restricted") + if (ref ~ "^refs/sip/" && ref != "refs/sip/orchestrator") + reject("sip: updates to " ref " are not accepted") + if (ref ~ "^refs/(notes|replace)/") reject("sip: updates to " ref " are not accepted") - if (ref !~ "^refs/(heads|tags|ci|workflows|pull|merge|notes|replace)/") + if (ref !~ "^refs/(heads|tags|ci|workflows|sip|pull|merge|notes|replace)/") reject("sip: updates to unknown ref namespace " ref " are not accepted") } diff --git a/src/main.rs b/src/main.rs index 85bdf33..222e7be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ use std::process::{Command, Stdio}; use wasmtime::{Caller, Engine, Linker, Module, Store}; const ZERO_OID: &str = "0000000000000000000000000000000000000000"; +const ORCHESTRATOR_REF: &str = "refs/sip/orchestrator"; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum EventKind { @@ -169,8 +170,7 @@ fn run_worker(repo: &Path) -> Result<(), String> { let log_db = dirs.logs.join("jobs.sqlite"); ensure_log_db(&log_db)?; - let protected_workflow_ref = - env::var("SIP_WORKFLOW_REF").unwrap_or_else(|_| "refs/workflows/default".to_string()); + let protected_workflow_ref = protected_workflow_ref(repo)?; let mut events = queue_events(&dirs.queue)?; events.sort(); @@ -196,6 +196,22 @@ fn run_worker(repo: &Path) -> Result<(), String> { Ok(()) } +fn protected_workflow_ref(repo: &Path) -> Result, String> { + let value = match env::var("SIP_WORKFLOW_REF") { + Ok(value) => value, + Err(_) => "refs/workflows/default".to_string(), + }; + + if value.starts_with("refs/workflows/") + && git_status(repo, ["check-ref-format", value.as_str()])? + { + Ok(Some(value)) + } else { + eprintln!("sip: ignoring unsafe SIP_WORKFLOW_REF {value}"); + Ok(None) + } +} + fn queue_events(queue: &Path) -> Result, String> { let mut events = Vec::new(); for entry in fs::read_dir(queue).map_err(|err| format!("could not read queue: {err}"))? { @@ -278,7 +294,7 @@ fn parse_event(path: &Path) -> Result { fn process_event( repo: &Path, dirs: &StateDirs, - protected_workflow_ref: &str, + protected_workflow_ref: &Option, name: &str, event: &Event, ) -> Result<(), String> { @@ -296,7 +312,11 @@ fn process_event( return Ok(()); } }; - let workflow = resolve_workflow(repo, protected_workflow_ref, checkout_oid.as_deref())?; + let workflow = resolve_workflow( + repo, + protected_workflow_ref.as_deref(), + checkout_oid.as_deref(), + )?; let workflow_execution_id = format!("wf-{name}"); let manifest = render_manifest( name, @@ -336,11 +356,16 @@ fn process_event( checkout_oid.as_deref(), &workflow, )?; - if let Some(module_path) = workflow_module_path()? { - if workflow.oid != "none" { + let mut workflow_ran = false; + if workflow.oid != "none" { + if let Some(module_path) = orchestrator_module_path(repo, dirs, name)? { + let module_path = TempPath::new(module_path); if let Some(workflow_yaml) = read_workflow_yaml(repo, &workflow)? { - let jobs = - run_workflow_module(&module_path, workflow_yaml.as_bytes().to_vec())?; + let jobs = run_workflow_module( + module_path.as_ref(), + workflow_yaml.as_bytes().to_vec(), + )?; + workflow_ran = true; let jobs = persist_declared_jobs(&db, &workflow_execution_id, &jobs)?; if let Some(executor_path) = executor_module_path()? { let checkout = checkout_workdir(repo, dirs, name, &workflow)?; @@ -351,7 +376,9 @@ fn process_event( } update_latest_status(repo, event, &blob)?; move_event(&event_path, &dirs.done, name)?; - println!("queued {run_ref}"); + if workflow_ran { + println!("sip: workflow ran {run_ref}"); + } } else { eprintln!("sip: failed to update {run_ref}"); move_event(&event_path, &dirs.failed, name)?; @@ -376,7 +403,7 @@ fn checkout_oid(repo: &Path, event: &Event) -> Result, String> { fn resolve_workflow( repo: &Path, - protected_workflow_ref: &str, + protected_workflow_ref: Option<&str>, checkout_oid: Option<&str>, ) -> Result { let mut workflow = Workflow::none(); @@ -393,26 +420,29 @@ fn resolve_workflow( } } - if git_status( - repo, - ["show-ref", "--verify", "--quiet", protected_workflow_ref], - )? { - if let Some(commit) = git_output( + if let Some(protected_workflow_ref) = protected_workflow_ref { + if git_status( repo, - [ - "rev-parse", - format!("{protected_workflow_ref}^{{commit}}").as_str(), - ], + ["show-ref", "--verify", "--quiet", protected_workflow_ref], )? { - if has_workflow_tree(repo, &commit)? { - workflow = Workflow { - source: format!("protected:{protected_workflow_ref}"), - oid: commit.clone(), - digest: tree_oid(repo, &commit)?.unwrap_or_else(|| "none".to_string()), - trust: "protected".to_string(), - grants: "status.set,jobs.enqueue,artifacts.put,http.fetch_allowed,secrets.sign" - .to_string(), - }; + if let Some(commit) = git_output( + repo, + [ + "rev-parse", + format!("{protected_workflow_ref}^{{commit}}").as_str(), + ], + )? { + if has_workflow_tree(repo, &commit)? { + workflow = Workflow { + source: format!("protected:{protected_workflow_ref}"), + oid: commit.clone(), + digest: tree_oid(repo, &commit)?.unwrap_or_else(|| "none".to_string()), + trust: "protected".to_string(), + grants: + "status.set,jobs.enqueue,artifacts.put,http.fetch_allowed,secrets.sign" + .to_string(), + }; + } } } } @@ -443,14 +473,6 @@ fn tree_oid(repo: &Path, commit: &str) -> Result, String> { ) } -fn workflow_module_path() -> Result, String> { - match env::var("SIP_WORKFLOW_MODULE") { - Ok(value) if !value.is_empty() => Ok(Some(PathBuf::from(value))), - Ok(_) | Err(env::VarError::NotPresent) => Ok(None), - Err(err) => Err(format!("invalid SIP_WORKFLOW_MODULE: {err}")), - } -} - fn executor_module_path() -> Result, String> { match env::var("SIP_EXECUTOR_MODULE") { Ok(value) if !value.is_empty() => Ok(Some(PathBuf::from(value))), @@ -459,6 +481,39 @@ fn executor_module_path() -> Result, String> { } } +fn orchestrator_module_path( + repo: &Path, + dirs: &StateDirs, + name: &str, +) -> Result, String> { + if !git_status(repo, ["show-ref", "--verify", "--quiet", ORCHESTRATOR_REF])? { + return Ok(None); + } + + let Some(commit) = git_output( + repo, + [ + "rev-parse", + "--verify", + format!("{ORCHESTRATOR_REF}^{{commit}}").as_str(), + ], + )? + else { + return Ok(None); + }; + + for filename in ["workflow.wasm", "workflow.wat"] { + if let Some(bytes) = git_bytes(repo, ["show", format!("{commit}:{filename}").as_str()])? { + let path = dirs.tmp.join(format!("{name}-{filename}")); + fs::write(&path, bytes) + .map_err(|err| format!("could not write {}: {err}", path.display()))?; + return Ok(Some(path)); + } + } + + Ok(None) +} + fn read_workflow_yaml(repo: &Path, workflow: &Workflow) -> Result, String> { for path in [ ".sip/workflows/sip.yml", @@ -476,6 +531,28 @@ fn read_workflow_yaml(repo: &Path, workflow: &Workflow) -> Result Ok(None) } +struct TempPath { + path: PathBuf, +} + +impl TempPath { + fn new(path: PathBuf) -> Self { + Self { path } + } +} + +impl AsRef for TempPath { + fn as_ref(&self) -> &Path { + &self.path + } +} + +impl Drop for TempPath { + fn drop(&mut self) { + let _ = fs::remove_file(&self.path); + } +} + fn run_workflow_module( module_path: &Path, workflow_yaml: Vec, @@ -1330,6 +1407,25 @@ where } } +fn git_bytes(repo: &Path, args: I) -> Result>, String> +where + I: IntoIterator, + S: AsRef, +{ + let output = git_command(repo, args) + .stderr(Stdio::null()) + .output() + .map_err(|err| format!("could not run git: {err}"))?; + if !output.status.success() { + return Ok(None); + } + if output.stdout.is_empty() { + Ok(None) + } else { + Ok(Some(output.stdout)) + } +} + fn git_command(repo: &Path, args: I) -> Command where I: IntoIterator, diff --git a/tests/integration.rc b/tests/integration.rc index d30c5d1..3c6bf7e 100755 --- a/tests/integration.rc +++ b/tests/integration.rc @@ -26,6 +26,17 @@ fn assert_contains { } } +fn assert_not_contains { + file=$1 + needle=$2 + msg=$3 + if(grep -F $needle $file >/dev/null >[2=1]) { + echo --- $file --- >[1=2] + sed -n '1,120p' $file >[1=2] + fail $msg^': unexpectedly found '^$needle + } +} + fn assert_ref_exists { repo=$1 ref=$2 @@ -61,6 +72,16 @@ fn build_unsafe_host_shell_module { echo $root^/examples/wasm-modules/unsafe-host-shell/target/wasm32-unknown-unknown/debug/sip_unsafe_host_shell.wasm } +fn install_orchestrator_module { + repo=$1 + module=$2 + filename=$3 + blob=`{git -C $repo hash-object -w $module} + tree=`{printf '100644 blob %s\t%s\n' $blob $filename | git -C $repo mktree} + commit=`{printf 'orchestrator\n' | git -C $repo commit-tree $tree} + git -C $repo update-ref refs/sip/orchestrator $commit || fail could not install orchestrator module +} + fn assert_event_schema { file=$1 awk -F ' ' ' @@ -174,6 +195,11 @@ fn test_branch_tag_and_protected_refs { if(git -C $src push origin HEAD:refs/replace/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >/dev/null >[2]/dev/null) fail push to refs/replace/* should be rejected + + if(! git -C $src push origin HEAD:refs/sip/orchestrator >/dev/null >[2]/dev/null) + fail push to refs/sip/orchestrator should be accepted + if(git -C $src push origin HEAD:refs/sip/other >/dev/null >[2]/dev/null) + fail push to unknown refs/sip/* should be rejected } fn test_no_workflow_repository { @@ -231,11 +257,82 @@ fn test_protected_workflow_policy { assert_contains $tmp^/main.manifest capability_grants=status.set,jobs.enqueue,artifacts.put,http.fetch_allowed,secrets.sign 'manifest records protected workflow grants' } +fn assert_unsafe_workflow_ref_is_untrusted { + override=$1 + name=$2 + tmp=`{setup_pair} + src=$tmp^/src + remote=$tmp^/remote.git + + mkdir -p $src^/.sip/workflows + printf 'name: pushed\n' >$src^/.sip/workflows/ci.yml + printf 'app\n' >$src^/README.md + git -C $src add README.md .sip/workflows/ci.yml + git -C $src commit -m pushed-workflow >/dev/null || fail could not commit pushed workflow + git -C $src branch -M main + git -C $src tag v1 + SIP_ACTOR=mallory git -C $src push origin main >/dev/null >[2]/dev/null || fail could not push unsafe override branch + commit=`{git -C $src rev-parse HEAD} + git -C $remote fetch $src refs/tags/v1:refs/tags/v1 >/dev/null >[2]/dev/null || fail could not install unsafe override tag + git -C $remote update-ref refs/ci/runs/manual $commit || fail could not install unsafe ci ref + + rm -f $remote^/sip/queue/*.event + printf '1\t0000000000000000000000000000000000000000\t%s\trefs/heads/main\tactor\t1\tcreate\n' $commit >$remote^/sip/queue/$name^.event + + SIP_WORKFLOW_REF=$override $root^/bin/sip-worker.rc $remote >/dev/null >[2]/dev/null || fail worker failed for unsafe workflow ref $override + + git -C $remote cat-file -p refs/ci/runs/$name >$tmp^/$name^.manifest + assert_contains $tmp^/$name^.manifest workflow_source=worktree:.sip/workflows 'unsafe workflow ref should not override pushed workflow source' + assert_contains $tmp^/$name^.manifest workflow_trust=untrusted 'unsafe workflow ref should not produce protected trust' + assert_contains $tmp^/$name^.manifest capability_grants=status.set,jobs.enqueue,artifacts.put 'unsafe workflow ref should keep pushed workflow grants' + assert_not_contains $tmp^/$name^.manifest secrets.sign 'unsafe workflow ref should not grant secrets' +} + +fn test_unsafe_workflow_ref_overrides_are_ignored { + assert_unsafe_workflow_ref_is_untrusted refs/heads/main unsafe-head + assert_unsafe_workflow_ref_is_untrusted refs/tags/v1 unsafe-tag + assert_unsafe_workflow_ref_is_untrusted refs/ci/runs/manual unsafe-ci + assert_unsafe_workflow_ref_is_untrusted 'refs/workflows/bad..name' unsafe-malformed +} + +fn test_push_reports_only_when_orchestrator_runs { + tmp=`{setup_pair} + src=$tmp^/src + remote=$tmp^/remote.git + + mkdir -p $src^/.sip/workflows + printf 'jobs:\n' >$src^/.sip/workflows/ci.yml + printf 'app\n' >$src^/README.md + git -C $src add README.md .sip/workflows/ci.yml + git -C $src commit -m no-orchestrator >/dev/null || fail could not commit no-orchestrator workflow + git -C $src branch -M main + SIP_ACTOR=frank git -C $src push origin main >$tmp^/no-orchestrator.push >[2=1] || fail could not push no-orchestrator workflow + assert_not_contains $tmp^/no-orchestrator.push 'sip: workflow ran' 'push without orchestrator should not report workflow run' + + tmp=`{setup_pair} + src=$tmp^/src + remote=$tmp^/remote.git + printf '(module (memory (export "memory") 1) (func (export "_start")))\n' >$tmp^/workflow.wat + install_orchestrator_module $remote $tmp^/workflow.wat workflow.wat + + mkdir -p $src^/.sip/workflows + printf 'jobs:\n' >$src^/.sip/workflows/ci.yml + printf 'app\n' >$src^/README.md + git -C $src add README.md .sip/workflows/ci.yml + git -C $src commit -m wat-orchestrator >/dev/null || fail could not commit wat-orchestrator workflow + git -C $src branch -M main + SIP_ACTOR=grace git -C $src push origin main >$tmp^/wat-orchestrator.push >[2=1] || fail could not push wat-orchestrator workflow + assert_contains $tmp^/wat-orchestrator.push 'sip: workflow ran refs/ci/runs/' 'push with WAT orchestrator should report workflow run' + assert_sql_count $remote^/sip/logs/jobs.sqlite 'select count(*) from workflow_executions;' 1 'wat orchestrator records workflow execution' + assert_sql_count $remote^/sip/logs/jobs.sqlite 'select count(*) from jobs;' 0 'wat orchestrator can run without declaring jobs' +} + fn test_basic_wasm_workflow_declares_jobs { module=`{build_basic_wasm_module} tmp=`{setup_pair} src=$tmp^/src remote=$tmp^/remote.git + install_orchestrator_module $remote $module workflow.wasm mkdir -p $src^/.sip/workflows cp $root^/examples/workflows/basic-jobs.yml $src^/.sip/workflows/ci.yml @@ -245,8 +342,6 @@ fn test_basic_wasm_workflow_declares_jobs { git -C $src branch -M main SIP_ACTOR=dana git -C $src push origin main >/dev/null >[2]/dev/null || fail could not push basic wasm workflow - SIP_WORKFLOW_MODULE=$module $root^/bin/sip-worker.rc $remote >/dev/null || fail worker failed for basic wasm workflow - assert_sql_count $remote^/sip/logs/jobs.sqlite 'select count(*) from workflow_executions;' 1 'basic wasm worker records workflow execution' assert_sql_count $remote^/sip/logs/jobs.sqlite 'select count(*) from jobs;' 2 'basic wasm module declares jobs' assert_sql_count $remote^/sip/logs/jobs.sqlite 'select count(*) from job_dependencies;' 1 'basic wasm module declares dependencies' @@ -260,14 +355,13 @@ fn test_unsafe_host_shell_executor_records_logs { tmp=`{setup_pair} src=$tmp^/src remote=$tmp^/remote.git + install_orchestrator_module $remote $workflow_module workflow.wasm cp -R $root^/examples/test-repo/. $src^/ git -C $src add README.md .sip/workflows/ci.yml git -C $src commit -m unsafe-host-shell-readonly >/dev/null || fail could not commit unsafe host shell workflow git -C $src branch -M main - SIP_ACTOR=erin git -C $src push origin main >/dev/null >[2]/dev/null || fail could not push unsafe host shell workflow - - SIP_WORKFLOW_MODULE=$workflow_module SIP_EXECUTOR_MODULE=$executor_module $root^/bin/sip-worker.rc $remote >/dev/null || fail worker failed for unsafe host shell workflow + SIP_ACTOR=erin SIP_EXECUTOR_MODULE=$executor_module git -C $src push origin main >/dev/null >[2]/dev/null || fail could not push unsafe host shell workflow assert_sql_count $remote^/sip/logs/jobs.sqlite 'select count(*) from jobs where status = ''passed'';' 2 'unsafe host shell jobs pass' assert_sql_count $remote^/sip/logs/jobs.sqlite 'select count(*) from job_dependencies;' 1 'unsafe host shell fixture records dependency' @@ -378,6 +472,8 @@ fn test_non_delete_events_require_commit_checkout { test_branch_tag_and_protected_refs test_no_workflow_repository test_protected_workflow_policy +test_unsafe_workflow_ref_overrides_are_ignored +test_push_reports_only_when_orchestrator_runs test_basic_wasm_workflow_declares_jobs test_unsafe_host_shell_executor_records_logs test_stale_events_do_not_overwrite_status