CI #426
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| permissions: | |
| contents: write | |
| checks: write | |
| pull-requests: write | |
| on: | |
| push: | |
| branches: [ main ] | |
| workflow_dispatch: # Allow manual trigger | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_VERSION: "1.94.0" | |
| CARGO_ENCODED_RUSTFLAGS: "" | |
| CARGO_INCREMENTAL: "0" | |
| # Keep CI test binaries small. Full debuginfo has filled the runner disk | |
| # (ENOSPC creating /tmp RocksDB fixtures near the end of the suite). | |
| CARGO_PROFILE_DEV_DEBUG: "line-tables-only" | |
| CARGO_PROFILE_TEST_DEBUG: "line-tables-only" | |
| RUSTC_WRAPPER: "" | |
| CARGO_BUILD_RUSTC_WRAPPER: "" | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| # CI has no pre-started dev server; e2e tests auto-start an ephemeral server per suite. | |
| KALAMDB_SERVER_TYPE: fresh | |
| KALAMDB_ROOT_PASSWORD: kalamdb123 | |
| # Durable fresh-server stdout/stderr for post-mortem debugging. | |
| KALAMDB_AUTO_TEST_SERVER_LOG: ${{ github.workspace }}/ci-artifacts/fresh-server.log | |
| jobs: | |
| fmt: | |
| name: Format Check | |
| runs-on: ubuntu-latest | |
| if: false # Only run on manual trigger | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_VERSION }} | |
| components: rustfmt | |
| - name: Run rustfmt | |
| run: cargo fmt --all -- --check | |
| clippy: | |
| name: Clippy Check | |
| runs-on: ubuntu-latest | |
| if: false # Only run on manual trigger | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_VERSION }} | |
| components: clippy | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends \ | |
| clang \ | |
| libclang-dev \ | |
| pkg-config \ | |
| libssl-dev | |
| - name: Cache cargo registry | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cargo/registry | |
| key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo-registry- | |
| - name: Cache cargo index | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cargo/git | |
| key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo-index- | |
| - name: Cache cargo build | |
| uses: actions/cache@v5 | |
| with: | |
| path: target | |
| key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo-build-target- | |
| - name: Run clippy | |
| run: cargo clippy --all-targets --all-features -- -D warnings | |
| # test: | |
| # name: Test Suite | |
| # runs-on: ubuntu-latest | |
| # steps: | |
| # - name: Checkout code | |
| # uses: actions/checkout@v6 | |
| # - name: Install Rust toolchain | |
| # uses: dtolnay/rust-toolchain@stable | |
| # with: | |
| # toolchain: ${{ env.RUST_VERSION }} | |
| # - name: Install system dependencies | |
| # run: | | |
| # sudo apt-get update | |
| # sudo apt-get install -y --no-install-recommends \ | |
| # clang \ | |
| # libclang-dev \ | |
| # pkg-config \ | |
| # libssl-dev | |
| # - name: Cache cargo registry | |
| # uses: actions/cache@v5 | |
| # with: | |
| # path: ~/.cargo/registry | |
| # key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} | |
| # restore-keys: | | |
| # ${{ runner.os }}-cargo-registry- | |
| # - name: Cache cargo index | |
| # uses: actions/cache@v5 | |
| # with: | |
| # path: ~/.cargo/git | |
| # key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} | |
| # restore-keys: | | |
| # ${{ runner.os }}-cargo-index- | |
| # - name: Cache cargo build | |
| # uses: actions/cache@v5 | |
| # with: | |
| # path: target | |
| # key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} | |
| # restore-keys: | | |
| # ${{ runner.os }}-cargo-build-target- | |
| # - name: Run tests | |
| # run: cargo test --workspace --all-features | |
| test: | |
| name: Test Suite | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Free disk space | |
| run: | | |
| set -euo pipefail | |
| echo "==> Disk before cleanup" | |
| df -h | |
| sudo rm -rf \ | |
| /usr/share/dotnet \ | |
| /usr/local/lib/android \ | |
| /opt/ghc \ | |
| /opt/hostedtoolcache/CodeQL \ | |
| /usr/share/swift \ | |
| /usr/local/share/powershell \ | |
| /usr/local/.ghcup | |
| sudo docker system prune -af || true | |
| echo "==> Disk after cleanup" | |
| df -h | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_VERSION }} | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends \ | |
| clang \ | |
| libclang-dev \ | |
| pkg-config \ | |
| libssl-dev | |
| - name: Cache Rust | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: ci-test-suite | |
| - name: Install cargo-nextest | |
| run: | | |
| curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin | |
| - name: Build kalam CLI binary for e2e tests | |
| run: cargo build -p kalam-cli --bin kalam | |
| - name: Report disk space | |
| run: | | |
| df -h | |
| du -sh target "${CARGO_HOME:-$HOME/.cargo}" 2>/dev/null || true | |
| - name: Serialization boundary | |
| run: python3 scripts/check-serialization-boundary.py --fail | |
| # Default features omit embedded-ui, AWS, Dex/testcontainers, and tokio-postgres. | |
| # Enable kalam-cli-e2e/oidc + kalamdb-server/oidc locally (or via cli/run-tests.sh) | |
| # when running Dex OIDC tests. MinIO: KALAMDB_SERVER_FEATURES=cloud-aws. | |
| - name: Run tests (nextest) | |
| id: nextest | |
| run: | | |
| mkdir -p ci-artifacts | |
| cargo nextest run \ | |
| --profile ci \ | |
| --workspace \ | |
| --exclude kalam-pg-extension \ | |
| --exclude kalam-link-wasm \ | |
| --exclude kalam-consumer-wasm \ | |
| --exclude kalam-link-dart \ | |
| --exclude kalamdb-python \ | |
| --exclude quickstart \ | |
| --failure-output immediate-final | |
| continue-on-error: true | |
| - name: Report disk space after tests | |
| if: always() | |
| run: | | |
| df -h | |
| du -sh target "${TMPDIR:-/tmp}" 2>/dev/null || true | |
| - name: Collect fresh server log | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| mkdir -p ci-artifacts | |
| LOG_PATH="${KALAMDB_AUTO_TEST_SERVER_LOG:-ci-artifacts/fresh-server.log}" | |
| PRESERVED="${TMPDIR:-/tmp}/kalamdb_auto_test_server/last-server.log" | |
| if [[ ! -s "$LOG_PATH" && -s "$PRESERVED" ]]; then | |
| cp "$PRESERVED" "$LOG_PATH" | |
| fi | |
| if [[ -s "$LOG_PATH" ]]; then | |
| echo "Fresh server log: $LOG_PATH ($(wc -c < "$LOG_PATH") bytes)" | |
| echo "=== fresh-server.log (last 120 lines) ===" | |
| tail -n 120 "$LOG_PATH" || true | |
| else | |
| echo "No fresh-server.log collected at $LOG_PATH" | |
| ls -la "${TMPDIR:-/tmp}/kalamdb_auto_test_server" 2>/dev/null || true | |
| fi | |
| - name: Upload fresh server log | |
| if: always() | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: fresh-server-log | |
| path: ci-artifacts/fresh-server.log | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| - name: Normalize JUnit suite durations for test-reporter | |
| if: always() && hashFiles('target/nextest/ci/junit.xml') != '' | |
| run: | | |
| python3 << 'PYTHON_SCRIPT' | |
| import math | |
| import xml.etree.ElementTree as ET | |
| junit_path = "target/nextest/ci/junit.xml" | |
| def parse_time(raw): | |
| if raw is None: | |
| return None | |
| try: | |
| value = float(raw) | |
| except ValueError: | |
| return None | |
| return value if math.isfinite(value) else None | |
| def format_time(value): | |
| return f"{value:.6f}".rstrip("0").rstrip(".") or "0" | |
| tree = ET.parse(junit_path) | |
| root = tree.getroot() | |
| suites = [root] if root.tag == "testsuite" else root.findall("./testsuite") | |
| suite_times = [] | |
| changed = False | |
| for testsuite in suites: | |
| suite_time = parse_time(testsuite.get("time")) | |
| if suite_time is None: | |
| suite_time = 0.0 | |
| for testcase in testsuite.findall("./testcase"): | |
| case_time = parse_time(testcase.get("time")) | |
| if case_time is not None: | |
| suite_time += case_time | |
| for attempt in testcase: | |
| attempt_time = parse_time(attempt.get("time")) | |
| if attempt_time is not None: | |
| suite_time += attempt_time | |
| testsuite.set("time", format_time(suite_time)) | |
| changed = True | |
| suite_times.append(suite_time) | |
| report_time = parse_time(root.get("time")) | |
| if report_time is None and suite_times: | |
| root.set("time", format_time(sum(suite_times))) | |
| changed = True | |
| if changed: | |
| tree.write(junit_path, encoding="utf-8", xml_declaration=True) | |
| print(f"Normalized JUnit durations in {junit_path}") | |
| else: | |
| print(f"JUnit durations already present in {junit_path}") | |
| PYTHON_SCRIPT | |
| - name: Parse test results and generate badge | |
| if: always() && hashFiles('target/nextest/ci/junit.xml') != '' | |
| run: | | |
| JUNIT_FILE="target/nextest/ci/junit.xml" | |
| if [ ! -f "$JUNIT_FILE" ]; then | |
| echo "Error: JUnit XML report not found at $JUNIT_FILE" | |
| exit 1 | |
| fi | |
| # Parse JUnit XML using Python (available in ubuntu-latest) | |
| eval $(python3 << 'PYTHON_SCRIPT' | |
| import xml.etree.ElementTree as ET | |
| import sys | |
| try: | |
| tree = ET.parse("target/nextest/ci/junit.xml") | |
| root = tree.getroot() | |
| total = 0 | |
| failures = 0 | |
| errors = 0 | |
| # Sum up all testsuite elements | |
| for testsuite in root.findall('.//testsuite'): | |
| total += int(testsuite.get('tests', 0)) | |
| failures += int(testsuite.get('failures', 0)) | |
| errors += int(testsuite.get('errors', 0)) | |
| failed = failures + errors | |
| passed = total - failed | |
| print(f"TOTAL={total}") | |
| print(f"PASSED={passed}") | |
| print(f"FAILED={failed}") | |
| if total == 0: | |
| print("Error: No tests found in JUnit report", file=sys.stderr) | |
| sys.exit(1) | |
| except Exception as e: | |
| print(f"Error parsing JUnit XML: {e}", file=sys.stderr) | |
| sys.exit(1) | |
| PYTHON_SCRIPT | |
| ) | |
| echo "Tests: $PASSED passed, $FAILED failed (total: $TOTAL)" | |
| # Determine badge color and message | |
| if [ "$FAILED" -eq "0" ]; then | |
| COLOR="brightgreen" | |
| MESSAGE="$PASSED/$TOTAL passed" | |
| else | |
| COLOR="red" | |
| MESSAGE="$PASSED/$TOTAL passed, $FAILED failed" | |
| fi | |
| echo "Badge: $MESSAGE ($COLOR)" | |
| # Generate badge JSON | |
| mkdir -p .github/badges | |
| cat > .github/badges/tests.json << EOF | |
| { | |
| "schemaVersion": 1, | |
| "label": "overall tests ($TOTAL)", | |
| "message": "$MESSAGE", | |
| "color": "$COLOR" | |
| } | |
| EOF | |
| cat .github/badges/tests.json | |
| # Fail the step if tests failed | |
| if [ "$FAILED" -ne "0" ]; then | |
| exit 1 | |
| fi | |
| - name: Fail if JUnit report is missing after a successful nextest run | |
| if: always() && steps.nextest.outcome == 'success' && hashFiles('target/nextest/ci/junit.xml') == '' | |
| run: | | |
| echo "Error: JUnit XML report not found at target/nextest/ci/junit.xml" | |
| exit 1 | |
| - name: Publish test report | |
| if: always() && hashFiles('target/nextest/ci/junit.xml') != '' | |
| uses: dorny/test-reporter@v1 | |
| with: | |
| name: nextest | |
| path: target/nextest/ci/junit.xml | |
| reporter: java-junit | |
| - name: Fail workflow if nextest did not complete successfully | |
| if: always() && steps.nextest.outcome != 'success' | |
| run: | | |
| echo "nextest failed before completing the workspace suite" | |
| exit 1 | |
| - name: Commit badge | |
| if: github.ref == 'refs/heads/main' && github.event_name == 'push' | |
| run: | | |
| set -euo pipefail | |
| git config --local user.email "github-actions[bot]@users.noreply.github.com" | |
| git config --local user.name "github-actions[bot]" | |
| git checkout -B ci-test-badge-update | |
| git add .github/badges/tests.json | |
| if git diff --staged --quiet; then | |
| echo "Test badge file already up to date" | |
| exit 0 | |
| fi | |
| git commit -m "Update test badge [skip ci]" | |
| git push origin HEAD:main | |
| license_compliance: | |
| name: License Compliance | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_VERSION }} | |
| - name: Setup Node 24 | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| - name: Install cargo-license | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cargo install cargo-license --locked | |
| - name: Generate Rust runtime license report | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p target/license | |
| cargo license -j --avoid-dev-deps --avoid-build-deps -o target/license/rust-runtime-licenses.json | |
| - name: Install UI dependencies | |
| shell: bash | |
| working-directory: ui | |
| run: | | |
| set -euo pipefail | |
| npm install --no-audit --no-fund | |
| - name: Generate UI production license report | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cd ui | |
| npx --yes license-checker --production --excludePrivatePackages --json > ../target/license/ui-licenses.json | |
| - name: Install SDK dependencies | |
| shell: bash | |
| working-directory: link/sdks/typescript/client | |
| run: | | |
| set -euo pipefail | |
| npm install --no-audit --no-fund | |
| - name: Generate SDK production license report | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cd link/sdks/typescript/client | |
| npx --yes license-checker --production --json > "$GITHUB_WORKSPACE/target/license/sdk-licenses.json" | |
| - name: Install npm CLI dependencies | |
| shell: bash | |
| working-directory: link/sdks/typescript/cli | |
| run: | | |
| set -euo pipefail | |
| npm install --ignore-scripts --no-audit --no-fund | |
| - name: Generate npm CLI production license report | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cd link/sdks/typescript/cli | |
| npx --yes license-checker --production --json > "$GITHUB_WORKSPACE/target/license/npm-cli-licenses.json" | |
| - name: Validate licenses and generate third-party report | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python3 scripts/license_compliance.py \ | |
| --policy cargo-license-check.toml \ | |
| --rust-report target/license/rust-runtime-licenses.json \ | |
| --ui-report target/license/ui-licenses.json \ | |
| --sdk-report target/license/sdk-licenses.json \ | |
| --cli-report target/license/npm-cli-licenses.json \ | |
| --output target/license/THIRD_PARTY_LICENSES.md | |
| - name: Upload license reports | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: license-reports | |
| path: target/license/ | |
| if-no-files-found: error |