mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
Five gates broke on the 2026-04-27 push of the smart_crusher branch.
Each is fixed below; the second half adds a `make ci-precheck` target
(plus an installable git pre-push hook) so the same dance never happens
again.
Failures fixed:
1. cargo fmt — 22 files had formatting drift introduced over the
stage 3c.1 work. `cargo fmt --all` reformatted them; no semantic
changes. `cargo test --workspace` still green (388 + supporting).
2. wheels job (macOS x86_64) — `fastembed -> ort -> ort-sys` does not
publish prebuilt ONNX Runtime binaries for `x86_64-apple-darwin`.
Removed that target from `.github/workflows/rust.yml`'s wheels
matrix. Apple Silicon (`aarch64-apple-darwin`) covers macOS
distribution; Intel macOS users can build from source. The matrix
now has 2 targets: linux x86_64 + macOS aarch64.
3. test-extras (relevance.py) — `tests/test_relevance.py::TestSmartCrusherIntegration`
constructs a `SmartCrusher`, which hard-imports `headroom._core`
since the python implementation was retired in stage 3c.1b. The
test-extras job didn't build the rust extension. Added the same
`maturin build + symlink` block the main `test` job uses.
4. smoke-test (eval.yml) — same root cause:
`compression_only.evaluate_ccr_lossless` instantiates a SmartCrusher.
Same fix: build the rust extension before the smoke test runs.
5. commitlint — three rules tripped:
- `subject-case` rejects PascalCase identifiers in subjects, but
the project deliberately names classes (SmartCrusher, HfTokenizer,
ContentRouter, DiffCompressor) in commit subjects. Disabled.
- `footer-leading-blank` is a warning that the wagoid action turns
into a CI failure; lines like `Module: foo.rs` in our bodies
match the conventional footer pattern and trip it. Disabled.
- `type-enum` doesn't include `parity`, but the project ships
parity-test infrastructure as its own concern (separate from
`test:`); added `parity` to the allowed types.
Pre-push verification — the prevention half:
`make ci-precheck` runs all of the above CI gates locally:
- `ci-precheck-rust`: cargo fmt --check + clippy + test --workspace.
- `ci-precheck-python`: builds the rust extension via maturin, then
runs the smart_crusher-affected python test files (185 tests across
test_transforms/, test_relevance*, test_ccr, test_acceptance,
test_critical_fixes, test_quality_retention).
- `ci-precheck-commitlint`: `npx commitlint --from origin/main --to
HEAD` against the same config CI uses. Skipped silently if npx is
not on PATH (install Node 18+ to enable).
`make install-git-hooks` (or `scripts/install-git-hooks.sh`) installs
a git pre-push hook that runs `make ci-precheck` automatically.
Bypass with `--no-verify` only when truly needed.
When new CI gates land in `.github/workflows/`, mirror them into a
`make ci-precheck-*` target. The Makefile is the local mirror of the
CI configuration; keeping them in sync is a load-bearing invariant.
Verification: `make ci-precheck` runs green on this commit.
328 lines
11 KiB
YAML
328 lines
11 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Python ${{ matrix.python-version }}
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: ${{ matrix.python-version }}
|
|
|
|
- name: Cache pip packages
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ~/.cache/pip
|
|
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-pip-${{ matrix.python-version }}-
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
pip install -e ".[dev]"
|
|
|
|
# `headroom.transforms.diff_compressor` is now a thin pyo3 shim that
|
|
# delegates to `headroom._core` (built from `crates/headroom-py`). Without
|
|
# the wheel installed, every `DiffCompressor()` call raises
|
|
# `ModuleNotFoundError: No module named 'headroom._core'` and ~25 tests
|
|
# in `tests/test_transforms/test_diff_compressor.py` fail. The script
|
|
# below runs `maturin develop` and symlinks the built `.so` into the
|
|
# in-tree `headroom/` package so the editable install resolves it.
|
|
- name: Install Rust toolchain
|
|
uses: dtolnay/rust-toolchain@stable
|
|
|
|
- name: Cache cargo registry + build
|
|
uses: Swatinem/rust-cache@v2
|
|
with:
|
|
workspaces: ". -> target"
|
|
|
|
- name: Install maturin
|
|
run: |
|
|
pip install 'maturin>=1.5,<2.0'
|
|
|
|
# `scripts/build_rust_extension.sh` uses `maturin develop` which requires
|
|
# a virtualenv. CI runs against bare setup-python with no venv, so build
|
|
# a wheel + pip-install it instead. Then symlink the `.so` into the
|
|
# in-tree `headroom/` package so the editable install resolves
|
|
# `import headroom._core` past the source dir shadowing site-packages.
|
|
#
|
|
# We locate the `.so` via filesystem (not via `import headroom._core`)
|
|
# because the import would fail at this point — the editable install's
|
|
# in-tree `headroom/` directory shadows site-packages and doesn't
|
|
# contain the `.so` yet. That's exactly the problem this step fixes.
|
|
- name: Build Rust extension (headroom._core)
|
|
run: |
|
|
set -euo pipefail
|
|
maturin build --release -m crates/headroom-py/Cargo.toml --out dist
|
|
pip install --force-reinstall --no-deps dist/headroom_core_py-*.whl
|
|
SITE_PACKAGES=$(python -c "import site; print(site.getsitepackages()[0])")
|
|
SO_FILE=$(find "$SITE_PACKAGES/headroom" -maxdepth 1 -name "_core.cpython-*.so" -print -quit 2>/dev/null)
|
|
if [[ -z "$SO_FILE" ]]; then
|
|
echo "error: could not find _core.cpython-*.so under $SITE_PACKAGES/headroom/" >&2
|
|
ls -la "$SITE_PACKAGES/headroom/" || true
|
|
exit 1
|
|
fi
|
|
ln -sf "$SO_FILE" "headroom/$(basename "$SO_FILE")"
|
|
python -c "from headroom._core import DiffCompressor; print('headroom._core OK:', DiffCompressor)"
|
|
|
|
- name: Run linting
|
|
if: matrix.python-version == '3.12'
|
|
run: |
|
|
ruff check .
|
|
ruff format --check .
|
|
|
|
- name: Run type checking
|
|
if: matrix.python-version == '3.12'
|
|
run: |
|
|
mypy headroom --ignore-missing-imports
|
|
|
|
- name: Run tests
|
|
run: |
|
|
pytest -v --tb=short tests scripts/tests
|
|
|
|
- name: Run tests with coverage
|
|
if: matrix.python-version == '3.11'
|
|
run: |
|
|
pytest tests scripts/tests --cov=headroom --cov-report=xml --cov-report=term-missing
|
|
|
|
- name: Upload coverage to Codecov
|
|
if: matrix.python-version == '3.11'
|
|
uses: codecov/codecov-action@v4
|
|
with:
|
|
file: ./coverage.xml
|
|
fail_ci_if_error: false
|
|
|
|
test-extras:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.11"
|
|
|
|
- name: Install with relevance extras
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
pip install -e ".[dev,relevance]"
|
|
|
|
# `tests/test_relevance.py::TestSmartCrusherIntegration` constructs
|
|
# a `SmartCrusher`, which is a hard import of `headroom._core` since
|
|
# the Python implementation was retired in Stage 3c.1b. Without the
|
|
# extension built, those tests `ModuleNotFoundError`. Build + install
|
|
# the wheel and symlink the `.so` into the in-tree `headroom/` so
|
|
# the editable install resolves it (same pattern as the main `test`
|
|
# job above).
|
|
- name: Install Rust toolchain
|
|
uses: dtolnay/rust-toolchain@stable
|
|
|
|
- name: Cache cargo registry + build
|
|
uses: Swatinem/rust-cache@v2
|
|
with:
|
|
workspaces: ". -> target"
|
|
|
|
- name: Install maturin
|
|
run: pip install 'maturin>=1.5,<2.0'
|
|
|
|
- name: Build Rust extension (headroom._core)
|
|
run: |
|
|
set -euo pipefail
|
|
maturin build --release -m crates/headroom-py/Cargo.toml --out dist
|
|
pip install --force-reinstall --no-deps dist/headroom_core_py-*.whl
|
|
SITE_PACKAGES=$(python -c "import site; print(site.getsitepackages()[0])")
|
|
SO_FILE=$(find "$SITE_PACKAGES/headroom" -maxdepth 1 -name "_core.cpython-*.so" -print -quit 2>/dev/null)
|
|
if [[ -z "$SO_FILE" ]]; then
|
|
echo "error: could not find _core.cpython-*.so under $SITE_PACKAGES/headroom/" >&2
|
|
ls -la "$SITE_PACKAGES/headroom/" || true
|
|
exit 1
|
|
fi
|
|
ln -sf "$SO_FILE" "headroom/$(basename "$SO_FILE")"
|
|
python -c "from headroom._core import SmartCrusher; print('headroom._core OK:', SmartCrusher)"
|
|
|
|
- name: Run relevance tests
|
|
run: |
|
|
pytest tests/test_relevance.py -v
|
|
|
|
test-agno:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.11"
|
|
|
|
- name: Install with agno extras
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
pip install -e ".[dev,agno]"
|
|
|
|
- name: Run agno tests
|
|
run: |
|
|
pytest tests/test_integrations/agno/ -v
|
|
|
|
docker-native-e2e:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.11"
|
|
|
|
- name: Build local Headroom image
|
|
run: |
|
|
docker build -t headroom-native-e2e:latest .
|
|
|
|
- name: Run Docker-native installer e2e
|
|
env:
|
|
HEADROOM_DOCKER_IMAGE: headroom-native-e2e:latest
|
|
run: |
|
|
bash e2e/docker-native-install.sh
|
|
|
|
- name: Run Docker-native compose smoke test
|
|
env:
|
|
HEADROOM_IMAGE: headroom-native-e2e:latest
|
|
HEADROOM_HOST_HOME: ${{ github.workspace }}
|
|
HEADROOM_WORKSPACE: ${{ github.workspace }}
|
|
run: |
|
|
mkdir -p .headroom .claude .codex .gemini
|
|
trap 'docker compose -f docker/docker-compose.native.yml down -v' EXIT
|
|
docker compose -f docker/docker-compose.native.yml up -d proxy
|
|
for attempt in $(seq 1 30); do
|
|
if curl --fail --silent http://127.0.0.1:8787/readyz >/dev/null; then
|
|
break
|
|
fi
|
|
if [ "$attempt" -eq 30 ]; then
|
|
docker compose -f docker/docker-compose.native.yml logs proxy
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
- name: Run Docker-native wrap e2e
|
|
run: |
|
|
docker build -f e2e/wrap/Dockerfile -t headroom-wrap-e2e .
|
|
docker run --rm headroom-wrap-e2e
|
|
|
|
- name: Run Docker-native init e2e
|
|
run: |
|
|
docker build -f e2e/init/Dockerfile -t headroom-init-e2e .
|
|
docker run --rm headroom-init-e2e
|
|
|
|
windows-native-wrapper:
|
|
runs-on: windows-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Install test dependencies
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
pip install pytest
|
|
|
|
- name: Run native installer wrapper tests
|
|
run: |
|
|
pytest tests/test_install/test_native_installers.py -q
|
|
|
|
macos-native-wrapper:
|
|
runs-on: macos-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.11"
|
|
|
|
- name: Install bash and test dependencies
|
|
run: |
|
|
brew install bash
|
|
python -m pip install --upgrade pip
|
|
python -m pip install --retries 10 --timeout 60 pytest
|
|
|
|
- name: Run native installer wrapper tests
|
|
run: |
|
|
BASH_PREFIX="$(brew --prefix bash)"
|
|
export PATH="$BASH_PREFIX/bin:$PATH"
|
|
pytest tests/test_install/test_native_installers.py -q
|
|
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.10"
|
|
|
|
- name: Install build tools
|
|
run: |
|
|
python -m pip install --upgrade pip build twine
|
|
|
|
- name: Build package
|
|
run: |
|
|
python -m build
|
|
|
|
- name: Check package
|
|
run: |
|
|
twine check dist/*
|
|
|
|
- name: Upload artifacts
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: dist
|
|
path: dist/
|
|
|
|
commitlint:
|
|
if: github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'Merge pull request ')
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
- uses: wagoid/commitlint-github-action@v5
|
|
with:
|
|
configFile: .commitlintrc.json
|
|
|
|
workflow-validation:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Install actionlint
|
|
run: |
|
|
curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash | bash
|
|
sudo mv ./actionlint /usr/local/bin/actionlint
|
|
|
|
- name: Install act
|
|
run: |
|
|
curl -fsSL https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
|
|
sudo install ./bin/act /usr/local/bin/act
|
|
|
|
- name: Validate workflow files
|
|
run: |
|
|
bash scripts/validate-workflows.sh
|