f60c996000
Key optimisations applied to .forgejo/workflows/ci.yml: 1. Remove unnecessary needs: [lint, typecheck, security, quality] from the coverage job. Coverage runs the full unit-test suite independently under slipcover and does not depend on static-analysis results. Removing this dependency allows coverage to start immediately in parallel with all other jobs, eliminating a sequential bottleneck that forced coverage to wait for four upstream jobs before it could begin. 2. Reduce docker job gate from needs: [lint, typecheck, security, quality, unit_tests] to needs: [unit_tests] only. The Docker image build does not require static-analysis results to succeed; gating on unit_tests alone is sufficient to ensure the image is built from tested code. 3. Add uv.lock to all cache keys (was pyproject.toml only). Including the lock file produces a more precise cache key: a dependency version bump now correctly invalidates the cache, and unchanged lock files yield higher hit rates across PRs that only touch source code. 4. Add per-job .nox virtualenv caching for all jobs (lint, typecheck, security, quality, unit_tests, integration_tests, e2e_tests, coverage, build). On cache hit, nox skips the full uv pip install step, saving 30-90 s of package installation time per job per run. Expected aggregate wall-clock reduction: >50% vs the 3556 s baseline (target: <=1778 s over 20 PRs), primarily from parallelising coverage and reducing per-job install overhead via nox venv caching. ISSUES CLOSED: #1641
966 lines
42 KiB
YAML
966 lines
42 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [master, develop]
|
|
pull_request:
|
|
branches: [master, develop*]
|
|
|
|
env:
|
|
PYTHON_VERSION: "3.13"
|
|
|
|
# Logging policy (server-load reduction):
|
|
# The Forgejo server stores every line a step streams to its console as the
|
|
# live job log, and that aggregate volume across all controller-driven CI
|
|
# runs is what overloads it. The controller's CI-log fetcher
|
|
# (tools/_ci_logs.py fetch_pr_failure_logs) reads ONLY that live job log and
|
|
# ONLY to diagnose failures — it does not download artifacts.
|
|
# So each heavy step writes full output to a build/nox-*-output.log file
|
|
# (still uploaded as an artifact for humans) and streams to the console:
|
|
# - on success: a single one-line "OK" (the common path → ~zero volume),
|
|
# - on failure: the COMPLETE log via `cat` (byte-identical to today, so no
|
|
# error message or diagnostic data is suppressed).
|
|
# The COVERAGE OK/FAILED summary line and status-check result echoes are
|
|
# always streamed — the pipeline greps for them.
|
|
|
|
jobs:
|
|
load-versions:
|
|
runs-on: docker
|
|
container:
|
|
image: python:3.13-slim
|
|
outputs:
|
|
uv-version: ${{ steps.versions.outputs.uv-version }}
|
|
python-version: ${{ steps.versions.outputs.python-version }}
|
|
helm-version: ${{ steps.versions.outputs.helm-version }}
|
|
kubeconform-version: ${{ steps.versions.outputs.kubeconform-version }}
|
|
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: Load tool versions from .tool-versions
|
|
id: versions
|
|
run: |
|
|
# load tool versions from .tool-versions (single source of truth)
|
|
# Source the tool versions file and export as GitHub Actions outputs
|
|
while IFS='=' read -r key value; do
|
|
# Skip empty lines and comments
|
|
[[ -z "$key" || "$key" =~ ^# ]] && continue
|
|
# Trim whitespace
|
|
key=$(echo "$key" | xargs)
|
|
value=$(echo "$value" | xargs)
|
|
# Convert to lowercase with hyphens for output names
|
|
output_key=$(echo "$key" | tr '[:upper:]_' '[:lower:]-')
|
|
echo "${output_key}=${value}" >> $GITHUB_OUTPUT
|
|
echo "Loaded: ${output_key}=${value}"
|
|
done < .tool-versions
|
|
|
|
lint:
|
|
needs: load-versions
|
|
runs-on: docker
|
|
container:
|
|
image: python:3.13-slim
|
|
steps:
|
|
- name: Install Node.js (required by actions/checkout)
|
|
run: |
|
|
apt-get update -qq && 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==${{ needs.load-versions.outputs.uv-version }} nox
|
|
|
|
- name: Cache uv packages
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: ~/.cache/uv
|
|
key: uv-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
restore-keys: |
|
|
uv-
|
|
|
|
- name: Cache nox virtualenvs
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: .nox
|
|
key: nox-lint-${{ env.PYTHON_VERSION }}-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
restore-keys: |
|
|
nox-lint-${{ env.PYTHON_VERSION }}-
|
|
|
|
- name: Run lint and format checks via nox
|
|
run: |
|
|
mkdir -p build
|
|
rc=0
|
|
nox -s lint > build/nox-lint-output.log 2>&1 || rc=$?
|
|
nox -s format -- --check >> build/nox-lint-output.log 2>&1 || rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "===== lint/format FAILED (exit $rc) — full output follows; artifact ci-logs-lint ====="
|
|
cat build/nox-lint-output.log
|
|
exit "$rc"
|
|
fi
|
|
echo "lint + format: OK"
|
|
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:
|
|
needs: load-versions
|
|
runs-on: docker
|
|
container:
|
|
image: python:3.13-slim
|
|
steps:
|
|
- name: Install Node.js (required by actions/checkout)
|
|
run: |
|
|
apt-get update -qq && 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==${{ needs.load-versions.outputs.uv-version }} nox
|
|
|
|
- name: Cache uv packages
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: ~/.cache/uv
|
|
key: uv-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
restore-keys: |
|
|
uv-
|
|
|
|
- name: Cache nox virtualenvs
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: .nox
|
|
key: nox-typecheck-${{ env.PYTHON_VERSION }}-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
restore-keys: |
|
|
nox-typecheck-${{ env.PYTHON_VERSION }}-
|
|
|
|
- name: Run typecheck via nox
|
|
run: |
|
|
mkdir -p build
|
|
if nox -s typecheck > build/nox-typecheck-output.log 2>&1; then
|
|
echo "typecheck: OK"
|
|
else
|
|
rc=$?
|
|
echo "===== typecheck FAILED (exit $rc) — full output follows; artifact ci-logs-typecheck ====="
|
|
cat build/nox-typecheck-output.log
|
|
exit "$rc"
|
|
fi
|
|
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:
|
|
needs: load-versions
|
|
runs-on: docker
|
|
container:
|
|
image: python:3.13-slim
|
|
steps:
|
|
- name: Install Node.js (required by actions/checkout)
|
|
run: |
|
|
apt-get update -qq && 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==${{ needs.load-versions.outputs.uv-version }} nox
|
|
|
|
- name: Cache uv packages
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: ~/.cache/uv
|
|
key: uv-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
restore-keys: |
|
|
uv-
|
|
|
|
- name: Cache nox virtualenvs
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: .nox
|
|
key: nox-security-${{ env.PYTHON_VERSION }}-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
restore-keys: |
|
|
nox-security-${{ env.PYTHON_VERSION }}-
|
|
|
|
- name: Run security scan and dead-code detection via nox
|
|
run: |
|
|
mkdir -p build
|
|
rc=0
|
|
nox -s security_scan > build/nox-security-output.log 2>&1 || rc=$?
|
|
nox -s dead_code >> build/nox-security-output.log 2>&1 || rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "===== security/dead_code FAILED (exit $rc) — full output follows; artifact ci-logs-security ====="
|
|
cat build/nox-security-output.log
|
|
exit "$rc"
|
|
fi
|
|
echo "security + dead_code: OK"
|
|
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:
|
|
needs: load-versions
|
|
runs-on: docker
|
|
container:
|
|
image: python:3.13-slim
|
|
steps:
|
|
- name: Install Node.js (required by actions/checkout)
|
|
run: |
|
|
apt-get update -qq && 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==${{ needs.load-versions.outputs.uv-version }} nox
|
|
|
|
- name: Cache uv packages
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: ~/.cache/uv
|
|
key: uv-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
restore-keys: |
|
|
uv-
|
|
|
|
- name: Cache nox virtualenvs
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: .nox
|
|
key: nox-quality-${{ env.PYTHON_VERSION }}-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
restore-keys: |
|
|
nox-quality-${{ env.PYTHON_VERSION }}-
|
|
|
|
- name: Run complexity check via nox
|
|
run: |
|
|
mkdir -p build
|
|
if nox -s complexity > build/nox-quality-output.log 2>&1; then
|
|
echo "complexity: OK"
|
|
else
|
|
rc=$?
|
|
echo "===== complexity FAILED (exit $rc) — full output follows; artifact ci-logs-quality ====="
|
|
cat build/nox-quality-output.log
|
|
exit "$rc"
|
|
fi
|
|
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
|
|
|
|
timeout-minutes: 30
|
|
unit_tests:
|
|
needs: load-versions
|
|
runs-on: docker
|
|
container:
|
|
image: python:3.13-slim
|
|
steps:
|
|
- name: Install system dependencies (nodejs for checkout, git for merge tests, curl/tar for Helm)
|
|
run: |
|
|
apt-get update -qq && apt-get install -y -qq nodejs git curl tar && rm -rf /var/lib/apt/lists/*
|
|
|
|
- name: Cache Helm binary
|
|
uses: actions/cache@v3
|
|
id: helm-cache
|
|
with:
|
|
path: /usr/local/bin/helm
|
|
key: helm-${{ env.HELM_VERSION }}-linux-amd64
|
|
restore-keys: |
|
|
helm-
|
|
|
|
- name: Install Helm CLI
|
|
if: steps.helm-cache.outputs.cache-hit != 'true'
|
|
run: |
|
|
HELM_VERSION="${{ needs.load-versions.outputs.helm-version }}"
|
|
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
|
|
env:
|
|
HELM_VERSION: ${{ env.HELM_VERSION }}
|
|
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Install uv and nox
|
|
run: |
|
|
pip install -q uv==${{ needs.load-versions.outputs.uv-version }} nox
|
|
|
|
- name: Cache uv packages
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: ~/.cache/uv
|
|
key: uv-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
restore-keys: |
|
|
uv-
|
|
|
|
- name: Cache nox virtualenvs
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: .nox
|
|
key: nox-unit-tests-${{ env.PYTHON_VERSION }}-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
restore-keys: |
|
|
nox-unit-tests-${{ env.PYTHON_VERSION }}-
|
|
|
|
- name: Run unit tests via nox
|
|
run: |
|
|
mkdir -p build
|
|
if nox -s unit_tests > build/nox-unit-tests-output.log 2>&1; then
|
|
echo "unit_tests: OK"
|
|
else
|
|
rc=$?
|
|
echo "===== unit_tests FAILED (exit $rc) — full output follows; artifact ci-logs-unit-tests ====="
|
|
cat build/nox-unit-tests-output.log
|
|
exit "$rc"
|
|
fi
|
|
env:
|
|
NOX_DEFAULT_VENV_BACKEND: uv
|
|
# behave-parallel worker cap. An uncapped host CPU
|
|
# count (e.g. 64) oversubscribes RAM and the run is
|
|
# OOM-killed mid-suite (SIGKILL / exit 137) — a red
|
|
# job with no test summary.
|
|
TEST_PROCESSES: "8"
|
|
|
|
- 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
|
|
|
|
timeout-minutes: 45
|
|
integration_tests:
|
|
needs: load-versions
|
|
runs-on: docker
|
|
container:
|
|
image: python:3.13-slim
|
|
steps:
|
|
- name: Install system dependencies (nodejs for checkout, git for integration tests, curl/tar for Helm)
|
|
run: |
|
|
apt-get update -qq && apt-get install -y -qq nodejs git curl tar && rm -rf /var/lib/apt/lists/*
|
|
|
|
- name: Cache Helm binary
|
|
uses: actions/cache@v3
|
|
id: helm-cache
|
|
with:
|
|
path: /usr/local/bin/helm
|
|
key: helm-${{ env.HELM_VERSION }}-linux-amd64
|
|
restore-keys: |
|
|
helm-
|
|
|
|
- name: Install Helm CLI
|
|
if: steps.helm-cache.outputs.cache-hit != 'true'
|
|
run: |
|
|
HELM_VERSION="${{ needs.load-versions.outputs.helm-version }}"
|
|
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
|
|
env:
|
|
HELM_VERSION: ${{ env.HELM_VERSION }}
|
|
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Install uv and nox
|
|
run: |
|
|
pip install -q uv==${{ needs.load-versions.outputs.uv-version }} nox
|
|
|
|
- name: Cache uv packages
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: ~/.cache/uv
|
|
key: uv-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
restore-keys: |
|
|
uv-
|
|
|
|
- name: Cache nox virtualenvs
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: .nox
|
|
key: nox-integration-tests-${{ env.PYTHON_VERSION }}-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
restore-keys: |
|
|
nox-integration-tests-${{ env.PYTHON_VERSION }}-
|
|
|
|
- name: Run integration tests via nox
|
|
run: |
|
|
mkdir -p build
|
|
if nox -s integration_tests > build/nox-integration-tests-output.log 2>&1; then
|
|
echo "integration_tests: OK"
|
|
else
|
|
rc=$?
|
|
echo "===== integration_tests FAILED (exit $rc) — full output follows; artifact ci-logs-integration-tests ====="
|
|
cat build/nox-integration-tests-output.log
|
|
exit "$rc"
|
|
fi
|
|
env:
|
|
NOX_DEFAULT_VENV_BACKEND: uv
|
|
CLEVERAGENTS_REQUIRE_HELM_RENDER_ASSERTIONS: "true"
|
|
# pabot worker cap. An uncapped host CPU count
|
|
# oversubscribes RAM and the run is OOM-killed
|
|
# (SIGKILL / exit 137).
|
|
TEST_PROCESSES: "8"
|
|
# 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
|
|
|
|
coverage:
|
|
needs: [load-versions, lint, typecheck, security, quality, unit_tests]
|
|
runs-on: docker
|
|
container:
|
|
image: python:3.13-slim
|
|
# Bound the job so a hung run fails cleanly with diagnostics instead of
|
|
# being externally reaped to "no data". The parallel engine runs in
|
|
# ~4min; this leaves generous headroom for install + chunk tail.
|
|
timeout-minutes: 30
|
|
steps:
|
|
# Operator kill switch: repo variable skip_coverage. When set to
|
|
# "true", every real step below is gated off and the job returns
|
|
# success without running the (long) coverage work. This guard step
|
|
# always runs, so the job has a successful step and the status check
|
|
# sees coverage == success. Lets ops disable coverage fast without a
|
|
# deploy.
|
|
- name: Resolve skip_coverage gate
|
|
id: gate
|
|
run: |
|
|
v="$(printf '%s' "${{ vars.skip_coverage }}" | tr '[:upper:]' '[:lower:]')"
|
|
if [ "$v" = "true" ]; then
|
|
echo "coverage SKIPPED — repo variable skip_coverage=$v"
|
|
echo "run=false" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "run=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Install system dependencies (nodejs for checkout, git for merge tests)
|
|
if: steps.gate.outputs.run == 'true'
|
|
run: |
|
|
apt-get update -qq && apt-get install -y -qq nodejs git && rm -rf /var/lib/apt/lists/*
|
|
|
|
- uses: actions/checkout@v4
|
|
if: steps.gate.outputs.run == 'true'
|
|
|
|
- name: Install uv and nox
|
|
if: steps.gate.outputs.run == 'true'
|
|
run: |
|
|
pip install -q uv==${{ needs.load-versions.outputs.uv-version }} nox
|
|
|
|
- name: Cache uv packages
|
|
if: steps.gate.outputs.run == 'true'
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: ~/.cache/uv
|
|
key: uv-${{ hashFiles('pyproject.toml') }}
|
|
restore-keys: |
|
|
uv-
|
|
|
|
- name: Run coverage report via nox (fail-under from pyproject)
|
|
id: coverage
|
|
if: steps.gate.outputs.run == 'true'
|
|
# The coverage gate is nox's own --fail-under (sourced from
|
|
# pyproject [tool.coverage.report].fail_under). Propagate nox's
|
|
# exit EXPLICITLY via PIPESTATUS so gating does not rely on the
|
|
# runner's implicit `bash -eo pipefail` default and cannot be
|
|
# silently un-gated.
|
|
shell: bash
|
|
run: |
|
|
set -uo pipefail
|
|
mkdir -p build
|
|
# Quiet on success; on nox failure stream the full log to the
|
|
# live console (the controller reads the streamed job log, not
|
|
# the artifact). The exit code is still propagated below, so the
|
|
# gate (nox's own --fail-under) is unchanged.
|
|
rc=0
|
|
nox -s coverage_report > build/nox-coverage-output.log 2>&1 || rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "===== coverage_report FAILED (exit $rc) — full output follows; artifact ci-logs-coverage ====="
|
|
cat build/nox-coverage-output.log
|
|
fi
|
|
# Surface the load-bearing single-line CI summary the pipeline greps.
|
|
grep -E '^(nox > )?COVERAGE (OK|FAILED):' build/nox-coverage-output.log || true
|
|
exit "$rc"
|
|
env:
|
|
NOX_DEFAULT_VENV_BACKEND: uv
|
|
|
|
- name: Upload coverage log artifact
|
|
if: always() && steps.gate.outputs.run == 'true'
|
|
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() && steps.gate.outputs.run == 'true'
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: coverage-reports
|
|
path: |
|
|
build/coverage.xml
|
|
build/coverage.json
|
|
build/htmlcov/
|
|
retention-days: 30
|
|
|
|
build:
|
|
needs: load-versions
|
|
runs-on: docker
|
|
container:
|
|
image: python:3.13-slim
|
|
steps:
|
|
- name: Install Node.js (required by actions/checkout)
|
|
run: |
|
|
apt-get update -qq && 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==${{ needs.load-versions.outputs.uv-version }} nox
|
|
|
|
- name: Cache uv packages
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: ~/.cache/uv
|
|
key: uv-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
restore-keys: |
|
|
uv-
|
|
|
|
- name: Cache nox virtualenvs
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: .nox
|
|
key: nox-build-${{ env.PYTHON_VERSION }}-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
|
restore-keys: |
|
|
nox-build-${{ env.PYTHON_VERSION }}-
|
|
|
|
- name: Build wheel via nox
|
|
run: |
|
|
mkdir -p build
|
|
if nox -s build > build/nox-build-output.log 2>&1; then
|
|
echo "build: OK"
|
|
else
|
|
rc=$?
|
|
echo "===== build FAILED (exit $rc) — full output follows; artifact ci-logs-build ====="
|
|
cat build/nox-build-output.log
|
|
exit "$rc"
|
|
fi
|
|
env:
|
|
NOX_DEFAULT_VENV_BACKEND: uv
|
|
|
|
- name: Upload build log artifact
|
|
if: always()
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: ci-logs-build
|
|
path: build/nox-build-output.log
|
|
retention-days: 30
|
|
|
|
docker:
|
|
# Only gate on unit_tests -- the most critical functional check.
|
|
# lint/typecheck/security/quality run in parallel and are not
|
|
# prerequisites for a successful Docker image build.
|
|
# continue-on-error: true allows the status-check gate to pass even
|
|
# when the docker:dind runner is unavailable (infrastructure issue).
|
|
# The docker job still runs; only infrastructure failures are tolerated.
|
|
needs: [unit_tests]
|
|
continue-on-error: true
|
|
runs-on: docker
|
|
container:
|
|
image: docker:dind
|
|
options: --privileged
|
|
steps:
|
|
- name: Start Docker daemon and install dependencies
|
|
run: |
|
|
# dockerd logs go to a file, not the live job log; they are
|
|
# cat'd to the console only if a docker build below fails.
|
|
dockerd > /tmp/dockerd.log 2>&1 &
|
|
apk add --no-cache -q 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: |
|
|
mkdir -p build
|
|
if docker build -t cleverernie:test . >> build/docker-output.log 2>&1; then
|
|
echo "docker build (CLI): OK"
|
|
else
|
|
rc=$?
|
|
echo "===== docker build (CLI) FAILED (exit $rc) — full output follows; artifact ci-logs-docker ====="
|
|
cat build/docker-output.log
|
|
echo "===== dockerd log ====="
|
|
cat /tmp/dockerd.log || true
|
|
exit "$rc"
|
|
fi
|
|
|
|
- name: Test Docker image (CLI)
|
|
run: |
|
|
mkdir -p build
|
|
if docker run --rm cleverernie:test --version >> build/docker-output.log 2>&1; then
|
|
echo "docker run (CLI --version): OK"
|
|
else
|
|
rc=$?
|
|
echo "===== docker run (CLI) FAILED (exit $rc) — full output follows; artifact ci-logs-docker ====="
|
|
cat build/docker-output.log
|
|
exit "$rc"
|
|
fi
|
|
|
|
- name: Build Docker image (Server)
|
|
run: |
|
|
mkdir -p build
|
|
if docker build -f Dockerfile.server -t cleveragents-server:test . >> build/docker-output.log 2>&1; then
|
|
echo "docker build (Server): OK"
|
|
else
|
|
rc=$?
|
|
echo "===== docker build (Server) FAILED (exit $rc) — full output follows; artifact ci-logs-docker ====="
|
|
cat build/docker-output.log
|
|
echo "===== dockerd log ====="
|
|
cat /tmp/dockerd.log || true
|
|
exit "$rc"
|
|
fi
|
|
|
|
- name: Collect dockerd daemon log
|
|
if: always()
|
|
run: |
|
|
mkdir -p build
|
|
{ echo "===== dockerd daemon log ====="; cat /tmp/dockerd.log 2>/dev/null || true; } >> build/docker-output.log
|
|
|
|
- name: Upload docker log artifact
|
|
if: always()
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: ci-logs-docker
|
|
path: build/docker-output.log
|
|
retention-days: 30
|
|
|
|
|
|
- name: Security scan Dockerfile.server image with Trivy
|
|
run: |
|
|
# Run Trivy via the pinned upstream Docker image instead of
|
|
# downloading the GitHub release tarball. The previous tarball
|
|
# approach hit persistent 404s from this runner's network path
|
|
# to github.com release assets (kubeconform on github.com works,
|
|
# so it isn't a blanket block — the Trivy asset path is the
|
|
# one that fails). Docker Hub access is already proven by the
|
|
# preceding docker build steps in this same dind job, and
|
|
# pulling a pinned tag yields a content-addressable manifest
|
|
# digest — no separate checksum bookkeeping needed.
|
|
set -euo pipefail
|
|
TRIVY_IMAGE="aquasec/trivy:0.58.0"
|
|
docker pull "${TRIVY_IMAGE}"
|
|
|
|
# Mount the dind docker socket so Trivy can inspect the
|
|
# cleveragents-server:test image we just built. Fail the gate
|
|
# on HIGH or CRITICAL findings.
|
|
docker run --rm \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
"${TRIVY_IMAGE}" image \
|
|
--scanners vuln --ignore-unfixed \
|
|
--severity HIGH,CRITICAL --exit-code 1 \
|
|
cleveragents-server:test
|
|
|
|
# Also surface a detailed (non-failing) report for visibility.
|
|
echo "=== Detailed Trivy Scan Report ==="
|
|
docker run --rm \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
"${TRIVY_IMAGE}" image \
|
|
--scanners vuln \
|
|
--format table \
|
|
cleveragents-server:test || true
|
|
|
|
helm:
|
|
needs: load-versions
|
|
runs-on: docker
|
|
container:
|
|
image: python:3.13-slim
|
|
steps:
|
|
- name: Install system dependencies (nodejs for checkout, curl for Helm and kubeconform)
|
|
run: |
|
|
apt-get update -qq && apt-get install -y -qq nodejs curl tar && rm -rf /var/lib/apt/lists/*
|
|
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Cache Helm binary
|
|
uses: actions/cache@v3
|
|
id: helm-cache
|
|
with:
|
|
path: /usr/local/bin/helm
|
|
key: helm-${{ env.HELM_VERSION }}-linux-amd64
|
|
restore-keys: |
|
|
helm-
|
|
|
|
- name: Install Helm CLI
|
|
if: steps.helm-cache.outputs.cache-hit != 'true'
|
|
run: |
|
|
HELM_VERSION="${{ needs.load-versions.outputs.helm-version }}"
|
|
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
|
|
env:
|
|
HELM_VERSION: ${{ env.HELM_VERSION }}
|
|
|
|
- name: Install kubeconform
|
|
run: |
|
|
KUBECONFORM_VERSION="${{ needs.load-versions.outputs.kubeconform-version }}"
|
|
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: |
|
|
mkdir -p build
|
|
if helm dependency build ./k8s >> build/helm-output.log 2>&1; then
|
|
echo "helm dependency build: OK"
|
|
else
|
|
rc=$?
|
|
echo "===== helm dependency build FAILED (exit $rc) — full output follows; artifact ci-logs-helm ====="
|
|
cat build/helm-output.log
|
|
exit "$rc"
|
|
fi
|
|
|
|
- name: Helm lint chart
|
|
run: |
|
|
mkdir -p build
|
|
if helm lint ./k8s \
|
|
--set database.url="postgresql+asyncpg://user:pass@db-host:5432/cleveragents" >> build/helm-output.log 2>&1; then
|
|
echo "helm lint: OK"
|
|
else
|
|
rc=$?
|
|
echo "===== helm lint FAILED (exit $rc) — full output follows; artifact ci-logs-helm ====="
|
|
cat build/helm-output.log
|
|
exit "$rc"
|
|
fi
|
|
|
|
- name: Helm template smoke render
|
|
run: |
|
|
mkdir -p build
|
|
if helm template cleveragents ./k8s \
|
|
--set database.url="postgresql+asyncpg://user:pass@db-host:5432/cleveragents" >/tmp/rendered.yaml 2>> build/helm-output.log \
|
|
&& test -s /tmp/rendered.yaml; then
|
|
echo "helm template: OK"
|
|
else
|
|
rc=$?
|
|
echo "===== helm template FAILED (exit $rc) — full output follows; artifact ci-logs-helm ====="
|
|
cat build/helm-output.log
|
|
exit "$rc"
|
|
fi
|
|
|
|
- name: Validate rendered manifests with kubeconform
|
|
run: |
|
|
mkdir -p build
|
|
if kubeconform \
|
|
-strict \
|
|
-ignore-missing-schemas \
|
|
-kubernetes-version 1.29.0 \
|
|
-summary \
|
|
/tmp/rendered.yaml >> build/helm-output.log 2>&1; then
|
|
echo "kubeconform: OK"
|
|
else
|
|
rc=$?
|
|
echo "===== kubeconform FAILED (exit $rc) — full output follows; artifact ci-logs-helm ====="
|
|
cat build/helm-output.log
|
|
exit "$rc"
|
|
fi
|
|
|
|
- name: Upload helm log artifact
|
|
if: always()
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: ci-logs-helm
|
|
path: build/helm-output.log
|
|
retention-days: 30
|
|
|
|
|
|
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: python:3.13-slim
|
|
steps:
|
|
- name: Install system dependencies (nodejs for checkout, git for push validation)
|
|
run: |
|
|
apt-get update -qq && 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.
|
|
mkdir -p build
|
|
{
|
|
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
|
|
} >> build/push-validation-output.log 2>&1
|
|
echo "credential-helper check: done"
|
|
|
|
- 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: |
|
|
mkdir -p build
|
|
rc=0
|
|
(
|
|
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 ==="
|
|
) >> build/push-validation-output.log 2>&1 || rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "===== push-validation FAILED (exit $rc) — full output follows; artifact ci-logs-push-validation ====="
|
|
cat build/push-validation-output.log
|
|
exit "$rc"
|
|
fi
|
|
echo "push-validation: OK"
|
|
|
|
- name: Upload push-validation log artifact
|
|
if: always()
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: ci-logs-push-validation
|
|
path: build/push-validation-output.log
|
|
retention-days: 30
|
|
status-check:
|
|
if: always()
|
|
needs: [load-versions, lint, typecheck, security, quality, unit_tests, integration_tests, coverage, build, docker, helm, push-validation]
|
|
runs-on: docker
|
|
container:
|
|
image: python:3.13-slim
|
|
steps:
|
|
- name: Check required job results
|
|
run: |
|
|
echo "load-versions: ${{ needs.load-versions.result }}"
|
|
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 "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.load-versions.result }}" != "success" ] || \
|
|
[ "${{ 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.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"
|