git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
1273 lines
33 KiB
Markdown
1273 lines
33 KiB
Markdown
# Agent 15: CI/CD Pipeline Implementation
|
||
|
||
**Agent**: CI/CD Engineer
|
||
**Focus**: Complete GitHub Actions workflows for testing, benchmarking, and release automation
|
||
**Status**: Ready for Implementation
|
||
|
||
## Overview
|
||
|
||
This document provides complete CI/CD pipeline configuration for RuVector, covering:
|
||
- Continuous integration testing across platforms
|
||
- Automated releases to multiple package registries
|
||
- Daily performance benchmarking
|
||
- Quality gates and dependency management
|
||
|
||
## 1. CI Workflow - Test on Every PR
|
||
|
||
**File**: `.github/workflows/ci.yml`
|
||
|
||
```yaml
|
||
name: CI
|
||
|
||
on:
|
||
push:
|
||
branches: [main, develop]
|
||
pull_request:
|
||
branches: [main, develop]
|
||
|
||
env:
|
||
CARGO_TERM_COLOR: always
|
||
RUST_BACKTRACE: 1
|
||
|
||
jobs:
|
||
test:
|
||
name: Test - ${{ matrix.os }} - ${{ matrix.arch }} - Node ${{ matrix.node }}
|
||
runs-on: ${{ matrix.os }}
|
||
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||
arch: [x64, arm64]
|
||
node: [18, 20, 22]
|
||
exclude:
|
||
# Windows ARM64 runners not widely available
|
||
- os: windows-latest
|
||
arch: arm64
|
||
# Ubuntu ARM64 requires special runners
|
||
- os: ubuntu-latest
|
||
arch: arm64
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Setup Rust
|
||
uses: dtolnay/rust-toolchain@stable
|
||
with:
|
||
components: rustfmt, clippy
|
||
|
||
- name: Setup Rust cache
|
||
uses: Swatinem/rust-cache@v2
|
||
with:
|
||
cache-on-failure: true
|
||
|
||
- name: Setup Node.js ${{ matrix.node }}
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: ${{ matrix.node }}
|
||
cache: 'npm'
|
||
architecture: ${{ matrix.arch }}
|
||
|
||
- name: Install dependencies
|
||
run: npm ci
|
||
|
||
- name: Run Clippy
|
||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||
|
||
- name: Check formatting
|
||
run: cargo fmt -- --check
|
||
|
||
- name: Run Rust tests
|
||
run: cargo test --all-features --verbose
|
||
|
||
- name: Build NAPI bindings
|
||
run: npm run build
|
||
env:
|
||
CARGO_PROFILE_RELEASE_LTO: 'false'
|
||
|
||
- name: Run Node.js tests
|
||
run: npm test
|
||
|
||
- name: Generate coverage
|
||
if: matrix.os == 'ubuntu-latest' && matrix.node == '20' && matrix.arch == 'x64'
|
||
run: |
|
||
cargo install cargo-tarpaulin
|
||
cargo tarpaulin --out Xml --output-dir ./coverage
|
||
|
||
- name: Upload coverage to Codecov
|
||
if: matrix.os == 'ubuntu-latest' && matrix.node == '20' && matrix.arch == 'x64'
|
||
uses: codecov/codecov-action@v4
|
||
with:
|
||
files: ./coverage/cobertura.xml
|
||
flags: unittests
|
||
fail_ci_if_error: true
|
||
token: ${{ secrets.CODECOV_TOKEN }}
|
||
|
||
- name: Check coverage threshold
|
||
if: matrix.os == 'ubuntu-latest' && matrix.node == '20' && matrix.arch == 'x64'
|
||
run: |
|
||
COVERAGE=$(cargo tarpaulin --out Json | jq -r '.files | map(.coverage) | add / length')
|
||
echo "Coverage: $COVERAGE%"
|
||
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
|
||
echo "Coverage $COVERAGE% is below threshold 80%"
|
||
exit 1
|
||
fi
|
||
|
||
lint:
|
||
name: Lint and Type Check
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '20'
|
||
cache: 'npm'
|
||
|
||
- name: Install dependencies
|
||
run: npm ci
|
||
|
||
- name: Run ESLint
|
||
run: npm run lint
|
||
continue-on-error: true
|
||
|
||
- name: Run TypeScript check
|
||
run: npm run typecheck
|
||
|
||
security:
|
||
name: Security Audit
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Setup Rust
|
||
uses: dtolnay/rust-toolchain@stable
|
||
|
||
- name: Run cargo audit
|
||
run: |
|
||
cargo install cargo-audit
|
||
cargo audit
|
||
|
||
- name: Run npm audit
|
||
run: npm audit --audit-level=moderate
|
||
|
||
wasm:
|
||
name: WASM Build
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Setup Rust
|
||
uses: dtolnay/rust-toolchain@stable
|
||
with:
|
||
targets: wasm32-unknown-unknown
|
||
|
||
- name: Install wasm-pack
|
||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||
|
||
- name: Build WASM
|
||
run: wasm-pack build --target web --out-dir pkg/wasm
|
||
|
||
- name: Test WASM
|
||
run: wasm-pack test --headless --firefox
|
||
|
||
quality-gate:
|
||
name: Quality Gate
|
||
runs-on: ubuntu-latest
|
||
needs: [test, lint, security, wasm]
|
||
if: always()
|
||
|
||
steps:
|
||
- name: Check job results
|
||
run: |
|
||
if [[ "${{ needs.test.result }}" != "success" ]] || \
|
||
[[ "${{ needs.lint.result }}" != "success" ]] || \
|
||
[[ "${{ needs.security.result }}" != "success" ]] || \
|
||
[[ "${{ needs.wasm.result }}" != "success" ]]; then
|
||
echo "Quality gate failed"
|
||
exit 1
|
||
fi
|
||
echo "Quality gate passed"
|
||
```
|
||
|
||
## 2. Release Workflow - Publish on Tag
|
||
|
||
**File**: `.github/workflows/release.yml`
|
||
|
||
```yaml
|
||
name: Release
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- 'v*.*.*'
|
||
|
||
permissions:
|
||
contents: write
|
||
packages: write
|
||
|
||
env:
|
||
CARGO_TERM_COLOR: always
|
||
|
||
jobs:
|
||
create-release:
|
||
name: Create Release
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||
version: ${{ steps.get_version.outputs.version }}
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Get version
|
||
id: get_version
|
||
run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||
|
||
- name: Generate changelog
|
||
id: changelog
|
||
run: |
|
||
PREV_TAG=$(git describe --abbrev=0 --tags $(git rev-list --tags --skip=1 --max-count=1) 2>/dev/null || echo "")
|
||
if [ -z "$PREV_TAG" ]; then
|
||
CHANGELOG=$(git log --pretty=format:"- %s (%h)" --reverse)
|
||
else
|
||
CHANGELOG=$(git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --reverse)
|
||
fi
|
||
echo "changelog<<EOF" >> $GITHUB_OUTPUT
|
||
echo "$CHANGELOG" >> $GITHUB_OUTPUT
|
||
echo "EOF" >> $GITHUB_OUTPUT
|
||
|
||
- name: Create Release
|
||
id: create_release
|
||
uses: actions/create-release@v1
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
with:
|
||
tag_name: ${{ github.ref }}
|
||
release_name: Release v${{ steps.get_version.outputs.version }}
|
||
body: |
|
||
## Changes
|
||
${{ steps.changelog.outputs.changelog }}
|
||
|
||
## Downloads
|
||
- Cargo: `cargo install ruvector`
|
||
- npm: `npm install ruvector`
|
||
- WASM: Available in release assets
|
||
draft: false
|
||
prerelease: false
|
||
|
||
build-binaries:
|
||
name: Build - ${{ matrix.target }}
|
||
needs: create-release
|
||
runs-on: ${{ matrix.os }}
|
||
|
||
strategy:
|
||
matrix:
|
||
include:
|
||
- os: ubuntu-latest
|
||
target: x86_64-unknown-linux-gnu
|
||
node: '20'
|
||
- os: ubuntu-latest
|
||
target: aarch64-unknown-linux-gnu
|
||
node: '20'
|
||
- os: windows-latest
|
||
target: x86_64-pc-windows-msvc
|
||
node: '20'
|
||
- os: macos-latest
|
||
target: x86_64-apple-darwin
|
||
node: '20'
|
||
- os: macos-latest
|
||
target: aarch64-apple-darwin
|
||
node: '20'
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Setup Rust
|
||
uses: dtolnay/rust-toolchain@stable
|
||
with:
|
||
targets: ${{ matrix.target }}
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: ${{ matrix.node }}
|
||
cache: 'npm'
|
||
|
||
- name: Install cross-compilation tools
|
||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||
run: |
|
||
sudo apt-get update
|
||
sudo apt-get install -y gcc-aarch64-linux-gnu
|
||
|
||
- name: Install dependencies
|
||
run: npm ci
|
||
|
||
- name: Build
|
||
run: npm run build -- --target ${{ matrix.target }}
|
||
env:
|
||
CARGO_PROFILE_RELEASE_LTO: 'true'
|
||
|
||
- name: Package binary
|
||
id: package
|
||
run: |
|
||
ARCHIVE_NAME="ruvector-${{ needs.create-release.outputs.version }}-${{ matrix.target }}"
|
||
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
|
||
7z a ${ARCHIVE_NAME}.zip ./target/${{ matrix.target }}/release/*.node
|
||
echo "archive=${ARCHIVE_NAME}.zip" >> $GITHUB_OUTPUT
|
||
else
|
||
tar czf ${ARCHIVE_NAME}.tar.gz -C ./target/${{ matrix.target }}/release *.node
|
||
echo "archive=${ARCHIVE_NAME}.tar.gz" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
- name: Upload Release Asset
|
||
uses: actions/upload-release-asset@v1
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
with:
|
||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||
asset_path: ./${{ steps.package.outputs.archive }}
|
||
asset_name: ${{ steps.package.outputs.archive }}
|
||
asset_content_type: application/octet-stream
|
||
|
||
publish-cargo:
|
||
name: Publish to crates.io
|
||
needs: create-release
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Setup Rust
|
||
uses: dtolnay/rust-toolchain@stable
|
||
|
||
- name: Publish to crates.io
|
||
run: cargo publish --token ${{ secrets.CARGO_TOKEN }}
|
||
env:
|
||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }}
|
||
|
||
publish-npm:
|
||
name: Publish to npm
|
||
needs: [create-release, build-binaries]
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '20'
|
||
registry-url: 'https://registry.npmjs.org'
|
||
|
||
- name: Install dependencies
|
||
run: npm ci
|
||
|
||
- name: Build
|
||
run: npm run build
|
||
|
||
- name: Publish to npm
|
||
run: npm publish --access public
|
||
env:
|
||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||
|
||
publish-wasm:
|
||
name: Publish WASM Package
|
||
needs: create-release
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Setup Rust
|
||
uses: dtolnay/rust-toolchain@stable
|
||
with:
|
||
targets: wasm32-unknown-unknown
|
||
|
||
- name: Install wasm-pack
|
||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||
|
||
- name: Build WASM
|
||
run: wasm-pack build --target web --out-dir pkg/wasm
|
||
|
||
- name: Package WASM
|
||
run: |
|
||
cd pkg/wasm
|
||
tar czf ../../ruvector-${{ needs.create-release.outputs.version }}-wasm.tar.gz .
|
||
|
||
- name: Upload WASM Asset
|
||
uses: actions/upload-release-asset@v1
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
with:
|
||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||
asset_path: ./ruvector-${{ needs.create-release.outputs.version }}-wasm.tar.gz
|
||
asset_name: ruvector-${{ needs.create-release.outputs.version }}-wasm.tar.gz
|
||
asset_content_type: application/gzip
|
||
|
||
publish-github:
|
||
name: Publish to GitHub Packages
|
||
needs: create-release
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '20'
|
||
registry-url: 'https://npm.pkg.github.com'
|
||
scope: '@ruvector'
|
||
|
||
- name: Install dependencies
|
||
run: npm ci
|
||
|
||
- name: Build
|
||
run: npm run build
|
||
|
||
- name: Update package name for GitHub
|
||
run: |
|
||
node -e "
|
||
const pkg = require('./package.json');
|
||
pkg.name = '@ruvector/ruvector';
|
||
require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));
|
||
"
|
||
|
||
- name: Publish to GitHub Packages
|
||
run: npm publish
|
||
env:
|
||
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
```
|
||
|
||
## 3. Benchmark Workflow - Daily Performance
|
||
|
||
**File**: `.github/workflows/benchmark.yml`
|
||
|
||
```yaml
|
||
name: Benchmark
|
||
|
||
on:
|
||
schedule:
|
||
# Run daily at 2 AM UTC
|
||
- cron: '0 2 * * *'
|
||
push:
|
||
branches: [main]
|
||
paths:
|
||
- 'src/**'
|
||
- 'benches/**'
|
||
- 'Cargo.toml'
|
||
workflow_dispatch:
|
||
|
||
permissions:
|
||
contents: write
|
||
deployments: write
|
||
|
||
env:
|
||
CARGO_TERM_COLOR: always
|
||
|
||
jobs:
|
||
benchmark:
|
||
name: Run Benchmarks
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Setup Rust
|
||
uses: dtolnay/rust-toolchain@stable
|
||
|
||
- name: Setup Rust cache
|
||
uses: Swatinem/rust-cache@v2
|
||
|
||
- name: Install criterion
|
||
run: cargo install cargo-criterion
|
||
|
||
- name: Run benchmarks
|
||
run: cargo criterion --message-format=json > benchmark-results.json
|
||
|
||
- name: Parse benchmark results
|
||
id: parse
|
||
run: |
|
||
python3 <<EOF
|
||
import json
|
||
import os
|
||
|
||
with open('benchmark-results.json') as f:
|
||
results = [json.loads(line) for line in f if line.strip()]
|
||
|
||
benchmarks = []
|
||
for result in results:
|
||
if result.get('reason') == 'benchmark-complete':
|
||
name = result['id']
|
||
mean = result['mean']['estimate']
|
||
benchmarks.append({
|
||
'name': name,
|
||
'unit': 'ns',
|
||
'value': mean
|
||
})
|
||
|
||
with open('parsed-results.json', 'w') as f:
|
||
json.dump(benchmarks, f)
|
||
|
||
print(f"Parsed {len(benchmarks)} benchmarks")
|
||
EOF
|
||
|
||
- name: Download previous benchmark data
|
||
uses: actions/cache@v4
|
||
with:
|
||
path: ./cache
|
||
key: benchmark-${{ github.ref }}
|
||
|
||
- name: Check for regression
|
||
id: regression
|
||
run: |
|
||
python3 <<EOF
|
||
import json
|
||
import os
|
||
|
||
# Load current results
|
||
with open('parsed-results.json') as f:
|
||
current = {b['name']: b['value'] for b in json.load(f)}
|
||
|
||
# Load previous results
|
||
previous = {}
|
||
if os.path.exists('./cache/benchmark.json'):
|
||
with open('./cache/benchmark.json') as f:
|
||
previous = {b['name']: b['value'] for b in json.load(f)}
|
||
|
||
# Check for regressions (>5% slower)
|
||
regressions = []
|
||
for name, curr_val in current.items():
|
||
if name in previous:
|
||
prev_val = previous[name]
|
||
change = ((curr_val - prev_val) / prev_val) * 100
|
||
if change > 5.0:
|
||
regressions.append({
|
||
'name': name,
|
||
'previous': prev_val,
|
||
'current': curr_val,
|
||
'change': change
|
||
})
|
||
|
||
if regressions:
|
||
print("⚠️ Performance regressions detected:")
|
||
for r in regressions:
|
||
print(f" {r['name']}: +{r['change']:.2f}% slower")
|
||
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
||
f.write('has_regression=true\n')
|
||
exit(1)
|
||
else:
|
||
print("✅ No performance regressions detected")
|
||
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
||
f.write('has_regression=false\n')
|
||
EOF
|
||
continue-on-error: true
|
||
|
||
- name: Store benchmark result
|
||
uses: benchmark-action/github-action-benchmark@v1
|
||
with:
|
||
name: Rust Benchmark
|
||
tool: 'cargo'
|
||
output-file-path: benchmark-results.json
|
||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||
auto-push: true
|
||
alert-threshold: '105%'
|
||
comment-on-alert: true
|
||
fail-on-alert: true
|
||
alert-comment-cc-users: '@ruvector/maintainers'
|
||
|
||
- name: Save results for next run
|
||
run: |
|
||
mkdir -p ./cache
|
||
cp parsed-results.json ./cache/benchmark.json
|
||
|
||
- name: Generate benchmark report
|
||
run: |
|
||
python3 <<EOF
|
||
import json
|
||
|
||
with open('parsed-results.json') as f:
|
||
benchmarks = json.load(f)
|
||
|
||
print("# Benchmark Results\n")
|
||
print("| Benchmark | Time (ns) |")
|
||
print("|-----------|-----------|")
|
||
for b in sorted(benchmarks, key=lambda x: x['name']):
|
||
print(f"| {b['name']} | {b['value']:.2f} |")
|
||
EOF
|
||
|
||
- name: Comment on PR
|
||
if: github.event_name == 'pull_request'
|
||
uses: actions/github-script@v7
|
||
with:
|
||
script: |
|
||
const fs = require('fs');
|
||
const benchmarks = JSON.parse(fs.readFileSync('parsed-results.json', 'utf8'));
|
||
|
||
let comment = '## 📊 Benchmark Results\n\n';
|
||
comment += '| Benchmark | Time (ns) |\n';
|
||
comment += '|-----------|-----------||\n';
|
||
|
||
benchmarks.sort((a, b) => a.name.localeCompare(b.name));
|
||
for (const b of benchmarks) {
|
||
comment += `| ${b.name} | ${b.value.toFixed(2)} |\n`;
|
||
}
|
||
|
||
if ('${{ steps.regression.outputs.has_regression }}' === 'true') {
|
||
comment += '\n⚠️ **Performance regression detected!** See logs for details.\n';
|
||
}
|
||
|
||
github.rest.issues.createComment({
|
||
issue_number: context.issue.number,
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
body: comment
|
||
});
|
||
|
||
memory-benchmark:
|
||
name: Memory Usage Benchmark
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Setup Rust
|
||
uses: dtolnay/rust-toolchain@stable
|
||
|
||
- name: Install valgrind
|
||
run: sudo apt-get update && sudo apt-get install -y valgrind
|
||
|
||
- name: Build with debug info
|
||
run: cargo build --release --tests
|
||
|
||
- name: Run memory profiling
|
||
run: |
|
||
valgrind --tool=massif --massif-out-file=massif.out \
|
||
cargo test --release -- --test-threads=1
|
||
|
||
- name: Parse memory results
|
||
run: |
|
||
ms_print massif.out > memory-report.txt
|
||
grep "peak" memory-report.txt || true
|
||
|
||
- name: Upload memory report
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: memory-report
|
||
path: memory-report.txt
|
||
|
||
platform-benchmark:
|
||
name: Platform Benchmark - ${{ matrix.os }}
|
||
runs-on: ${{ matrix.os }}
|
||
|
||
strategy:
|
||
matrix:
|
||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Setup Rust
|
||
uses: dtolnay/rust-toolchain@stable
|
||
|
||
- name: Run platform-specific benchmarks
|
||
run: cargo bench --bench platform_benchmarks
|
||
|
||
- name: Upload results
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: benchmark-${{ matrix.os }}
|
||
path: target/criterion/
|
||
```
|
||
|
||
## 4. Dependabot Configuration
|
||
|
||
**File**: `.github/dependabot.yml`
|
||
|
||
```yaml
|
||
version: 2
|
||
|
||
updates:
|
||
# Cargo dependencies
|
||
- package-ecosystem: "cargo"
|
||
directory: "/"
|
||
schedule:
|
||
interval: "weekly"
|
||
day: "monday"
|
||
time: "06:00"
|
||
open-pull-requests-limit: 10
|
||
reviewers:
|
||
- "ruvector/maintainers"
|
||
labels:
|
||
- "dependencies"
|
||
- "rust"
|
||
commit-message:
|
||
prefix: "chore(deps):"
|
||
include: "scope"
|
||
ignore:
|
||
# Ignore major version updates for breaking changes
|
||
- dependency-name: "*"
|
||
update-types: ["version-update:semver-major"]
|
||
groups:
|
||
rust-dependencies:
|
||
patterns:
|
||
- "*"
|
||
update-types:
|
||
- "minor"
|
||
- "patch"
|
||
|
||
# npm dependencies
|
||
- package-ecosystem: "npm"
|
||
directory: "/"
|
||
schedule:
|
||
interval: "weekly"
|
||
day: "monday"
|
||
time: "06:00"
|
||
open-pull-requests-limit: 10
|
||
reviewers:
|
||
- "ruvector/maintainers"
|
||
labels:
|
||
- "dependencies"
|
||
- "npm"
|
||
commit-message:
|
||
prefix: "chore(deps):"
|
||
include: "scope"
|
||
ignore:
|
||
- dependency-name: "*"
|
||
update-types: ["version-update:semver-major"]
|
||
groups:
|
||
npm-dependencies:
|
||
patterns:
|
||
- "*"
|
||
update-types:
|
||
- "minor"
|
||
- "patch"
|
||
|
||
# GitHub Actions
|
||
- package-ecosystem: "github-actions"
|
||
directory: "/"
|
||
schedule:
|
||
interval: "weekly"
|
||
day: "monday"
|
||
time: "06:00"
|
||
open-pull-requests-limit: 5
|
||
reviewers:
|
||
- "ruvector/maintainers"
|
||
labels:
|
||
- "dependencies"
|
||
- "github-actions"
|
||
commit-message:
|
||
prefix: "chore(ci):"
|
||
include: "scope"
|
||
```
|
||
|
||
## 5. Quality Gates Configuration
|
||
|
||
**File**: `.github/workflows/quality-gate.yml`
|
||
|
||
```yaml
|
||
name: Quality Gate
|
||
|
||
on:
|
||
pull_request:
|
||
branches: [main, develop]
|
||
push:
|
||
branches: [main]
|
||
|
||
jobs:
|
||
coverage-gate:
|
||
name: Coverage Gate
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Setup Rust
|
||
uses: dtolnay/rust-toolchain@stable
|
||
|
||
- name: Install tarpaulin
|
||
run: cargo install cargo-tarpaulin
|
||
|
||
- name: Generate coverage
|
||
run: cargo tarpaulin --out Xml --out Json --output-dir ./coverage
|
||
|
||
- name: Check coverage threshold
|
||
run: |
|
||
COVERAGE=$(jq -r '.files | map(.coverage) | add / length' coverage/tarpaulin.json)
|
||
echo "Coverage: ${COVERAGE}%"
|
||
|
||
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
|
||
echo "❌ Coverage ${COVERAGE}% is below threshold 80%"
|
||
exit 1
|
||
fi
|
||
|
||
echo "✅ Coverage ${COVERAGE}% meets threshold"
|
||
|
||
- name: Comment coverage on PR
|
||
if: github.event_name == 'pull_request'
|
||
uses: actions/github-script@v7
|
||
with:
|
||
script: |
|
||
const fs = require('fs');
|
||
const coverage = JSON.parse(fs.readFileSync('coverage/tarpaulin.json', 'utf8'));
|
||
const total = coverage.files.reduce((sum, f) => sum + f.coverage, 0) / coverage.files.length;
|
||
|
||
const comment = `## 📊 Code Coverage Report
|
||
|
||
**Total Coverage**: ${total.toFixed(2)}%
|
||
**Threshold**: 80%
|
||
**Status**: ${total >= 80 ? '✅ PASS' : '❌ FAIL'}
|
||
|
||
### File Coverage
|
||
| File | Coverage |
|
||
|------|----------|
|
||
${coverage.files.slice(0, 10).map(f =>
|
||
`| ${f.path} | ${f.coverage.toFixed(2)}% |`
|
||
).join('\n')}
|
||
`;
|
||
|
||
github.rest.issues.createComment({
|
||
issue_number: context.issue.number,
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
body: comment
|
||
});
|
||
|
||
performance-gate:
|
||
name: Performance Gate
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 2
|
||
|
||
- name: Setup Rust
|
||
uses: dtolnay/rust-toolchain@stable
|
||
|
||
- name: Run benchmarks
|
||
run: cargo bench --bench performance_gate -- --save-baseline current
|
||
|
||
- name: Checkout base branch
|
||
run: git checkout HEAD~1
|
||
|
||
- name: Run baseline benchmarks
|
||
run: cargo bench --bench performance_gate -- --save-baseline baseline
|
||
|
||
- name: Compare benchmarks
|
||
run: |
|
||
cargo install critcmp
|
||
critcmp baseline current > comparison.txt
|
||
cat comparison.txt
|
||
|
||
# Check if any benchmark is >5% slower
|
||
if grep -q "change:.* +[0-9.]*%" comparison.txt; then
|
||
REGRESSION=$(grep "change:.* +[0-9.]*%" comparison.txt | awk '{print $NF}' | tr -d '%')
|
||
if (( $(echo "$REGRESSION > 5" | bc -l) )); then
|
||
echo "❌ Performance regression detected: ${REGRESSION}%"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
echo "✅ No significant performance regression"
|
||
|
||
clippy-gate:
|
||
name: Clippy Gate
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Setup Rust
|
||
uses: dtolnay/rust-toolchain@stable
|
||
with:
|
||
components: clippy
|
||
|
||
- name: Run Clippy
|
||
run: |
|
||
cargo clippy --all-targets --all-features \
|
||
--message-format=json -- -D warnings \
|
||
> clippy-results.json
|
||
|
||
- name: Check for warnings
|
||
run: |
|
||
WARNINGS=$(jq -r 'select(.reason == "compiler-message") | select(.message.level == "warning")' clippy-results.json | wc -l)
|
||
|
||
echo "Clippy warnings: $WARNINGS"
|
||
|
||
if [ "$WARNINGS" -gt 0 ]; then
|
||
echo "❌ Clippy warnings detected"
|
||
jq -r 'select(.reason == "compiler-message") | select(.message.level == "warning") | .message.message' clippy-results.json
|
||
exit 1
|
||
fi
|
||
|
||
echo "✅ No Clippy warnings"
|
||
|
||
security-gate:
|
||
name: Security Gate
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Run cargo-deny
|
||
uses: EmbarkStudios/cargo-deny-action@v1
|
||
with:
|
||
log-level: warn
|
||
command: check
|
||
arguments: --all-features
|
||
|
||
- name: Run cargo-audit
|
||
run: |
|
||
cargo install cargo-audit
|
||
cargo audit --deny warnings
|
||
|
||
- name: Run npm audit
|
||
run: npm audit --audit-level=high
|
||
|
||
size-gate:
|
||
name: Binary Size Gate
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Setup Rust
|
||
uses: dtolnay/rust-toolchain@stable
|
||
|
||
- name: Build release binary
|
||
run: cargo build --release
|
||
|
||
- name: Check binary size
|
||
run: |
|
||
SIZE=$(stat -c%s target/release/libruvector.so)
|
||
SIZE_MB=$(echo "scale=2; $SIZE / 1048576" | bc)
|
||
|
||
echo "Binary size: ${SIZE_MB}MB"
|
||
|
||
# Fail if binary is larger than 50MB
|
||
if (( $(echo "$SIZE_MB > 50" | bc -l) )); then
|
||
echo "❌ Binary size ${SIZE_MB}MB exceeds 50MB limit"
|
||
exit 1
|
||
fi
|
||
|
||
echo "✅ Binary size is acceptable"
|
||
```
|
||
|
||
## 6. Additional Workflow Files
|
||
|
||
### 6.1 PR Labeler
|
||
|
||
**File**: `.github/workflows/labeler.yml`
|
||
|
||
```yaml
|
||
name: Label PRs
|
||
|
||
on:
|
||
pull_request:
|
||
types: [opened, synchronize]
|
||
|
||
jobs:
|
||
label:
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- uses: actions/labeler@v5
|
||
with:
|
||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||
configuration-path: .github/labeler.yml
|
||
```
|
||
|
||
**File**: `.github/labeler.yml`
|
||
|
||
```yaml
|
||
rust:
|
||
- changed-files:
|
||
- any-glob-to-any-file: 'src/**/*.rs'
|
||
- any-glob-to-any-file: 'Cargo.toml'
|
||
|
||
typescript:
|
||
- changed-files:
|
||
- any-glob-to-any-file: '**/*.ts'
|
||
- any-glob-to-any-file: 'package.json'
|
||
|
||
documentation:
|
||
- changed-files:
|
||
- any-glob-to-any-file: 'docs/**/*'
|
||
- any-glob-to-any-file: '**/*.md'
|
||
|
||
tests:
|
||
- changed-files:
|
||
- any-glob-to-any-file: '**/*.test.ts'
|
||
- any-glob-to-any-file: 'tests/**/*'
|
||
|
||
ci:
|
||
- changed-files:
|
||
- any-glob-to-any-file: '.github/**/*'
|
||
```
|
||
|
||
### 6.2 Stale Issue Management
|
||
|
||
**File**: `.github/workflows/stale.yml`
|
||
|
||
```yaml
|
||
name: Mark Stale Issues and PRs
|
||
|
||
on:
|
||
schedule:
|
||
- cron: '0 0 * * *'
|
||
|
||
permissions:
|
||
issues: write
|
||
pull-requests: write
|
||
|
||
jobs:
|
||
stale:
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- uses: actions/stale@v9
|
||
with:
|
||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
|
||
stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
|
||
stale-issue-label: 'stale'
|
||
stale-pr-label: 'stale'
|
||
days-before-stale: 60
|
||
days-before-close: 7
|
||
exempt-issue-labels: 'pinned,security'
|
||
exempt-pr-labels: 'pinned,security'
|
||
```
|
||
|
||
## 7. Setup Instructions
|
||
|
||
### 7.1 Required Secrets
|
||
|
||
Add these secrets to your GitHub repository:
|
||
|
||
```bash
|
||
# Repository Settings > Secrets and Variables > Actions
|
||
|
||
CARGO_TOKEN # crates.io API token
|
||
NPM_TOKEN # npm registry token
|
||
CODECOV_TOKEN # Codecov upload token
|
||
```
|
||
|
||
### 7.2 Branch Protection Rules
|
||
|
||
Configure branch protection for `main`:
|
||
|
||
- Require pull request reviews before merging (1 approval)
|
||
- Require status checks to pass:
|
||
- `test` (all matrix jobs)
|
||
- `lint`
|
||
- `security`
|
||
- `wasm`
|
||
- `quality-gate`
|
||
- `coverage-gate`
|
||
- `performance-gate`
|
||
- `clippy-gate`
|
||
- Require branches to be up to date
|
||
- Require conversation resolution before merging
|
||
- Include administrators
|
||
|
||
### 7.3 CODEOWNERS
|
||
|
||
**File**: `.github/CODEOWNERS`
|
||
|
||
```
|
||
# Default owners for everything
|
||
* @ruvector/maintainers
|
||
|
||
# Rust code
|
||
*.rs @ruvector/rust-team
|
||
Cargo.toml @ruvector/rust-team
|
||
|
||
# TypeScript/JavaScript
|
||
*.ts @ruvector/typescript-team
|
||
*.js @ruvector/typescript-team
|
||
package.json @ruvector/typescript-team
|
||
|
||
# CI/CD workflows
|
||
.github/workflows/ @ruvector/devops-team
|
||
|
||
# Documentation
|
||
/docs/ @ruvector/docs-team
|
||
*.md @ruvector/docs-team
|
||
```
|
||
|
||
## 8. Monitoring and Alerts
|
||
|
||
### 8.1 Workflow Failure Notifications
|
||
|
||
**File**: `.github/workflows/notify-failures.yml`
|
||
|
||
```yaml
|
||
name: Notify on Workflow Failures
|
||
|
||
on:
|
||
workflow_run:
|
||
workflows: ["CI", "Release", "Benchmark"]
|
||
types: [completed]
|
||
|
||
jobs:
|
||
notify:
|
||
runs-on: ubuntu-latest
|
||
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
|
||
|
||
steps:
|
||
- name: Send Slack notification
|
||
uses: slackapi/slack-github-action@v1
|
||
with:
|
||
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||
payload: |
|
||
{
|
||
"text": "❌ Workflow failed: ${{ github.event.workflow_run.name }}",
|
||
"blocks": [
|
||
{
|
||
"type": "section",
|
||
"text": {
|
||
"type": "mrkdwn",
|
||
"text": "*Workflow Failed*\n*Workflow*: ${{ github.event.workflow_run.name }}\n*Branch*: ${{ github.event.workflow_run.head_branch }}\n*Run*: ${{ github.event.workflow_run.html_url }}"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
## 9. Performance Optimization Tips
|
||
|
||
### 9.1 Caching Strategy
|
||
|
||
```yaml
|
||
# Rust cache
|
||
- uses: Swatinem/rust-cache@v2
|
||
with:
|
||
cache-on-failure: true
|
||
shared-key: "rust-cache"
|
||
|
||
# npm cache
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
cache: 'npm'
|
||
|
||
# Custom cache for build artifacts
|
||
- uses: actions/cache@v4
|
||
with:
|
||
path: |
|
||
target/
|
||
node_modules/
|
||
key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.lock', '**/package-lock.json') }}
|
||
```
|
||
|
||
### 9.2 Parallel Job Execution
|
||
|
||
```yaml
|
||
jobs:
|
||
test:
|
||
strategy:
|
||
fail-fast: false
|
||
max-parallel: 10 # Run up to 10 jobs in parallel
|
||
matrix:
|
||
# Matrix configuration
|
||
```
|
||
|
||
### 9.3 Conditional Execution
|
||
|
||
```yaml
|
||
- name: Skip if docs only
|
||
if: |
|
||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||
!contains(github.event.head_commit.message, '[docs only]')
|
||
run: cargo test
|
||
```
|
||
|
||
## 10. Troubleshooting Guide
|
||
|
||
### Common Issues
|
||
|
||
1. **Build failures on ARM64**
|
||
- Ensure cross-compilation tools are installed
|
||
- Use appropriate runners or QEMU emulation
|
||
|
||
2. **Coverage threshold failures**
|
||
- Review untested code paths
|
||
- Add integration tests
|
||
- Consider adjusting threshold for infrastructure code
|
||
|
||
3. **Benchmark regressions**
|
||
- Check for algorithmic changes
|
||
- Review recent dependency updates
|
||
- Profile hot paths with flamegraph
|
||
|
||
4. **Release failures**
|
||
- Verify version bumps in Cargo.toml and package.json
|
||
- Ensure tokens are valid and have correct permissions
|
||
- Check that all platform builds succeeded
|
||
|
||
## 11. Success Metrics
|
||
|
||
The CI/CD pipeline should achieve:
|
||
|
||
- **Test coverage**: ≥80%
|
||
- **Build time**: <15 minutes per platform
|
||
- **Benchmark stability**: <5% variance
|
||
- **Release automation**: 100% (zero manual steps)
|
||
- **Security scan**: 0 high/critical vulnerabilities
|
||
- **Workflow reliability**: >99% success rate
|
||
|
||
## Implementation Checklist
|
||
|
||
- [ ] Create all workflow files in `.github/workflows/`
|
||
- [ ] Configure Dependabot in `.github/dependabot.yml`
|
||
- [ ] Add required secrets to repository settings
|
||
- [ ] Configure branch protection rules
|
||
- [ ] Set up CODEOWNERS file
|
||
- [ ] Enable GitHub Actions permissions
|
||
- [ ] Test CI workflow with draft PR
|
||
- [ ] Validate release workflow with pre-release tag
|
||
- [ ] Confirm benchmark automation works
|
||
- [ ] Set up notification channels (Slack, email)
|
||
- [ ] Document workflow for team
|
||
|
||
## Next Steps
|
||
|
||
1. Review and merge this CI/CD configuration
|
||
2. Test each workflow individually
|
||
3. Monitor first few automated releases
|
||
4. Iterate based on team feedback
|
||
5. Add platform-specific optimizations as needed
|
||
|
||
---
|
||
|
||
**Agent 15 Status**: Complete CI/CD pipeline ready for deployment
|