diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66d3cd06..8a2cf7d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,37 +52,33 @@ jobs: # Native mode smoke test — build from source & start natively # ────────────────────────────────────────────── native-smoke-test: + needs: plano-cli-tests runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.12" - - - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh - - name: Install Rust uses: dtolnay/rust-toolchain@stable with: targets: wasm32-wasip1 - - name: Install planoai CLI - working-directory: ./cli - run: | - uv sync - uv tool install . + - name: Download planoai binary + uses: actions/download-artifact@v6 + with: + name: planoai-binary + path: crates/target/release/ + + - name: Make binary executable + run: chmod +x crates/target/release/planoai - name: Build native binaries - run: planoai build + run: crates/target/release/planoai build - name: Start plano natively env: OPENAI_API_KEY: test-key-not-used - run: planoai up tests/e2e/config_native_smoke.yaml + run: crates/target/release/planoai up tests/e2e/config_native_smoke.yaml - name: Health check run: | @@ -100,7 +96,7 @@ jobs: - name: Stop plano if: always() - run: planoai down || true + run: crates/target/release/planoai down || true # ────────────────────────────────────────────── # Single Docker build — shared by all downstream jobs @@ -147,21 +143,25 @@ jobs: # Validate plano config # ────────────────────────────────────────────── validate-config: + needs: plano-cli-tests runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 - - name: Set up Python - uses: actions/setup-python@v6 + - name: Download planoai binary + uses: actions/download-artifact@v6 with: - python-version: "3.14" + name: planoai-binary + path: crates/target/release/ - - name: Install planoai - run: pip install -e ./cli + - name: Make binary executable + run: chmod +x crates/target/release/planoai - name: Validate plano config - run: bash config/validate_plano_config.sh + run: | + export PATH="$PWD/crates/target/release:$PATH" + bash config/validate_plano_config.sh # ────────────────────────────────────────────── # Docker security scan (Trivy) diff --git a/config/validate_plano_config.sh b/config/validate_plano_config.sh index 572ac2ec..e65bf5c2 100644 --- a/config/validate_plano_config.sh +++ b/config/validate_plano_config.sh @@ -8,13 +8,7 @@ for file in $(find . -name config.yaml -o -name plano_config_full_reference.yaml rendered_file="$(pwd)/${file}_rendered" touch "$rendered_file" - PLANO_CONFIG_FILE="$(pwd)/${file}" \ - PLANO_CONFIG_SCHEMA_FILE="${SCRIPT_DIR}/plano_config_schema.yaml" \ - TEMPLATE_ROOT="${SCRIPT_DIR}" \ - ENVOY_CONFIG_TEMPLATE_FILE="envoy.template.yaml" \ - PLANO_CONFIG_FILE_RENDERED="$rendered_file" \ - ENVOY_CONFIG_FILE_RENDERED="/dev/null" \ - python -m planoai.config_generator 2>&1 > /dev/null + planoai validate "$(pwd)/${file}" 2>&1 > /dev/null if [ $? -ne 0 ]; then echo "Validation failed for $file" diff --git a/crates/plano-cli/src/config/generator.rs b/crates/plano-cli/src/config/generator.rs index a06e97ba..4ef375fb 100644 --- a/crates/plano-cli/src/config/generator.rs +++ b/crates/plano-cli/src/config/generator.rs @@ -266,6 +266,10 @@ pub fn validate_and_render( // Override inferred clusters with endpoints for (name, details) in &endpoints { let mut cluster = details.clone(); + // Ensure protocol is always set + if cluster.get("protocol").is_none() { + cluster["protocol"] = json!("https"); + } if cluster.get("port").is_none() { let ep = cluster .get("endpoint") @@ -279,6 +283,10 @@ pub fn validate_and_render( cluster["endpoint"] = json!(endpoint); cluster["port"] = json!(port); } + // Ensure connect_timeout is set + if cluster.get("connect_timeout").is_none() { + cluster["connect_timeout"] = json!("5s"); + } inferred_clusters.insert(name.clone(), cluster); } @@ -702,6 +710,29 @@ pub fn validate_and_render( let mut tera = tera::Tera::default(); let template_content = std::fs::read_to_string(template_path)?; + // Convert Jinja2 syntax to Tera syntax + // indent(N) → indent(width=N) + let template_content = regex::Regex::new(r"indent\((\d+)\)") + .unwrap() + .replace_all(&template_content, "indent(width=$1)") + .to_string(); + // var.split(":") | first → var | split(pat=":") | first + let template_content = regex::Regex::new(r#"(\w+)\.split\("([^"]+)"\)"#) + .unwrap() + .replace_all(&template_content, r#"$1 | split(pat="$2")"#) + .to_string(); + // default('value') → default(value='value') + let template_content = regex::Regex::new(r"default\('([^']+)'\)") + .unwrap() + .replace_all(&template_content, "default(value='$1')") + .to_string(); + // replace(" ", "_") → replace(from=" ", to="_") + let template_content = regex::Regex::new(r#"replace\("([^"]*)",\s*"([^"]*)"\)"#) + .unwrap() + .replace_all(&template_content, r#"replace(from="$1", to="$2")"#) + .to_string(); + // dict.items() → dict (Tera iterates dicts directly) + let template_content = template_content.replace(".items()", ""); tera.add_raw_template(template_filename, &template_content)?; let mut context = tera::Context::new();