Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Generated by gunbc-codegen cigen. Do not edit manually.
# Regenerate with: cargo run -p gunbc-codegen -- cigen
# Generated by gunbc-codegen
# DO NOT EDIT - regenerate with: cargo run -p gunbc-codegen -- cigen

name: ci

Expand All @@ -13,6 +13,7 @@ on:

env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -D warnings

jobs:
ci:
Expand Down
5 changes: 3 additions & 2 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Generated by gunbc-codegen cigen. Do not edit manually.
# Regenerate with: cargo run -p gunbc-codegen -- cigen
# Generated by gunbc-codegen
# DO NOT EDIT - regenerate with: cargo run -p gunbc-codegen -- cigen

image: rust:latest

variables:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"

stages:
- ci
Expand Down
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
# Library crates (DAG op libraries)
"lib/primitives",
"lib/gist-ops",
"lib/llm-ops",
"lib/markdown",
"lib/transport",

Expand Down
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

.DEFAULT_GOAL := help

.PHONY: help codegen ensure-codegen build clean fmt-fix lint-fix test test-fix check check-fix clippy clippy-fix fmt fmt-check ci-yaml gist gist-dry buck2 buck2-dry makegen makegen-dry deps deps-dry ci ci-dry bootstrap bootstrap-dry
.PHONY: help codegen ensure-codegen build clean fmt-fix lint-fix test test-fix check check-fix clippy clippy-fix fmt fmt-check ci-yaml gist gist-dry buck2 buck2-dry makegen makegen-dry deps deps-dry ci ci-dry bootstrap bootstrap-dry testgen testgen-dry

# Ensure codegen has run (upsert pattern: check stamp -> run if missing)
ensure-codegen:
Expand Down Expand Up @@ -61,6 +61,8 @@ help:
@echo " ci - Run CI pipeline"
@echo " bootstrap - Generate Makefile and .gitignore"
@echo ""
@echo " testgen - Generate tests from DAG structures and MockSpecs"
@echo ""
@echo "Add -dry suffix for dry-run (e.g., make gist-dry)"

# ============================================================================
Expand Down Expand Up @@ -152,3 +154,10 @@ bootstrap: ensure-codegen
bootstrap-dry: ensure-codegen
@cargo run -p gunbc-dag --bin gunbc-bootstrap -- --dry-run

# gunbc-testgen: Generate tests from DAG structures and MockSpecs
testgen:
@cargo run -p gunbc-dag --bin gunbc-testgen --

testgen-dry:
@cargo run -p gunbc-dag --bin gunbc-testgen -- --dry-run

215 changes: 85 additions & 130 deletions core/codegen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ use gunbc_clippy::ClippyConfigRenderer;
use gunbc_codegen::{
all_cleanable_outputs, all_tools, generate_cli_with_import, generate_graph_rs, FileWriter,
};
use gunbc_ir::transport::ci::{CiRenderer, GitHubActionsProvider, GitLabCiProvider, RenderConfig};
use gunbc_ir::transport::ci::{
yaml_block, CacheConfig, CiRenderer, GitHubActionsProvider, GitLabCiProvider, RenderConfig,
};
use gunbc_ir::Renderable;
use std::env;
use std::fs;
Expand Down Expand Up @@ -292,48 +294,34 @@ fn cmd_cigen(dry_run: bool) {
&codegen.binary,
&format!("{} -- cigen", codegen.command()),
)
.with_runner("ubuntu-latest")
.with_env("CARGO_TERM_COLOR", "always")
.with_branches(vec!["main"]);

// Generate GitHub Actions YAML
let ci_yaml = generate_github_actions_template(&config);
let github_path = github_provider.output_path("ci");

match writer.write(Path::new(&github_path), &ci_yaml) {
Ok(result) => {
let status = if result.written {
if result.changed { "written" } else { "unchanged" }
} else {
"dry-run"
};
println!(" [ci] {} ({})", github_path, status);
}
Err(e) => {
eprintln!(" [ci] GitHub Actions ERROR: {}", e);
}
}

// Generate GitLab CI YAML
let gitlab_yaml = generate_gitlab_ci_template(&config);
let gitlab_path = gitlab_provider.output_path("ci");

