Skip to content

Commit fe7ac78

Browse files
committed
synthesize dynamic setup job automatically
1 parent 00dbce4 commit fe7ac78

File tree

3 files changed

+173
-4
lines changed

3 files changed

+173
-4
lines changed

src/commands/generate.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,29 @@ fn generate_from_jobs(
316316
provider.name(),
317317
output_path.display()
318318
);
319+
320+
// If dynamic mode is enabled but no explicit setup workflow exists, synthesize one
321+
if loaded_config.config.dynamic.unwrap_or(false)
322+
&& !merged_workflow_configs.keys().any(|k| k == "setup")
323+
{
324+
println!("Generating synthesized setup workflow");
325+
// Setup should go to provider default output path (config.yml) unless overridden
326+
let setup_output = if let Some(setup_out) = &loaded_config.config.output_path {
327+
original_dir_path(Path::new(setup_out))
328+
} else {
329+
original_dir_path(Path::new(provider.default_output_path()))
330+
};
331+
// Call provider-specific synthesized setup if available (only CircleCI for now)
332+
// Provider-agnostic support: synthesize CircleCI setup when using CircleCI provider
333+
if loaded_config.config.provider == "circleci" {
334+
cigen::providers::circleci::CircleCIProvider::new()
335+
.generator
336+
.generate_synthesized_setup(&loaded_config.config, &setup_output)
337+
.map_err(|e| {
338+
anyhow::anyhow!("Failed to generate synthesized setup: {}", e)
339+
})?;
340+
}
341+
}
319342
}
320343
}
321344

src/providers/circleci/generator.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,152 @@ impl CircleCIGenerator {
261261
Ok(circleci_config)
262262
}
263263

