1use anyhow::{Context, Result};
8use tracing::info;
9use ulid::Ulid;
10
11use crate::jobstore::{JobDir, resolve_root};
12use crate::run::{mask_env_vars, pre_create_log_files, resolve_effective_cwd};
13use crate::schema::{CreateData, JobMeta, JobMetaJob, Response};
14use crate::tag::dedup_tags;
15
16#[derive(Debug)]
25pub struct CreateOpts<'a> {
26 pub command: Vec<String>,
28 pub root: Option<&'a str>,
30 pub timeout_ms: u64,
32 pub kill_after_ms: u64,
34 pub cwd: Option<&'a str>,
36 pub env_vars: Vec<String>,
38 pub env_files: Vec<String>,
40 pub inherit_env: bool,
42 pub mask: Vec<String>,
44 pub progress_every_ms: u64,
46 pub notify_command: Option<String>,
48 pub notify_file: Option<String>,
50 pub shell_wrapper: Vec<String>,
52 pub tags: Vec<String>,
54 pub output_pattern: Option<String>,
56 pub output_match_type: Option<String>,
58 pub output_stream: Option<String>,
60 pub output_command: Option<String>,
62 pub output_file: Option<String>,
64}
65
66pub fn execute(opts: CreateOpts) -> Result<()> {
68 if opts.command.is_empty() {
69 anyhow::bail!("no command specified for create");
70 }
71
72 let root = resolve_root(opts.root);
73 std::fs::create_dir_all(&root)
74 .with_context(|| format!("create jobs root {}", root.display()))?;
75
76 let job_id = Ulid::new().to_string();
77 let created_at = crate::run::now_rfc3339_pub();
78
79 let env_keys: Vec<String> = opts
80 .env_vars
81 .iter()
82 .map(|kv| kv.split('=').next().unwrap_or(kv.as_str()).to_string())
83 .collect();
84
85 let masked_env_vars = mask_env_vars(&opts.env_vars, &opts.mask);
86
87 let effective_cwd = resolve_effective_cwd(opts.cwd);
88
89 let on_output_match = crate::notify::build_output_match_config(
91 opts.output_pattern,
92 opts.output_match_type,
93 opts.output_stream,
94 opts.output_command,
95 opts.output_file,
96 None,
97 );
98
99 let notification =
100 if opts.notify_command.is_some() || opts.notify_file.is_some() || on_output_match.is_some()
101 {
102 Some(crate::schema::NotificationConfig {
103 notify_command: opts.notify_command.clone(),
104 notify_file: opts.notify_file.clone(),
105 on_output_match,
106 })
107 } else {
108 None
109 };
110
111 let tags = dedup_tags(opts.tags)?;
113
114 let meta = JobMeta {
115 job: JobMetaJob { id: job_id.clone() },
116 schema_version: crate::schema::SCHEMA_VERSION.to_string(),
117 command: opts.command.clone(),
118 created_at: created_at.clone(),
119 root: root.display().to_string(),
120 env_keys,
121 env_vars: masked_env_vars,
122 env_vars_runtime: opts.env_vars.clone(),
126 mask: opts.mask.clone(),
127 cwd: Some(effective_cwd),
128 notification,
129 tags,
130 inherit_env: opts.inherit_env,
132 env_files: opts.env_files.clone(),
133 timeout_ms: opts.timeout_ms,
134 kill_after_ms: opts.kill_after_ms,
135 progress_every_ms: opts.progress_every_ms,
136 shell_wrapper: Some(opts.shell_wrapper.clone()),
137 };
138
139 let job_dir = JobDir::create(&root, &job_id, &meta)?;
140 info!(job_id = %job_id, "created job directory (created state)");
141
142 pre_create_log_files(&job_dir)?;
144
145 job_dir.init_state_created()?;
147
148 let stdout_log_path = job_dir.stdout_path().display().to_string();
149 let stderr_log_path = job_dir.stderr_path().display().to_string();
150
151 Response::new(
152 "create",
153 CreateData {
154 job_id,
155 state: "created".to_string(),
156 stdout_log_path,
157 stderr_log_path,
158 },
159 )
160 .print();
161
162 Ok(())
163}