Skip to content

Commit 6da5513

Browse files
committed
feat(config): support working_dir at job, build, and run levels
Add 3-way working_dir configuration similar to env_file: - job.working_dir: shared default for build and run - build.working_dir: build-specific (overrides job-level) - run.working_dir: run-specific (overrides job-level) Priority: build/run.working_dir > job.working_dir
1 parent 835b840 commit 6da5513

File tree

3 files changed

+87
-9
lines changed

3 files changed

+87
-9
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,14 @@ jobs:
119119
build:
120120
sh: cargo build --release
121121
timeout: 30m
122+
working_dir: ./src
122123
env:
123124
CARGO_INCREMENTAL: "1"
124125
run:
125126
sh: ./target/release/my-app
126127
timeout: 5m
127128
concurrency: skip
128-
working_dir: ./app
129+
working_dir: ./dist
129130
retry:
130131
max: 3
131132
delay: 5s
@@ -136,6 +137,7 @@ jobs:
136137
log:
137138
file: output.log
138139
max_size: 50M
140+
working_dir: ./app
139141
env_file: .env.job
140142
env:
141143
DEBUG: "false"
@@ -176,6 +178,7 @@ Options:
176178
| `schedule.cron` | string | **required** | Cron expression (5 fields: min hour day month weekday) |
177179
| `schedule.timezone` | string, optional | runner's | Job-specific timezone override |
178180
| `enabled` | bool, optional | `true` | Enable/disable job |
181+
| `working_dir` | string, optional | - | Working directory for build and run (can be overridden) |
179182
| `env_file` | string, optional | - | Shared .env file for build and run |
180183
| `env` | map, optional | - | Shared environment variables for build and run |
181184
| `webhook` | list, optional | - | Job-specific webhooks (extends runner webhooks) |
@@ -186,6 +189,7 @@ Options:
186189
|-------|------|---------|-------------|
187190
| `sh` | string | **required** | Build command (runs in `build/` directory) |
188191
| `timeout` | duration, optional | run.timeout | Build timeout |
192+
| `working_dir` | string, optional | job's | Working directory (relative to build dir) |
189193
| `env_file` | string, optional | - | Build-specific .env file |
190194
| `env` | map, optional | - | Build-specific environment variables |
191195

@@ -196,7 +200,7 @@ Options:
196200
| `sh` | string | **required** | Run command (runs in `run/` directory) |
197201
| `timeout` | duration, optional | `1h` | Execution timeout |
198202
| `concurrency` | string, optional | `skip` | `parallel`, `wait`, `skip`, or `replace` |
199-
| `working_dir` | string, optional | - | Working directory (relative to run dir) |
203+
| `working_dir` | string, optional | job's | Working directory (relative to run dir) |
200204
| `env_file` | string, optional | - | Run-specific .env file |
201205
| `env` | map, optional | - | Run-specific environment variables |
202206

src/actor/job/executor.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,11 @@ async fn run_build_command(
158158
}
159159
};
160160

161+
let work_dir = resolve_work_dir(build_dir, &job.id, &build_config.working_dir);
162+
161163
let mut cmd = Command::new("sh");
162164
cmd.args(["-c", &build_config.command])
163-
.current_dir(build_dir)
165+
.current_dir(&work_dir)
164166
.stdout(std::process::Stdio::piped())
165167
.stderr(std::process::Stdio::piped());
166168

@@ -381,12 +383,12 @@ pub async fn execute_job(job: &Job, sot_path: &Path, runner: &RunnerConfig) -> b
381383
false
382384
}
383385

