name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
pull-requests: write
security-events: write
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
detect-changes:
name: Detect Changes
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
docs-only: ${{ steps.classify.outputs.docs-only }}
specs-only: ${{ steps.classify.outputs.specs-only }}
run-full-ci: ${{ steps.classify.outputs.run-full-ci }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Filter changed paths
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d id: filter
with:
filters: |
docs:
- 'book/src/**'
- 'docs/**'
- '**.md'
- '.local/testing/**'
specs:
- 'specs/**'
code:
- 'crates/**'
- 'src/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'build.rs'
workflows:
- '.github/workflows/**'
- '.cargo/**'
shell:
- 'install/**'
- name: Classify changes
id: classify
run: |
DOCS="${{ steps.filter.outputs.docs }}"
SPECS="${{ steps.filter.outputs.specs }}"
CODE="${{ steps.filter.outputs.code }}"
WORKFLOWS="${{ steps.filter.outputs.workflows }}"
# docs-only: docs or specs changed, nothing that requires building
if [[ "$CODE" == "false" && "$WORKFLOWS" == "false" && ("$DOCS" == "true" || "$SPECS" == "true") ]]; then
echo "docs-only=true" >> "$GITHUB_OUTPUT"
else
echo "docs-only=false" >> "$GITHUB_OUTPUT"
fi
# specs-only: specs changed, no code or workflow changes
if [[ "$SPECS" == "true" && "$CODE" == "false" && "$WORKFLOWS" == "false" && "$DOCS" == "false" ]]; then
echo "specs-only=true" >> "$GITHUB_OUTPUT"
else
echo "specs-only=false" >> "$GITHUB_OUTPUT"
fi
# run-full-ci: any code, workflow, or shell change triggers full CI
if [[ "$CODE" == "true" || "$WORKFLOWS" == "true" ]]; then
echo "run-full-ci=true" >> "$GITHUB_OUTPUT"
else
echo "run-full-ci=false" >> "$GITHUB_OUTPUT"
fi
cla:
name: CLA Check
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 with:
path-to-signatures: cla-signatures.json
path-to-document: https://github.com/bug-ops/zeph/blob/main/.github/CLA.md
branch: cla-signatures
allowlist: bug-ops,dependabot[bot],github-actions[bot],renovate[bot]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
lint-shellcheck:
name: Lint (shellcheck)
needs: detect-changes
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Run shellcheck
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 with:
scandir: install
lint-fmt:
name: Lint (fmt)
needs: detect-changes
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 with:
toolchain: nightly
components: rustfmt
- name: Check formatting
run: cargo +nightly fmt --check
lint-clippy:
name: Lint (clippy, ${{ matrix.label }})
needs: detect-changes
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
include:
- features: "desktop,ide,server,chat,pdf,scheduler,testing"
label: desktop-ide-server-chat-pdf-scheduler
- features: bench
label: bench
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 with:
toolchain: stable
components: clippy
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 with:
shared-key: "ci"
- name: Clippy
run: cargo clippy --profile ci --workspace --all-targets --features ${{ matrix.features }} -- -D warnings
msrv:
name: MSRV check (1.95, ${{ matrix.label }})
needs: detect-changes
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- features: "desktop,ide,server,chat,pdf,scheduler"
label: desktop-ide-server-chat-pdf-scheduler
- features: bench
label: bench
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 with:
toolchain: "1.95"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 with:
shared-key: "msrv-${{ matrix.label }}"
- name: cargo check (MSRV, --all-targets)
run: cargo check --workspace --all-targets --features ${{ matrix.features }} --locked
build-tests:
name: Build Tests (${{ matrix.os }})
needs: detect-changes
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 with:
cache-targets: "false"
shared-key: "ci"
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad - uses: taiki-e/install-action@dfd6a81ff1efc1c100332d760e0c9edfe2ae0a51 - name: Build and archive tests
run: cargo nextest archive --config-file .github/nextest.toml --cargo-profile ci --workspace --features "desktop,ide,server,chat,pdf,scheduler" --lib --bins --tests --archive-file nextest-archive.tar.zst
- name: Upload test archive
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with:
name: nextest-archive-${{ matrix.os }}
path: nextest-archive.tar.zst
retention-days: 1
- name: Build and archive zeph-db postgres integration tests
if: matrix.os == 'ubuntu-latest'
run: cargo nextest archive --config-file .github/nextest.toml --cargo-profile ci -p zeph-db --no-default-features --features test-utils --tests --archive-file nextest-archive-zeph-db-postgres.tar.zst
- name: Upload zeph-db postgres test archive
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with:
name: nextest-archive-zeph-db-postgres
path: nextest-archive-zeph-db-postgres.tar.zst
retention-days: 1
- name: Upload binary
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with:
name: zeph-binary
path: target/ci/zeph
retention-days: 1
test:
name: "Test (shard ${{ matrix.partition }})"
needs: [detect-changes, lint-fmt, lint-clippy, build-tests]
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
partition: ["1/5", "2/5", "3/5", "4/5", "5/5"]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: taiki-e/install-action@dfd6a81ff1efc1c100332d760e0c9edfe2ae0a51 - name: Download test archive
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
name: nextest-archive-ubuntu-latest
- name: "Run tests (shard ${{ matrix.partition }})"
run: |
cargo nextest run \
--config-file .github/nextest.toml \
--archive-file nextest-archive.tar.zst \
--workspace-remap . \
--profile ci-partition \
--partition hash:${{ matrix.partition }}
integration:
name: Integration Tests
needs: [detect-changes, lint-fmt, lint-clippy, build-tests]
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: taiki-e/install-action@dfd6a81ff1efc1c100332d760e0c9edfe2ae0a51 - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Restore testcontainer image cache
id: tc-image-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae with:
path: /tmp/testcontainer-images
key: testcontainer-images-${{ hashFiles('.github/testcontainers-images.txt') }}
- name: Pull and save testcontainer images (cache miss)
if: steps.tc-image-cache.outputs.cache-hit != 'true'
run: |
mkdir -p /tmp/testcontainer-images
while IFS= read -r image; do
[ -z "$image" ] && continue
echo "Pulling $image"
docker pull "$image"
fname=$(echo "$image" | tr '/:' '__')
docker save "$image" | zstd -T0 -3 -o /tmp/testcontainer-images/"$fname".tar.zst
done < .github/testcontainers-images.txt
ls -lh /tmp/testcontainer-images/
- name: Load testcontainer images (cache hit)
if: steps.tc-image-cache.outputs.cache-hit == 'true'
run: |
for f in /tmp/testcontainer-images/*.tar.zst; do
echo "Loading $f"
zstd -d -c "$f" | docker load
done
docker image ls
- name: Download test archive
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
name: nextest-archive-ubuntu-latest
- name: Run integration tests (testcontainers)
run: cargo nextest run --config-file .github/nextest.toml --archive-file nextest-archive.tar.zst --workspace-remap . --profile ci -E 'binary(~integration)'
- name: Download zeph-db postgres test archive
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
name: nextest-archive-zeph-db-postgres
- name: Run postgres integration tests (testcontainers)
run: cargo nextest run --config-file .github/nextest.toml --archive-file nextest-archive-zeph-db-postgres.tar.zst --workspace-remap . --profile ci --run-ignored ignored-only
- name: Run Qdrant integration tests (testcontainers)
run: cargo nextest run --config-file .github/nextest.toml --archive-file nextest-archive.tar.zst --workspace-remap . --profile ci --run-ignored ignored-only -E 'binary(qdrant_integration)'
coverage:
name: Coverage
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.run-full-ci == 'true'
needs: [detect-changes, lint-fmt, lint-clippy]
runs-on: ubuntu-latest
timeout-minutes: 20
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
RUSTFLAGS: ""
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 with:
cache-targets: "false"
shared-key: "coverage"
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad - uses: taiki-e/install-action@195422eb3ed4087e3a6571fac204cfc3cb1dd3ed - uses: taiki-e/install-action@dfd6a81ff1efc1c100332d760e0c9edfe2ae0a51 - name: Generate coverage
run: cargo llvm-cov nextest --config-file .github/nextest.toml --cargo-profile ci --workspace --features full --lib --bins --lcov --output-path lcov.info
- name: Upload coverage
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
fail_ci_if_error: false
docker-build-and-scan:
name: Docker Build and Security Scan
needs: [detect-changes, lint-fmt, lint-clippy, build-tests]
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Download binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
name: zeph-binary
path: target/ci
- name: Prepare binaries for Docker
run: |
mkdir -p binaries
cp target/ci/zeph binaries/zeph-amd64
cp target/ci/zeph binaries/zeph-arm64
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd - name: Build Docker image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f with:
context: .
file: docker/Dockerfile
load: true
tags: zeph:local
push: false
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 continue-on-error: true
with:
image-ref: zeph:local
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: '1'
limit-severities-for-sarif: true
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 with:
sarif_file: trivy-results.sarif
bundle-check:
name: Bundle Check (${{ matrix.bundle }})
needs: detect-changes
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
bundle: [desktop, ide, server, chat, ml, bench]
include:
- bundle: ml
allow_failure: true
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 with:
shared-key: "ci"
- name: Check bundle
continue-on-error: ${{ matrix.allow_failure == true }}
run: cargo check --features ${{ matrix.bundle }}
build-postgres:
name: Build (postgres)
needs: detect-changes
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 with:
shared-key: "ci"
- name: Check postgres build (zeph-db)
run: cargo check -p zeph-db --no-default-features --features postgres
rustdoc:
name: Rustdoc
needs: detect-changes
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
env:
RUSTDOCFLAGS: "--deny rustdoc::broken_intra_doc_links"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 with:
shared-key: "ci"
- name: Build docs (all features except candle)
run: cargo doc --no-deps --workspace --features "desktop,ide,server,chat,pdf,scheduler"
- name: Doc-tests
run: cargo test --doc --workspace --features "desktop,ide,server,chat,pdf,scheduler"
validate-specs:
name: Validate Specs
needs: detect-changes
if: needs.detect-changes.outputs.specs-only == 'true'
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Check spec structure
run: |
echo "Spec files found:"
find specs -name "*.md" -type f | sort
echo "Total: $(find specs -name "*.md" -type f | wc -l)"
# Verify README index exists
test -f specs/README.md || (echo "::error::specs/README.md index is missing" && exit 1)
ci-status:
name: CI Status
if: always()
needs: [cla, lint-shellcheck, lint-fmt, lint-clippy, msrv, build-tests, test, integration, coverage, docker-build-and-scan, bundle-check, validate-specs, build-postgres, rustdoc]
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
- name: Check all jobs
run: |
results=(
"${{ needs.cla.result }}"
"${{ needs.lint-shellcheck.result }}"
"${{ needs.lint-fmt.result }}"
"${{ needs.lint-clippy.result }}"
"${{ needs.msrv.result }}"
"${{ needs.build-tests.result }}"
"${{ needs.test.result }}"
"${{ needs.integration.result }}"
"${{ needs.coverage.result }}"
"${{ needs.docker-build-and-scan.result }}"
"${{ needs.bundle-check.result }}"
"${{ needs.validate-specs.result }}"
"${{ needs.build-postgres.result }}"
"${{ needs.rustdoc.result }}"
)
for r in "${results[@]}"; do
if [[ "$r" != "success" && "$r" != "skipped" ]]; then
echo "::error::One or more jobs failed or were cancelled"
exit 1
fi
done
echo "All jobs passed"