match writer.write(Path::new(&gitlab_path), &gitlab_yaml) {
Ok(result) => {
let status = if result.written {
if result.changed { "written" } else { "unchanged" }
} else {
"dry-run"
};
println!(" [ci] {} ({})", gitlab_path, status);
}
Err(e) => {
eprintln!(" [ci] GitLab CI ERROR: {}", e);
.with_runner(gunbc_ir::transport::github_actions::ubuntu_latest())
.with_cargo_env(gunbc_ir::CargoEnv::ci())
.with_git(gunbc_ir::GitConfig::default())
.with_cache(CacheConfig::rust());

let outputs: Vec<(&str, String, String)> = vec![
("GitHub Actions", generate_github_actions_template(&config), github_provider.output_path("ci")),
("GitLab CI", generate_gitlab_ci_template(&config), gitlab_provider.output_path("ci")),
];

for (label, yaml, path) in &outputs {
match writer.write(Path::new(path), yaml) {
Ok(result) => {
let status = if result.written {
if result.changed { "written" } else { "unchanged" }
} else {
"dry-run"
};
println!(" [ci] {} ({})", path, status);
}
Err(e) => {
eprintln!(" [ci] {} ERROR: {}", label, e);
}
}
}

println!();
println!("Generated: 2 CI files");
println!("Generated: {} CI files", outputs.len());
}

/// Generate clippy.toml from ClippyConfig.
Expand Down Expand Up @@ -372,74 +360,57 @@ fn cmd_clippy_toml(dry_run: bool) {
}

/// Generate GitHub Actions YAML template.
///
/// Renders checkout, cache, and steps from `RenderConfig` model types
/// rather than hardcoding them in the template.
fn generate_github_actions_template(config: &RenderConfig) -> String {
let mut yaml = String::new();

yaml.push_str(&config.header("#"));
yaml.push_str(&format!("\n\nname: {}\n\n", config.workflow_name));

// Triggers
yaml.push_str("on:\n");
yaml.push_str(" push:\n");
yaml.push_str(" branches:\n");
for branch in &config.branches {
yaml.push_str(&format!(" - {}\n", branch));
}

// Triggers — derived from git config
let branches = config.git.ci_branches();
yaml.push_str("on:\n push:\n");
yaml_block(&mut yaml, " branches:", &branches, |b| format!(" - {}", b));
yaml.push_str(" pull_request:\n");
yaml.push_str(" branches:\n");
for branch in &config.branches {
yaml.push_str(&format!(" - {}\n", branch));
}
yaml.push('\n');

// Environment
yaml.push_str("env:\n");
yaml.push_str(" CARGO_TERM_COLOR: always\n");
for (key, value) in &config.env {
if key != "CARGO_TERM_COLOR" {
yaml.push_str(&format!(" {}: {}\n", key, value));
yaml_block(&mut yaml, " branches:", &branches, |b| format!(" - {}", b));

// Environment — derived from cargo env + manual overrides
yaml_block(&mut yaml, "env:", &config.all_env(), |(k, v)| format!(" {}: {}", k, v));

// Job
yaml.push_str(&format!(
"jobs:\n {}:\n runs-on: {}\n steps:\n",
config.workflow_name, config.runner.id,
));

// Checkout (from config model)
if let Some(checkout) = &config.checkout {
yaml.push_str(" - name: Checkout\n uses: actions/checkout@v4\n");
if let Some(depth) = checkout.fetch_depth {
yaml.push_str(&format!(" with:\n fetch-depth: {}\n", depth));
}
yaml.push('\n');
}
yaml.push('\n');

// Job
yaml.push_str("jobs:\n");
yaml.push_str(&format!(" {}:\n", config.workflow_name));
yaml.push_str(&format!(" runs-on: {}\n", config.runner));
yaml.push_str(" steps:\n");
yaml.push_str(" - name: Checkout\n");
yaml.push_str(" uses: actions/checkout@v4\n");
yaml.push_str(" with:\n");
yaml.push_str(" fetch-depth: 1\n");
yaml.push('\n');


// Rust toolchain
yaml.push_str(" - name: Setup Rust\n");
yaml.push_str(" uses: dtolnay/rust-toolchain@stable\n");
yaml.push('\n');

// Cache
yaml.push_str(" - name: Cache Cargo\n");
yaml.push_str(" uses: actions/cache@v4\n");
yaml.push_str(" with:\n");
yaml.push_str(" path: |\n");
yaml.push_str(" ~/.cargo/bin/\n");
yaml.push_str(" ~/.cargo/registry/index/\n");
yaml.push_str(" ~/.cargo/registry/cache/\n");
yaml.push_str(" ~/.cargo/git/db/\n");
yaml.push_str(" target/\n");
yaml.push_str(" key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}\n");
yaml.push_str(" restore-keys: |\n");
yaml.push_str(" cargo-${{ runner.os }}-\n");
yaml.push('\n');

yaml.push_str(" - name: Setup Rust\n uses: dtolnay/rust-toolchain@stable\n\n");

// Cache (from config model)
if let Some(cache) = &config.cache {
yaml.push_str(" - name: Cache Cargo\n uses: actions/cache@v4\n with:\n");
yaml_block(&mut yaml, " path: |", &cache.paths, |p| format!(" {}", p));
yaml.push_str(&format!(" key: {}\n", cache.key));
yaml_block(&mut yaml, " restore-keys: |", &cache.restore_keys, |k| format!(" {}", k));
}

// Run CI Pipeline
// gunbc-ci has a handwritten main.rs that handles codegen internally via the prep node.
// The prep node uses the resource acquisition (upsert) pattern: check if generated
// files exist, generate them if not. This makes CI self-healing.
yaml.push_str(" - name: Run CI Pipeline\n");
yaml.push_str(&format!(" run: {} --release\n", config.tool.command()));

yaml.push_str(&format!(
" - name: Run CI Pipeline\n run: {} --release\n",
config.tool.command(),
));

yaml
}

Expand All @@ -448,38 +419,22 @@ fn generate_gitlab_ci_template(config: &RenderConfig) -> String {
let mut yaml = String::new();

yaml.push_str(&config.header("#"));
yaml.push_str("\n\n");

// Image
yaml.push_str("image: rust:latest\n\n");

// Variables
yaml.push_str("variables:\n");
yaml.push_str(" CARGO_TERM_COLOR: always\n");
for (key, value) in &config.env {
if key != "CARGO_TERM_COLOR" {
yaml.push_str(&format!(" {}: \"{}\"\n", key, value));
}
}
yaml.push('\n');

// Stages
yaml.push_str("stages:\n");
yaml.push_str(" - ci\n\n");

// Cache
yaml.push_str("cache:\n");
yaml.push_str(" key: cargo-${CI_COMMIT_REF_SLUG}\n");
yaml.push_str(" paths:\n");
yaml.push_str(" - .cargo/\n");
yaml.push_str(" - target/\n\n");

// CI job - gunbc-ci handles codegen internally via the prep node
yaml.push_str(&format!("{}:\n", config.workflow_name));
yaml.push_str(" stage: ci\n");
yaml.push_str(" script:\n");
yaml.push_str(&format!(" - {} --release\n", config.tool.command()));

yaml.push_str("\n\nimage: rust:latest\n\n");

// Variables — derived from cargo env + manual overrides
yaml_block(&mut yaml, "variables:", &config.all_env(), |(k, v)| format!(" {}: \"{}\"", k, v));

yaml.push_str("stages:\n - ci\n\n");

// Cache — GitLab uses CI_COMMIT_REF_SLUG and relative paths
yaml.push_str("cache:\n key: cargo-${CI_COMMIT_REF_SLUG}\n paths:\n - .cargo/\n - target/\n\n");

// CI job
yaml.push_str(&format!(
"{}:\n stage: ci\n script:\n - {} --release\n",
config.workflow_name, config.tool.command(),
));

yaml
}

Expand Down
17 changes: 17 additions & 0 deletions core/exec/src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,16 @@ fn execute_flat<T: Executable>(
}
};

// Mask any secret values in CI context so that CI runners
// (GitHub Actions, GitLab CI) redact them from all output.
if let Some(ref mut ci) = ci_ctx {
for value in outputs.values() {
if let Value::Secret(s) = value {
ci.mask(s.expose());
}
}
}

node_outputs.insert(node_id.0.clone(), outputs.clone());
let entry = LogEntry {
node_id: node_id.0.clone(),
Expand Down Expand Up @@ -531,9 +541,16 @@ fn execute_flat<T: Executable>(
///
/// Used inside CI groups so that node outputs appear within the
/// collapsible section rather than in a flat summary after all groups.
///
/// Secret values are always redacted — they print as `***` regardless
/// of context. This is the last line of defense against credential leaks.
fn print_log_entry(entry: &LogEntry) {
for (port, value) in &entry.outputs {
match value {
Value::Secret(_) => {
// Always redact secrets — never print actual values
println!(" {port}: ***");
}
Value::Str(s) => {
if port.ends_with("stderr") || port.ends_with("stdout") {
if !s.is_empty() {
Expand Down
Loading