384-
fn resolve_work_dir(run_dir: &Path, job_id: &str, working_dir: &Option<String>) -> PathBuf {
386+
fn resolve_work_dir(base_dir: &Path, job_id: &str, working_dir: &Option<String>) -> PathBuf {
385387
match working_dir {
386388
Some(dir) => {
387389
let expanded = env::expand_string(dir);
388-
let work_path = run_dir.join(&expanded);
389-
match (work_path.canonicalize(), run_dir.canonicalize()) {
390+
let work_path = base_dir.join(&expanded);
391+
match (work_path.canonicalize(), base_dir.canonicalize()) {
390392
(Ok(resolved), Ok(base)) if resolved.starts_with(&base) => resolved,
391393
_ => {
392394
warn!(
@@ -395,11 +397,11 @@ fn resolve_work_dir(run_dir: &Path, job_id: &str, working_dir: &Option<String>)
395397
working_dir = %dir,
396398
"Invalid working_dir: path traversal or non-existent"
397399
);
398-
run_dir.to_path_buf()
400+
base_dir.to_path_buf()
399401
}
400402
}
401403
}
402-
None => run_dir.to_path_buf(),
404+
None => base_dir.to_path_buf(),
403405
}
404406
}
405407

src/config.rs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ pub struct BuildConfigRaw {
116116
pub timeout: Option<String>,
117117
pub env_file: Option<String>,
118118
pub env: Option<HashMap<String, String>>,
119+
pub working_dir: Option<String>,
119120
}
120121

121122
#[derive(Debug, Deserialize)]
@@ -148,6 +149,7 @@ pub struct JobConfig {
148149
pub enabled: Option<bool>,
149150
pub env_file: Option<String>,
150151
pub env: Option<HashMap<String, String>>,
152+
pub working_dir: Option<String>,
151153
#[serde(default)]
152154
pub webhook: Vec<WebhookConfig>,
153155
}
@@ -185,6 +187,7 @@ pub struct BuildConfig {
185187
pub timeout: Duration,
186188
pub env_file: Option<String>,
187189
pub env: Option<HashMap<String, String>>,
190+
pub working_dir: Option<String>,
188191
}
189192

190193
#[derive(Debug, Clone)]
@@ -283,6 +286,7 @@ fn parse_job(
283286
timeout: build_timeout,
284287
env_file: b.env_file,
285288
env: b.env,
289+
working_dir: b.working_dir.or_else(|| job.working_dir.clone()),
286290
})
287291
})
288292
.transpose()?;
@@ -349,7 +353,7 @@ fn parse_job(
349353
timeout,
350354
concurrency: job.run.concurrency,
351355
retry,
352-
working_dir: job.run.working_dir,
356+
working_dir: job.run.working_dir.or(job.working_dir),
353357
enabled: job.enabled.unwrap_or(true),
354358
timezone: job_timezone,
355359
env_file: job.env_file,
@@ -649,6 +653,74 @@ jobs:
649653
assert_eq!(jobs[0].working_dir.as_deref(), Some("./scripts"));
650654
}
651655

656+
#[test]
657+
fn parse_job_level_working_dir() {
658+
let yaml = r#"
659+
jobs:
660+
test:
661+
schedule:
662+
cron: "* * * * *"
663+
run:
664+
sh: echo test
665+
working_dir: ./job-dir
666+
"#;
667+
let (_, jobs) = parse_config(yaml).unwrap();
668+
assert_eq!(jobs[0].working_dir.as_deref(), Some("./job-dir"));
669+
}
670+
671+
#[test]
672+
fn parse_working_dir_run_overrides_job() {
673+
let yaml = r#"
674+
jobs:
675+
test:
676+
schedule:
677+
cron: "* * * * *"
678+
run:
679+
sh: echo test
680+
working_dir: ./run-dir
681+
working_dir: ./job-dir
682+
"#;
683+
let (_, jobs) = parse_config(yaml).unwrap();
684+
assert_eq!(jobs[0].working_dir.as_deref(), Some("./run-dir"));
685+
}
686+
687+
#[test]
688+
fn parse_working_dir_build_inherits_job() {
689+
let yaml = r#"
690+
jobs:
691+
test:
692+
schedule:
693+
cron: "* * * * *"
694+
build:
695+
sh: cargo build
696+
run:
697+
sh: ./app
698+
working_dir: ./job-dir
699+
"#;
700+
let (_, jobs) = parse_config(yaml).unwrap();
701+
let build = jobs[0].build.as_ref().unwrap();
702+
assert_eq!(build.working_dir.as_deref(), Some("./job-dir"));
703+
}
704+
705+
#[test]
706+
fn parse_working_dir_build_overrides_job() {
707+
let yaml = r#"
708+
jobs:
709+
test:
710+
schedule:
711+
cron: "* * * * *"
712+
build:
713+
sh: cargo build
714+
working_dir: ./build-dir
715+
run:
716+
sh: ./app
717+
working_dir: ./job-dir
718+
"#;
719+
let (_, jobs) = parse_config(yaml).unwrap();
720+
let build = jobs[0].build.as_ref().unwrap();
721+
assert_eq!(build.working_dir.as_deref(), Some("./build-dir"));
722+
}
723+
652724
#[test]
653725
fn parse_invalid_timezone() {
654726
let yaml = r#"

0 commit comments

Comments
 (0)