name: CI on: push: branches: [master, develop] pull_request: branches: [master, develop*] vars: docker_prefix: "http://harbor.cleverthis.com/docker/" env: UV_VERSION: "0.8.0" PYTHON_VERSION: "3.13" NOX_DEFAULT_VENV_BACKEND: "uv" jobs: lint: runs-on: docker container: image: ${{vars.docker_prefix}}python:3.13-slim steps: - name: Install Node.js (required by actions/checkout) run: | apt-get update && apt-get install -y -qq nodejs && rm -rf /var/lib/apt/lists/* - uses: actions/checkout@v4 - name: Install uv and nox run: | pip install -q uv==${{ env.UV_VERSION }} nox - name: Cache uv packages uses: actions/cache@v3 with: path: ~/.cache/uv key: uv-${{ hashFiles('pyproject.toml') }} restore-keys: | uv- - name: Run lint via nox run: | mkdir -p build nox -s lint 2>&1 | tee build/nox-lint-output.log env: NOX_DEFAULT_VENV_BACKEND: uv - name: Run format check via nox run: | nox -s format -- --check 2>&1 | tee -a build/nox-lint-output.log env: NOX_DEFAULT_VENV_BACKEND: uv - name: Upload lint log artifact if: always() uses: actions/upload-artifact@v3 with: name: ci-logs-lint path: build/nox-lint-output.log retention-days: 30 typecheck: runs-on: docker container: image: ${{vars.docker_prefix}}python:3.13-slim steps: - name: Install Node.js (required by actions/checkout) run: | apt-get update && apt-get install -y -qq nodejs && rm -rf /var/lib/apt/lists/* - uses: actions/checkout@v4 - name: Install uv and nox run: | pip install -q uv==${{ env.UV_VERSION }} nox - name: Cache uv packages uses: actions/cache@v3 with: path: ~/.cache/uv key: uv-${{ hashFiles('pyproject.toml') }} restore-keys: | uv- - name: Run typecheck via nox run: | mkdir -p build nox -s typecheck 2>&1 | tee build/nox-typecheck-output.log env: NOX_DEFAULT_VENV_BACKEND: uv - name: Upload typecheck log artifact if: always() uses: actions/upload-artifact@v3 with: name: ci-logs-typecheck path: build/nox-typecheck-output.log retention-days: 30 security: runs-on: docker container: image: ${{vars.docker_prefix}}python:3.13-slim steps: - name: Install Node.js (required by actions/checkout) run: | apt-get update && apt-get install -y -qq nodejs && rm -rf /var/lib/apt/lists/* - uses: actions/checkout@v4 - name: Install uv and nox run: | pip install -q uv==${{ env.UV_VERSION }} nox - name: Cache uv packages uses: actions/cache@v3 with: path: ~/.cache/uv key: uv-${{ hashFiles('pyproject.toml') }} restore-keys: | uv- - name: Run security scan via nox run: | mkdir -p build nox -s security_scan 2>&1 | tee build/nox-security-output.log env: NOX_DEFAULT_VENV_BACKEND: uv - name: Run dead code detection via nox run: | nox -s dead_code 2>&1 | tee -a build/nox-security-output.log env: NOX_DEFAULT_VENV_BACKEND: uv - name: Upload security log artifact if: always() uses: actions/upload-artifact@v3 with: name: ci-logs-security path: build/nox-security-output.log retention-days: 30 quality: runs-on: docker container: image: ${{vars.docker_prefix}}python:3.13-slim steps: - name: Install Node.js (required by actions/checkout) run: | apt-get update && apt-get install -y -qq nodejs && rm -rf /var/lib/apt/lists/* - uses: actions/checkout@v4 - name: Install uv and nox run: | pip install -q uv==${{ env.UV_VERSION }} nox - name: Cache uv packages uses: actions/cache@v3 with: path: ~/.cache/uv key: uv-${{ hashFiles('pyproject.toml') }} restore-keys: | uv- - name: Run complexity check via nox run: | mkdir -p build nox -s complexity 2>&1 | tee build/nox-quality-output.log env: NOX_DEFAULT_VENV_BACKEND: uv - name: Upload quality log artifact if: always() uses: actions/upload-artifact@v3 with: name: ci-logs-quality path: build/nox-quality-output.log retention-days: 30 unit_tests: runs-on: docker container: image: ${{vars.docker_prefix}}python:3.13-slim steps: - name: Install system dependencies (nodejs for checkout, git for merge tests, curl/tar for Helm) run: | apt-get update && apt-get install -y -qq nodejs git curl tar && rm -rf /var/lib/apt/lists/* - name: Install Helm CLI run: | HELM_VERSION="v3.16.4" ARCH="amd64" HELM_TARBALL="helm-${HELM_VERSION}-linux-${ARCH}.tar.gz" curl -fsSL "https://get.helm.sh/${HELM_TARBALL}" -o "/tmp/${HELM_TARBALL}" curl -fsSL "https://get.helm.sh/${HELM_TARBALL}.sha256sum" -o /tmp/helm.sha256sum cd /tmp && sha256sum -c helm.sha256sum tar -xzf "/tmp/${HELM_TARBALL}" -C /tmp mv /tmp/linux-${ARCH}/helm /usr/local/bin/helm chmod +x /usr/local/bin/helm helm version --short - uses: actions/checkout@v4 - name: Install uv and nox run: | pip install -q uv==${{ env.UV_VERSION }} nox - name: Cache uv packages uses: actions/cache@v3 with: path: ~/.cache/uv key: uv-${{ hashFiles('pyproject.toml') }} restore-keys: | uv- - name: Run unit tests via nox run: | mkdir -p build nox -s unit_tests 2>&1 | tee build/nox-unit-tests-output.log env: NOX_DEFAULT_VENV_BACKEND: uv - name: Upload unit tests log artifact if: always() uses: actions/upload-artifact@v3 with: name: ci-logs-unit-tests path: build/nox-unit-tests-output.log retention-days: 30 integration_tests: runs-on: docker container: image: ${{vars.docker_prefix}}python:3.13-slim steps: - name: Install system dependencies (nodejs for checkout, git for integration tests, curl/tar for Helm) run: | apt-get update && apt-get install -y -qq nodejs git curl tar && rm -rf /var/lib/apt/lists/* - name: Install Helm CLI run: | HELM_VERSION="v3.16.4" ARCH="amd64" HELM_TARBALL="helm-${HELM_VERSION}-linux-${ARCH}.tar.gz" curl -fsSL "https://get.helm.sh/${HELM_TARBALL}" -o "/tmp/${HELM_TARBALL}" curl -fsSL "https://get.helm.sh/${HELM_TARBALL}.sha256sum" -o /tmp/helm.sha256sum cd /tmp && sha256sum -c helm.sha256sum tar -xzf "/tmp/${HELM_TARBALL}" -C /tmp mv /tmp/linux-${ARCH}/helm /usr/local/bin/helm chmod +x /usr/local/bin/helm helm version --short - uses: actions/checkout@v4 - name: Install uv and nox run: | pip install -q uv==${{ env.UV_VERSION }} nox - name: Cache uv packages uses: actions/cache@v3 with: path: ~/.cache/uv key: uv-${{ hashFiles('pyproject.toml') }} restore-keys: | uv- - name: Run integration tests via nox run: | mkdir -p build nox -s integration_tests 2>&1 | tee build/nox-integration-tests-output.log env: NOX_DEFAULT_VENV_BACKEND: uv CLEVERAGENTS_REQUIRE_HELM_RENDER_ASSERTIONS: "true" # LLM API keys required for Robot Framework integration tests. # These secrets must be configured in Forgejo UI: # Repository Settings > Actions > Secrets # See docs/development/ci-cd.md for details. ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: Upload integration tests log artifact if: always() uses: actions/upload-artifact@v3 with: name: ci-logs-integration-tests path: build/nox-integration-tests-output.log retention-days: 30 e2e_tests: runs-on: docker timeout-minutes: 45 container: image: ${{vars.docker_prefix}}python:3.13-slim steps: - name: Install system dependencies (nodejs for checkout, git for E2E tests) run: | apt-get update && apt-get install -y -qq nodejs git && rm -rf /var/lib/apt/lists/* - uses: actions/checkout@v4 - name: Install uv and nox run: | pip install -q uv==${{ env.UV_VERSION }} nox - name: Cache uv packages uses: actions/cache@v3 with: path: ~/.cache/uv key: uv-${{ hashFiles('pyproject.toml') }} restore-keys: | uv- - name: Run E2E tests via nox run: | mkdir -p build nox -s e2e_tests 2>&1 | tee build/nox-e2e-tests-output.log env: NOX_DEFAULT_VENV_BACKEND: uv # Run E2E suites in parallel via pabot. 4 workers keeps # wall-clock time well under the 45-minute timeout while # staying within the memory budget of the docker runner. TEST_PROCESSES: "4" ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - name: Upload E2E tests log artifact if: failure() uses: actions/upload-artifact@v3 with: name: ci-logs-e2e-tests path: | build/nox-e2e-tests-output.log build/reports/robot-e2e/ retention-days: 30 coverage: runs-on: docker container: image: ${{vars.docker_prefix}}python:3.13-slim # unit_tests is included so coverage only runs after tests pass, # preventing misleading results when tests are still in-flight or failing. needs: [lint, typecheck, security, quality, unit_tests] steps: - name: Install system dependencies (nodejs for checkout, git for merge tests) run: | apt-get update && apt-get install -y -qq nodejs git && rm -rf /var/lib/apt/lists/* - uses: actions/checkout@v4 - name: Install uv and nox run: | pip install -q uv==${{ env.UV_VERSION }} nox - name: Cache uv packages uses: actions/cache@v3 with: path: ~/.cache/uv key: uv-${{ hashFiles('pyproject.toml') }} restore-keys: | uv- - name: Run coverage report via nox (fail-under 97%) id: coverage run: | mkdir -p build nox -s coverage_report 2>&1 | tee build/nox-coverage-output.log # Extract the single-line CI summary from nox output grep -E '^(nox > )?COVERAGE (OK|FAILED):' build/nox-coverage-output.log || true env: NOX_DEFAULT_VENV_BACKEND: uv - name: Surface coverage summary if: always() run: | if [ -f build/coverage.json ]; then python3 -c " import json, sys with open('build/coverage.json') as f: data = json.load(f) summary = data.get('summary') or data.get('totals') or {} pct = round(summary.get('percent_covered', 0), 1) threshold = 50 # Temporarily lowered; see issues #4183 and #4184 if pct >= threshold: print(f'COVERAGE OK: {pct}% (threshold: {threshold}%)') else: print(f'COVERAGE FAILED: {pct}% < {threshold}% threshold') sys.exit(1) " else echo "COVERAGE FAILED: no coverage data generated" exit 1 fi - name: Upload coverage log artifact if: always() uses: actions/upload-artifact@v3 with: name: ci-logs-coverage path: build/nox-coverage-output.log retention-days: 30 - name: Upload coverage artifacts if: always() uses: actions/upload-artifact@v3 with: name: coverage-reports path: | build/coverage.xml build/coverage.json build/htmlcov/ retention-days: 30 build: runs-on: docker container: image: ${{vars.docker_prefix}}python:3.13-slim steps: - name: Install Node.js (required by actions/checkout) run: | apt-get update && apt-get install -y -qq nodejs && rm -rf /var/lib/apt/lists/* - uses: actions/checkout@v4 - name: Install uv and nox run: | pip install -q uv==${{ env.UV_VERSION }} nox - name: Cache uv packages uses: actions/cache@v3 with: path: ~/.cache/uv key: uv-${{ hashFiles('pyproject.toml') }} restore-keys: | uv- - name: Build wheel via nox run: | nox -s build env: NOX_DEFAULT_VENV_BACKEND: uv docker: needs: [lint, typecheck, security, quality, unit_tests] runs-on: docker container: image: docker:dind options: --privileged steps: - name: Start Docker daemon and install dependencies run: | dockerd & apk add --no-cache git nodejs for i in $(seq 1 30); do docker info >/dev/null 2>&1 && break || sleep 1; done - uses: actions/checkout@v4 - name: Build Docker image (CLI) run: | docker build -t cleverernie:test . - name: Test Docker image (CLI) run: | docker run --rm cleverernie:test --version - name: Build Docker image (Server) run: | docker build -f Dockerfile.server -t cleveragents-server:test . helm: runs-on: docker container: image: ${{vars.docker_prefix}}python:3.13-slim steps: - name: Install system dependencies (nodejs for checkout, curl for Helm and kubeconform) run: | apt-get update && apt-get install -y -qq nodejs curl tar && rm -rf /var/lib/apt/lists/* - uses: actions/checkout@v4 - name: Install Helm CLI run: | HELM_VERSION="v3.16.4" ARCH="amd64" HELM_TARBALL="helm-${HELM_VERSION}-linux-${ARCH}.tar.gz" curl -fsSL "https://get.helm.sh/${HELM_TARBALL}" -o "/tmp/${HELM_TARBALL}" curl -fsSL "https://get.helm.sh/${HELM_TARBALL}.sha256sum" -o /tmp/helm.sha256sum cd /tmp && sha256sum -c helm.sha256sum tar -xzf "/tmp/${HELM_TARBALL}" -C /tmp mv /tmp/linux-${ARCH}/helm /usr/local/bin/helm chmod +x /usr/local/bin/helm helm version --short - name: Install kubeconform run: | KUBECONFORM_VERSION="v0.7.0" ARCH="amd64" KUBECONFORM_TARBALL="kubeconform-linux-${ARCH}.tar.gz" curl -fsSL "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/${KUBECONFORM_TARBALL}" \ -o "/tmp/${KUBECONFORM_TARBALL}" tar -xzf "/tmp/${KUBECONFORM_TARBALL}" -C /tmp mv /tmp/kubeconform /usr/local/bin/kubeconform chmod +x /usr/local/bin/kubeconform kubeconform -v - name: Build Helm chart dependencies run: | helm dependency build ./k8s - name: Helm lint chart run: | helm lint ./k8s \ --set database.url="postgresql+asyncpg://user:pass@db-host:5432/cleveragents" - name: Helm template smoke render run: | helm template cleveragents ./k8s \ --set database.url="postgresql+asyncpg://user:pass@db-host:5432/cleveragents" >/tmp/rendered.yaml test -s /tmp/rendered.yaml - name: Validate rendered manifests with kubeconform run: | kubeconform \ -strict \ -ignore-missing-schemas \ -kubernetes-version 1.29.0 \ -summary \ /tmp/rendered.yaml push-validation: # Validates that the CI runner can authenticate and push to the repository. # Root cause of the push failure: actions/checkout@v4 was not configured with # explicit push credentials (token + persist-credentials), and no git user # config (name/email) was set — both are required for any push operation. runs-on: docker container: image: ${{vars.docker_prefix}}python:3.13-slim steps: - name: Install system dependencies (nodejs for checkout, git for push validation) run: | apt-get update && apt-get install -y -qq nodejs git curl && rm -rf /var/lib/apt/lists/* - name: Checkout with explicit write credentials uses: actions/checkout@v4 with: # Pass the Forgejo token explicitly so the credential helper is # configured for HTTPS push operations. Without this, the default # checkout may only have read access and push will fail with a # 403 or authentication error. token: ${{ secrets.FORGEJO_TOKEN }} persist-credentials: true - name: Configure git user for CI operations run: | # Required for any git commit or push operation in CI. # Uses a bot identity to distinguish CI-generated commits from # human commits. Without this, git push fails with: # "Author identity unknown — please tell me who you are." git config user.name "CleverAgents CI" git config user.email "ci-bot@cleverthis.com" - name: Verify HTTPS credential helper is configured run: | # Confirm that the credential helper set up by actions/checkout # is active. This ensures HTTPS push operations will authenticate # correctly without prompting for a password. echo "=== Git credential configuration ===" git config --list | grep -E "credential|url" || echo "WARNING: No credential helper found" echo "=== Remote URL ===" git remote get-url origin echo "=== Credential helper check ===" if git config credential.helper > /dev/null 2>&1; then echo "OK: Credential helper is configured: $(git config credential.helper)" else echo "WARNING: No credential helper configured — push may fail" fi - name: Smoke-test push access via API # Validates write permission using the Forgejo API before attempting # any real push. This catches credential issues early with a clear # error message rather than a cryptic git error. env: FORGEJO_URL: ${{ secrets.FORGEJO_URL }} FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} run: | REPO="${{ forgejo.repository }}" API_URL="${FORGEJO_URL}/api/v1/repos/${REPO}" echo "=== Testing repository API access ===" HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "${API_URL}") if [ "${HTTP_STATUS}" != "200" ]; then echo "ERROR: FORGEJO_TOKEN cannot access repository API (HTTP ${HTTP_STATUS})." echo "Ensure FORGEJO_TOKEN is set in Repository Settings > Actions > Secrets" echo "and that the token has repository (write) scope." exit 1 fi PUSH_ALLOWED=$(curl -s \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "${API_URL}" | python3 -c "import sys,json; d=json.load(sys.stdin); print(str(d.get('permissions',{}).get('push',False)).lower())") if [ "${PUSH_ALLOWED}" != "true" ]; then echo "ERROR: FORGEJO_TOKEN does not have push (write) permission." echo "Grant the token Contents: Write permission or use a token with full repository scope." exit 1 fi echo "OK: Push access verified -- FORGEJO_TOKEN has write permission on ${REPO}" echo "=== Push access smoke-test passed ===" status-check: if: always() needs: [lint, typecheck, security, quality, unit_tests, integration_tests, e2e_tests, coverage, build, docker, helm, push-validation] runs-on: docker container: image: ${{vars.docker_prefix}}python:3.13-slim steps: - name: Check required job results run: | echo "lint: ${{ needs.lint.result }}" echo "typecheck: ${{ needs.typecheck.result }}" echo "security: ${{ needs.security.result }}" echo "quality: ${{ needs.quality.result }}" echo "unit_tests: ${{ needs.unit_tests.result }}" echo "integration_tests: ${{ needs.integration_tests.result }}" echo "e2e_tests: ${{ needs.e2e_tests.result }}" echo "coverage: ${{ needs.coverage.result }}" echo "build: ${{ needs.build.result }}" echo "docker: ${{ needs.docker.result }}" echo "helm: ${{ needs.helm.result }}" echo "push-validation: ${{ needs.push-validation.result }}" if [ "${{ needs.lint.result }}" != "success" ] || \ [ "${{ needs.typecheck.result }}" != "success" ] || \ [ "${{ needs.security.result }}" != "success" ] || \ [ "${{ needs.quality.result }}" != "success" ] || \ [ "${{ needs.unit_tests.result }}" != "success" ] || \ [ "${{ needs.integration_tests.result }}" != "success" ] || \ [ "${{ needs.e2e_tests.result }}" != "success" ] || \ [ "${{ needs.coverage.result }}" != "success" ] || \ [ "${{ needs.build.result }}" != "success" ] || \ [ "${{ needs.docker.result }}" != "success" ] || \ [ "${{ needs.helm.result }}" != "success" ] || \ [ "${{ needs.push-validation.result }}" != "success" ]; then echo "FAILED: One or more required jobs did not succeed" exit 1 fi echo "All required CI checks passed"