264+
/// Synthesize a minimal setup workflow that generates continuation config and continues the pipeline
265+
pub fn generate_synthesized_setup(
266+
&self,
267+
config: &Config,
268+
output_path: &std::path::Path,
269+
) -> Result<()> {
270+
use crate::providers::circleci::config as cc;
271+
use serde_yaml::{Mapping, Value};
272+
273+
let mut circleci_config = cc::CircleCIConfig {
274+
setup: Some(true),
275+
..Default::default()
276+
};
277+
278+
// Include orbs if specified (continuation or others)
279+
if let Some(orbs) = &config.orbs {
280+
circleci_config.orbs = Some(orbs.clone());
281+
}
282+
283+
// Build setup workflow with a single job
284+
let workflow_name = "setup".to_string();
285+
let mut workflow = cc::CircleCIWorkflow {
286+
when: None,
287+
unless: None,
288+
jobs: Vec::new(),
289+
};
290+
291+
let job_name = "generate_config".to_string();
292+
workflow
293+
.jobs
294+
.push(cc::CircleCIWorkflowJob::Simple(job_name.clone()));
295+
circleci_config.workflows.insert(workflow_name, workflow);
296+
297+
// Construct the job
298+
let mut job = cc::CircleCIJob {
299+
executor: None,
300+
docker: Some(vec![cc::CircleCIDockerImage {
301+
image: "cimg/ruby:3.3.5".to_string(),
302+
auth: None,
303+
name: None,
304+
entrypoint: None,
305+
command: None,
306+
user: None,
307+
environment: None,
308+
}]),
309+
machine: None,
310+
resource_class: Some("small".to_string()),
311+
working_directory: None,
312+
parallelism: None,
313+
environment: None,
314+
parameters: None,
315+
steps: Vec::new(),
316+
};
317+
318+
// Step: checkout
319+
job.steps
320+
.push(cc::CircleCIStep::new(Value::String("checkout".to_string())));
321+
322+
// Optional hook: set_package_versions_hash if present in user commands
323+
if let Some(cmds) = &config.parameters {
324+
let _ = cmds; // retain for future param-driven hooks
325+
}
326+
// We can't access user commands here; add a safe reference (CircleCI will error if missing)
327+
// To avoid validation errors, only add if config.vars contains a marker (future). Skipping for now.
328+
329+
// Step: run cigen to generate continuation config (.circleci/main.yml)
330+
let mut run_gen = Mapping::new();
331+
let mut run_gen_details = Mapping::new();
332+
run_gen_details.insert(
333+
Value::String("name".to_string()),
334+
Value::String("Generate continuation config".to_string()),
335+
);
336+
run_gen_details.insert(
337+
Value::String("command".to_string()),
338+
Value::String("cigen generate".to_string()),
339+
);
340+
run_gen.insert(
341+
Value::String("run".to_string()),
342+
Value::Mapping(run_gen_details),
343+
);
344+
job.steps
345+
.push(cc::CircleCIStep::new(Value::Mapping(run_gen)));
346+
347+
// Step: continue pipeline using jq --rawfile
348+
let mut run_cont = Mapping::new();
349+
let mut run_cont_details = Mapping::new();
350+
run_cont_details.insert(
351+
Value::String("name".to_string()),
352+
Value::String("Continue Pipeline".to_string()),
353+
);
354+
let cont_cmd = r#"
355+
if [ -z "${CIRCLE_CONTINUATION_KEY}" ]; then
356+
echo "CIRCLE_CONTINUATION_KEY is required. Make sure setup workflows are enabled."
357+
exit 1
358+
fi
359+
360+
CONFIG_PATH=".circleci/main.yml"
361+
if [ ! -f "$CONFIG_PATH" ]; then
362+
echo "Continuation config not found at $CONFIG_PATH" >&2
363+
exit 1
364+
fi
365+
366+
jq -n \
367+
--arg continuation "$CIRCLE_CONTINUATION_KEY" \
368+
--rawfile config "$CONFIG_PATH" \
369+
'{"continuation-key": $continuation, "configuration": $config}' \
370+
> /tmp/continuation.json
371+
372+
cat /tmp/continuation.json
373+
374+
[[ $(curl \
375+
-o /dev/stderr \
376+
-w '%{http_code}' \
377+
-XPOST \
378+
-H "Content-Type: application/json" \
379+
-H "Accept: application/json" \
380+
--data "@/tmp/continuation.json" \
381+
"https://circleci.com/api/v2/pipeline/continue") -eq 200 ]]
382+
"#
383+
.trim()
384+
.to_string();
385+
run_cont_details.insert(
386+
Value::String("command".to_string()),
387+
Value::String(cont_cmd),
388+
);
389+
run_cont.insert(
390+
Value::String("run".to_string()),
391+
Value::Mapping(run_cont_details),
392+
);
393+
job.steps
394+
.push(cc::CircleCIStep::new(Value::Mapping(run_cont)));
395+
396+
circleci_config.jobs.insert(job_name, job);
397+
398+
// Write YAML
399+
let yaml_content = serde_yaml::to_string(&circleci_config).into_diagnostic()?;
400+
let output_file = output_path.join("config.yml");
401+
std::fs::create_dir_all(output_path).into_diagnostic()?;
402+
std::fs::write(&output_file, yaml_content).into_diagnostic()?;
403+
404+
// Validate setup config
405+
self.validate_config(&output_file)?;
406+
407+
Ok(())
408+
}
409+
264410
fn build_workflow(
265411
&self,
266412
_workflow_name: &str,

src/providers/circleci/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,6 @@ impl Provider for CircleCIProvider {
3939
"circleci"
4040
}
4141

42-
fn default_output_path(&self) -> &'static str {
43-
".circleci"
44-
}
45-
4642
fn generate_workflow(
4743
&self,
4844
config: &Config,
@@ -65,4 +61,8 @@ impl Provider for CircleCIProvider {
6561
self.generator
6662
.generate_all(config, workflows, commands, output_path)
6763
}
64+
65+
fn default_output_path(&self) -> &'static str {
66+
".circleci"
67+
}
6868
}

0 commit comments

Comments
 (0)