chore: import upstream snapshot with attribution
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:03:20 +08:00
commit 0ef5fcb1c5
1951 changed files with 606278 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# CODEOWNERS — default reviewers for this repository.
# Docs: https://docs.github.com/articles/about-code-owners
#
# Owners listed here are auto-requested for review on matching pull requests.
# When branch protection requires code-owner review, any one of them can
# satisfy it. Owners must have write access to the repo or the line is ignored.
#
# Order matters: the last matching pattern wins.
# Catch-all: the maintainers own everything by default.
* @chopratejas @JerrettDavis @DevanshiVyas
+53
View File
@@ -0,0 +1,53 @@
---
name: Bug Report
about: Report a bug to help us improve Headroom
title: '[BUG] '
labels: bug
assignees: ''
---
## Description
A clear and concise description of what the bug is.
## To Reproduce
Steps to reproduce the behavior:
1. Install headroom with '...'
2. Run this code '...'
3. See error
## Expected Behavior
What you expected to happen.
## Actual Behavior
What actually happened.
## Code Sample
```python
# Minimal code to reproduce the issue
from headroom import HeadroomClient
# Your code here
```
## Error Output
```
Paste any error messages or stack traces here
```
## Environment
- **Headroom version**: (run `python -c "import headroom; print(headroom.__version__)"`)
- **Python version**: (run `python --version`)
- **OS**: (e.g., macOS 14.0, Ubuntu 22.04, Windows 11)
- **LLM Provider**: (e.g., OpenAI, Anthropic)
## Additional Context
Add any other context about the problem here (logs, screenshots, etc.)
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Questions & Discussions
url: https://github.com/chopratejas/headroom/discussions
about: Ask questions and discuss ideas in GitHub Discussions
- name: Documentation
url: https://headroom-docs.vercel.app/docs
about: Check out the documentation for guides and API reference
@@ -0,0 +1,54 @@
---
name: Copilot Subscription Test Report
about: Report results of testing `headroom wrap copilot --subscription` on Linux/Windows/macOS
title: '[COPILOT-SUB] <OS> test report'
labels: copilot-subscription, testing
assignees: ''
---
<!--
Thanks for helping verify Copilot subscription mode across platforms!
See TESTING-copilot-subscription.md for the step-by-step flows.
Redact your actual token everywhere.
-->
## Environment
- **OS + version**: (e.g., Ubuntu 24.04, Windows 11 23H2, macOS 14.5)
- **Architecture**: (x86_64 / arm64)
- **How you installed headroom**: (pipx/pip `--pre` wheel · Docker install.sh/ps1 · built from source)
- **headroom version**: (`headroom --version`)
- **Copilot CLI version**: (`copilot --version`)
- **Was plain `copilot` logged in before the test?**: yes / no
## Result
- **Command run**:
```
headroom wrap copilot --subscription -- --model gpt-4o -p "Reply with exactly: HEADROOM_OK"
```
- **Did it print `HEADROOM_OK`?**: yes / no
- **Worked WITHOUT `GITHUB_COPILOT_TOKEN` (auto-discovery)?**: yes / no / didn't try
- **Worked WITH `GITHUB_COPILOT_TOKEN` set?**: yes / no / didn't try
## Error output (if any)
```
paste any error here
```
## Token storage schema (only if auto-discovery failed)
Helps us fix auto-discovery. **Redact the secret value.**
- Linux: `secret-tool search --all 2>/dev/null | sed -E 's/^secret = .*/secret = <redacted>/'`
- Windows: `cmd /c "cmdkey /list"` (paste the Copilot-related `Target:` line)
- macOS (reference): service `copilot-cli`
```
paste the attribute / Target lines here (secret redacted)
```
## Anything else
(logs from `~/.headroom/logs/proxy.log`, surprises, etc.)
+44
View File
@@ -0,0 +1,44 @@
---
name: Feature Request
about: Suggest a new feature for Headroom
title: '[FEATURE] '
labels: enhancement
assignees: ''
---
## Problem Statement
A clear description of the problem you're trying to solve.
Ex: "I'm always frustrated when..."
## Proposed Solution
Describe the solution you'd like. Be as specific as possible.
## Use Case
Explain your use case and why this feature would be valuable:
- What type of application are you building?
- How would this feature help you?
- How many tokens/cost would this save?
## Alternatives Considered
Describe any alternative solutions or features you've considered.
## Example API (Optional)
If you have ideas about how the API should look:
```python
# How you'd like to use this feature
from headroom import SomeNewFeature
# Example usage
```
## Additional Context
- Are you willing to contribute this feature?
- Any relevant links, papers, or prior art?
+65
View File
@@ -0,0 +1,65 @@
## Description
<!-- Briefly explain the change and why it is needed. -->
Closes #
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
-
## Testing
<!-- Check what you actually ran, then paste the real command output below. -->
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
# Paste relevant command output or artifact links here
```
## Real Behavior Proof
- Environment:
- Exact command / steps:
- Observed result:
- Not tested:
## Review Readiness
- [ ] I have performed a self-review
- [ ] This PR is ready for human review
## Checklist
- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
Add screenshots to help explain your changes.
## Additional Notes
<!-- Mention any N/A checklist items, tradeoffs, follow-ups, or maintainer context. -->
+5
View File
@@ -0,0 +1,5 @@
{
"inputs": {
"version": "0.6.1"
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"inputs": {
"dry_run": "true"
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"action": "opened",
"number": 42,
"pull_request": {
"number": 42,
"draft": false,
"title": "feat: add PR governance",
"body": "## Description\n\nFixes #123\n",
"user": {
"login": "octocat"
},
"base": {
"sha": "dff6a199"
}
},
"repository": {
"full_name": "JerrettDavis/headroom"
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"action": "ready_for_review",
"number": 42,
"pull_request": {
"number": 42,
"draft": false,
"title": "feat: add PR governance",
"body": "## Description\n\nAdd a required PR governance check and commit-msg enforcement.\n\nCloses #123\n\n## Type of Change\n\n- [x] New feature (non-breaking change that adds functionality)\n\n## Changes Made\n\n- Added workflow validation for PR template completeness.\n- Added a commit-msg hook that runs commitlint locally.\n\n## Testing\n\n- [x] Unit tests pass (`pytest`)\n- [x] Manual testing performed\n\n### Test Output\n\n```text\npytest scripts/tests/test_pr_governance.py -q\n```\n\n## Real Behavior Proof\n\n- Environment: Ubuntu runner, Python 3.12\n- Exact command / steps: Opened a PR with an incomplete template, then fixed the body.\n- Observed result: The governance check failed until the template and readiness boxes were complete.\n- Not tested: Repository-level automatic Copilot rulesets.\n\n## Review Readiness\n\n- [x] I have performed a self-review\n- [x] This PR is ready for human review\n",
"user": {
"login": "octocat"
},
"base": {
"sha": "dff6a199"
}
},
"repository": {
"full_name": "JerrettDavis/headroom"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"ref": "refs/heads/main",
"commits": [
{
"message": "feat: add new release automation"
}
]
}
+11
View File
@@ -0,0 +1,11 @@
{
"ref": "refs/heads/main",
"head_commit": {
"message": "Merge pull request #217 from JerrettDavis/fix/openclaw-local-node-publish\n\nFix OpenClaw GPR package build"
},
"commits": [
{
"message": "Merge pull request #217 from JerrettDavis/fix/openclaw-local-node-publish\n\nFix OpenClaw GPR package build"
}
]
}
+11
View File
@@ -0,0 +1,11 @@
{
"action": "published",
"release": {
"tag_name": "v0.9.2",
"name": "Release v0.9.2",
"draft": false,
"prerelease": false,
"body": "## What's Changed\n\n* fix: example change for act dry-run simulation"
},
"ref": "refs/tags/v0.9.2"
}
@@ -0,0 +1,175 @@
name: Headroom e2e setup
description: >-
Checkout-agnostic setup shared by native e2e workflows (init, install, wrap).
Installs Python, optionally installs the Rust toolchain + editable headroom
package, and (optionally) drops PATH shims for the local ``headroom`` CLI and
target binaries so ``headroom init -g <target>`` can detect tools that aren't
actually installed on the runner.
inputs:
python-version:
description: Python version to install
required: false
default: "3.11"
install-mode:
description: >-
Install strategy. ``editable-proxy`` builds the local package with
``pip install -e .[proxy]`` and verifies ``headroom._core``.
``deps-only-proxy`` installs the base + ``[proxy]`` dependency set from
pyproject.toml, then drops a local ``headroom`` launcher that imports
from the checkout without building the package; use this for CLI tests
that do not exercise the Rust extension.
required: false
default: "editable-proxy"
shim-target:
description: >-
Name of the shim to drop on PATH (e.g. ``claude``, ``codex``). Leave
empty to skip shim creation.
required: false
default: ""
outputs:
shim-dir:
description: Absolute path to the directory containing the dropped shim
value: ${{ steps.shim.outputs.shim-dir }}
runs:
using: composite
steps:
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
# Single-wheel architecture: `pip install -e .` invokes maturin (declared
# in pyproject.toml's build-system) which calls cargo to compile the Rust
# extension. Toolchain has to be set up before the editable install path.
- name: Install Rust toolchain
if: ${{ inputs.install-mode == 'editable-proxy' }}
uses: dtolnay/rust-toolchain@1.95.0
- name: Cache cargo registry + build
if: ${{ inputs.install-mode == 'editable-proxy' }}
uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target"
# macos-latest (macos-15) runners have varying Xcode versions installed.
# The Rust cc crate probes the active Xcode for
# .../lib/clang/<ver>/lib/darwin/libclang_rt.osx.a. Some Xcode versions
# (notably 16.4 / clang 17 on certain runner images) lack this path.
# 1. Find an Xcode whose clang runtime directory actually exists.
# 2. If none found, locate libclang_rt.osx and create the expected symlink.
- name: Fix clang_rt.osx linker path (macOS)
if: runner.os == 'macOS'
shell: bash
run: |
found=
for app in /Applications/Xcode_*.app; do
[ -d "$app" ] || continue
clang_dir=$(ls -d "$app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/"*/lib/darwin 2>/dev/null | head -1)
if [ -n "$clang_dir" ]; then
sudo xcode-select -s "$app"
echo "Selected Xcode: $app (has clang runtime at $clang_dir)"
found=1
break
fi
done
if [ -z "$found" ]; then
rt_lib=$(find /Applications -name "libclang_rt.osx*" 2>/dev/null | head -1)
if [ -n "$rt_lib" ]; then
xcode_ver=$(xcodebuild -version 2>/dev/null | head -1 | awk '{print $2}')
clang_ver=$(clang --version 2>/dev/null | head -1 | grep -oP 'version \K\d+')
exp_dir="/Applications/Xcode_${xcode_ver}.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/${clang_ver}/lib/darwin"
sudo mkdir -p "$exp_dir"
target="$exp_dir/$(basename "$rt_lib")"
[ -f "$target" ] || sudo ln -sf "$rt_lib" "$target"
echo "Symlinked $rt_lib -> $target"
else
echo "WARNING: libclang_rt.osx not found anywhere. Build may fail."
fi
fi
- name: Install headroom (editable, with proxy extras — builds Rust extension)
if: ${{ inputs.install-mode == 'editable-proxy' }}
shell: bash
run: |
python -m pip install --upgrade pip
# ``headroom/cli/__init__.py`` eagerly imports ``proxy.server`` (via
# ``cli/proxy.py``), which requires ``fastapi`` even for ``init``.
# Install with the ``[proxy]`` extras to match the Docker e2e image.
pip install -e ".[proxy]"
python -c "from headroom._core import DiffCompressor; print('headroom._core OK:', DiffCompressor)"
- name: Install base + proxy dependencies without building headroom
if: ${{ inputs.install-mode == 'deps-only-proxy' }}
shell: bash
run: |
python -m pip install --upgrade pip
python - <<'PY'
import subprocess
import sys
import tomllib
from pathlib import Path
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))
requirements = list(project["project"]["dependencies"])
requirements.extend(project["project"]["optional-dependencies"]["proxy"])
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "--retries", "10", "--timeout", "60", *requirements]
)
PY
python -c "from headroom.cli.main import main; print('headroom CLI OK:', main)"
- name: Drop local headroom launcher (POSIX)
if: ${{ inputs.install-mode == 'deps-only-proxy' && runner.os != 'Windows' }}
shell: bash
run: |
shim_dir="${RUNNER_TEMP}/headroom-local-bin"
mkdir -p "$shim_dir"
cat > "$shim_dir/headroom" <<'SH'
#!/usr/bin/env bash
exec python -m headroom.cli "$@"
SH
chmod +x "$shim_dir/headroom"
echo "$shim_dir" >> "$GITHUB_PATH"
- name: Drop local headroom launcher (Windows)
if: ${{ inputs.install-mode == 'deps-only-proxy' && runner.os == 'Windows' }}
shell: pwsh
run: |
$shimDir = Join-Path $env:RUNNER_TEMP "headroom-local-bin"
New-Item -ItemType Directory -Force -Path $shimDir | Out-Null
@"
@echo off
python -m headroom.cli %*
"@ | Out-File -FilePath (Join-Path $shimDir "headroom.cmd") -Encoding ascii
Add-Content -Path $env:GITHUB_PATH -Value $shimDir
- name: Drop shim (POSIX)
if: ${{ inputs.shim-target != '' && runner.os != 'Windows' }}
id: shim-posix
shell: bash
run: |
shim_dir="${RUNNER_TEMP}/headroom-e2e-shims"
bash e2e/_lib/make_shim.sh "${{ inputs.shim-target }}" "$shim_dir"
echo "$shim_dir" >> "$GITHUB_PATH"
echo "shim-dir=$shim_dir" >> "$GITHUB_OUTPUT"
- name: Drop shim (Windows)
if: ${{ inputs.shim-target != '' && runner.os == 'Windows' }}
id: shim-windows
shell: pwsh
run: |
$shimDir = Join-Path $env:RUNNER_TEMP "headroom-e2e-shims"
& pwsh -File e2e/_lib/make_shim.ps1 -Name "${{ inputs.shim-target }}" -Dir $shimDir
Add-Content -Path $env:GITHUB_PATH -Value $shimDir
"shim-dir=$shimDir" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
- name: Export shim dir to job output
if: ${{ inputs.shim-target != '' }}
id: shim
shell: bash
run: |
if [ "${{ runner.os }}" = "Windows" ]; then
echo "shim-dir=${{ steps.shim-windows.outputs.shim-dir }}" >> "$GITHUB_OUTPUT"
else
echo "shim-dir=${{ steps.shim-posix.outputs.shim-dir }}" >> "$GITHUB_OUTPUT"
fi
+7
View File
@@ -0,0 +1,7 @@
When performing a pull request review in this repository:
1. Treat `.github/PULL_REQUEST_TEMPLATE.md` and `CONTRIBUTING.md` as required policy, not optional guidance.
2. Flag pull requests that do not include concrete "Real Behavior Proof" with environment, exact commands or steps, observed result, and what was not tested.
3. Be strict about contributor verification: missing tests, missing runtime evidence, or placeholder PR text should be called out.
4. For user-facing, release, dependency, workflow, or security-sensitive changes, prefer blocking feedback over optional suggestions.
5. Focus on correctness, safety, and whether the PR is actually ready for human maintainer review.
+74
View File
@@ -0,0 +1,74 @@
version: 2
updates:
# Docker base image digest updates
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
commit-message:
prefix: "docker"
groups:
docker-minor-patch:
update-types:
- "minor"
- "patch"
# GitHub Actions version updates
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
commit-message:
prefix: "ci"
groups:
actions-minor-patch:
update-types:
- "minor"
- "patch"
# Python dependency updates (pip)
- package-ecosystem: pip
directory: /
schedule:
interval: weekly
commit-message:
prefix: "deps"
# Only open PRs for security updates to avoid noise
open-pull-requests-limit: 5
groups:
pip-minor-patch:
update-types:
- "minor"
- "patch"
# Rust dependency updates (cargo workspace: crates/*)
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
commit-message:
prefix: "deps"
open-pull-requests-limit: 5
groups:
cargo-minor-patch:
update-types:
- "minor"
- "patch"
# npm dependency updates (TS SDK, plugins, docs site)
- package-ecosystem: npm
directories:
- "/sdk/typescript"
- "/plugins/openclaw"
- "/plugins/opencode"
- "/docs"
schedule:
interval: weekly
commit-message:
prefix: "deps"
open-pull-requests-limit: 5
groups:
npm-minor-patch:
update-types:
- "minor"
- "patch"
+30
View File
@@ -0,0 +1,30 @@
{
"name": "headroom-marketplace",
"owner": {
"name": "Headroom Contributors"
},
"metadata": {
"description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
"version": "0.31.0"
},
"plugins": [
{
"name": "headroom",
"source": "./plugins/headroom-agent-hooks",
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
"version": "0.31.0",
"author": {
"name": "Headroom Contributors",
"url": "https://github.com/chopratejas/headroom"
},
"homepage": "https://github.com/chopratejas/headroom",
"repository": "https://github.com/chopratejas/headroom",
"keywords": [
"headroom",
"hooks",
"claude-code",
"copilot-cli"
]
}
]
}
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Helpers for PR health maintenance labels."""
from __future__ import annotations
import argparse
import json
import sys
from datetime import datetime, timezone
from typing import Any
FAILING_STATES = {"FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED", "ERROR"}
def _parse_timestamp(value: Any) -> datetime:
if not isinstance(value, str) or not value:
return datetime.min.replace(tzinfo=timezone.utc)
normalized = value.removesuffix("Z") + "+00:00" if value.endswith("Z") else value
try:
parsed = datetime.fromisoformat(normalized)
except ValueError:
return datetime.min.replace(tzinfo=timezone.utc)
if parsed.tzinfo is None:
return parsed.replace(tzinfo=timezone.utc)
return parsed
def _check_key(check: dict[str, Any]) -> tuple[str, str]:
workflow = str(check.get("workflowName") or check.get("workflow") or "")
name = str(check.get("name") or check.get("context") or "")
return workflow, name
def _check_time(check: dict[str, Any]) -> datetime:
return max(
_parse_timestamp(check.get("startedAt")),
_parse_timestamp(check.get("completedAt")),
)
def _state(check: dict[str, Any]) -> str:
return str(check.get("conclusion") or check.get("state") or "").upper()
def current_checks(payload: dict[str, Any]) -> list[dict[str, Any]]:
latest_by_key: dict[tuple[str, str], dict[str, Any]] = {}
for check in payload.get("statusCheckRollup") or []:
if not isinstance(check, dict):
continue
key = _check_key(check)
if not any(key):
continue
previous = latest_by_key.get(key)
if previous is None or _check_time(check) >= _check_time(previous):
latest_by_key[key] = check
return list(latest_by_key.values())
def check_state(payload: dict[str, Any]) -> str:
for check in current_checks(payload):
if _state(check) in FAILING_STATES:
return "failing"
return "passing"
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--state-json", required=True, help="JSON from gh pr view")
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv or sys.argv[1:])
print(check_state(json.loads(args.state_json)))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+555
View File
@@ -0,0 +1,555 @@
name: CI
# Intelligent + parallel pipeline (cutover from the old 4-version matrix):
# changes — paths-filter; skips heavy work for docs-only changes
# build-wheel — compile the Rust ext ONCE (fast `ci` cargo profile), share via artifact
# lint — ruff + mypy, once
# prefetch-model — download the embedding model ONCE (authenticated), warm shared cache
# test — 4 parallel shards (pytest-split), each a fresh runner VM; run offline
# test-extras / test-agno / build / commitlint / workflow-validation / *-e2e — preserved
#
# Notes: CPU-only torch everywhere (no CUDA stack); test shards run HF_HUB_OFFLINE.
# Multi-version (3.10/3.11/3.13) coverage on main is a planned follow-up.
# Windows wheel (win_amd64) built separately — builds the Rust ext just like the
# Linux wheel, then uploads as a separate artifact for downstream consumption.
on:
push:
branches: [main]
pull_request:
branches: [main]
paths-ignore:
- 'docs/**'
- 'wiki/**'
- '**/*.md'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
# Cancel superseded runs on PRs/branches, but never cancel a main build.
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
PY_VERSION: "3.12"
# CPU-only torch — runners have no GPU; the default CUDA wheels pull ~2.5 GB.
PIP_EXTRA_INDEX_URL: https://download.pytorch.org/whl/cpu
jobs:
changes:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
code: ${{ steps.filter.outputs.code }}
e2e: ${{ steps.filter.outputs.e2e }}
workflows: ${{ steps.filter.outputs.workflows }}
steps:
- uses: actions/checkout@v7
- uses: dorny/paths-filter@v4
id: filter
with:
filters: |
code:
- 'headroom/**'
- 'crates/**'
- '**/*.rs'
- 'pyproject.toml'
- 'Cargo.toml'
- 'Cargo.lock'
- 'tests/**'
- 'scripts/**'
- '.github/workflows/**'
e2e:
- 'headroom/**'
- 'crates/**'
- 'docker/**'
- 'Dockerfile'
- 'e2e/**'
- 'scripts/install*'
- 'pyproject.toml'
workflows:
- '.github/workflows/**'
lint:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PY_VERSION }}
- name: Cache pip
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-lint-${{ hashFiles('pyproject.toml') }}
restore-keys: ${{ runner.os }}-pip-lint-
- run: python -m pip install --upgrade pip "ruff==0.15.17" "mypy==1.20.2"
- name: ruff check
run: ruff check .
- name: ruff format --check
run: ruff format --check .
- name: mypy
run: mypy headroom --ignore-missing-imports
build-wheel:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PY_VERSION }}
- uses: dtolnay/rust-toolchain@1.96.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target"
- name: Build wheel once (fast CI cargo profile)
run: |
python -m pip install --upgrade pip maturin
maturin build --profile ci --out dist --interpreter "python${PY_VERSION}"
- uses: actions/upload-artifact@v7
with:
name: headroom-wheel
path: dist/*.whl
retention-days: 1
build-wheel-windows:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: windows-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PY_VERSION }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target"
- name: Build wheel (fast CI cargo profile)
shell: bash
run: |
python -m pip install --upgrade pip maturin
maturin build --profile ci --out dist --interpreter "python${{ env.PY_VERSION }}"
- uses: actions/upload-artifact@v7
with:
name: headroom-wheel-windows
path: dist/*.whl
retention-days: 1
prefetch-model:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PY_VERSION }}
- name: Cache HuggingFace model
id: hfcache
uses: actions/cache@v6
with:
path: ~/.cache/huggingface
key: ${{ runner.os }}-models-allMiniLM-v2
- name: Fetch all-MiniLM-L6-v2 once (authenticated, resilient)
if: steps.hfcache.outputs.cache-hit != 'true'
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
HF_HUB_DISABLE_TELEMETRY: "1"
run: |
python -m pip install --upgrade pip huggingface_hub
for i in 1 2 3 4 5 6; do
if python -c "from huggingface_hub import snapshot_download; snapshot_download('sentence-transformers/all-MiniLM-L6-v2')"; then exit 0; fi
echo "::warning::model fetch attempt $i failed; backing off"; sleep $((i * 30))
done
echo "::error::could not fetch all-MiniLM-L6-v2 from HuggingFace"; exit 1
test:
needs: [changes, build-wheel, prefetch-model]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
env:
TRANSFORMERS_OFFLINE: "1"
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PY_VERSION }}
- name: Cache pip
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ env.PY_VERSION }}-${{ hashFiles('pyproject.toml') }}
restore-keys: ${{ runner.os }}-pip-${{ env.PY_VERSION }}-
- name: Restore HuggingFace model cache (warmed by prefetch-model)
id: restore-hfcache
uses: actions/cache@v6
with:
path: ~/.cache/huggingface
key: ${{ runner.os }}-models-allMiniLM-v2
- name: Fallback model download if cache missed
if: steps.restore-hfcache.outputs.cache-hit != 'true'
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
HF_HUB_DISABLE_TELEMETRY: "1"
TRANSFORMERS_OFFLINE: "0"
HF_HUB_OFFLINE: "0"
run: |
python -m pip install --upgrade pip huggingface_hub
for i in 1 2 3 4 5 6; do
if python -c "from huggingface_hub import snapshot_download; snapshot_download('sentence-transformers/all-MiniLM-L6-v2')"; then exit 0; fi
if [ "$i" -lt 6 ]; then echo "::warning::fallback model fetch attempt $i failed; backing off"; sleep $((i * 30)); fi
done
echo "::error::could not fetch all-MiniLM-L6-v2 from HuggingFace (fallback)"; exit 1
- name: Download prebuilt wheel
uses: actions/download-artifact@v8
with:
name: headroom-wheel
path: dist
- name: Install (CPU torch + prebuilt wheel + dev deps, no cargo rebuild)
run: |
python -m pip install --upgrade pip
pip install torch --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple
WHEEL="$(ls dist/*.whl)"
pip install "${WHEEL}[dev]" pytest-split
# cwd's ./headroom source tree shadows the installed wheel; copy the
# compiled extension in so tests import it (no second cargo build).
SITE="$(python -c 'import sysconfig; print(sysconfig.get_path("platlib"))')"
cp "${SITE}/headroom/"_core*.so headroom/
python -c "from headroom._core import DiffCompressor; print('headroom._core OK')"
- name: Verify offline HuggingFace model cache
env:
HF_HUB_OFFLINE: "1"
TRANSFORMERS_OFFLINE: "1"
HF_HUB_DISABLE_TELEMETRY: "1"
run: python scripts/ci/verify_hf_model_cache.py
# Coverage upload: without this, codecov only receives reports from
# the two native-e2e workflows (3 CLI test files total), so head
# coverage reads ~6% and codecov/patch fails for ANY diff not
# exercised by those files — a false negative on every PR. The main
# suite runs here; its coverage must be what codecov sees.
- name: Run test shard ${{ matrix.shard }}/4
run: |
pytest tests scripts/tests \
--splits 4 --group ${{ matrix.shard }} \
--cov=headroom --cov-branch \
--cov-report=xml:coverage-${{ matrix.shard }}.xml \
--cov-report= \
--tb=short -q
- name: Upload coverage shard ${{ matrix.shard }} to Codecov
uses: codecov/codecov-action@v5
with:
files: coverage-${{ matrix.shard }}.xml
flags: python
name: python-shard-${{ matrix.shard }}
# Token is sent so uploads authenticate once the repo is activated on
# Codecov. Until then Codecov may 404 ("Repository not found"); either
# way, coverage upload is reporting-only and must never fail a build
# whose tests pass — so this stays non-blocking.
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
test-extras:
needs: [changes, build-wheel]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
env:
FASTEMBED_CACHE_PATH: ${{ github.workspace }}/.fastembed-cache
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PY_VERSION }}
- name: Cache pip
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-extras-${{ hashFiles('pyproject.toml') }}
restore-keys: ${{ runner.os }}-pip-extras-
- name: Cache fastembed model
uses: actions/cache@v6
with:
path: ${{ github.workspace }}/.fastembed-cache
key: ${{ runner.os }}-fastembed-bge-small-v1
- name: Download prebuilt wheel
uses: actions/download-artifact@v8
with:
name: headroom-wheel
path: dist
- name: Install (CPU torch + wheel[dev,relevance])
run: |
python -m pip install --upgrade pip
pip install torch --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple
WHEEL="$(ls dist/*.whl)"
pip install "${WHEEL}[dev,relevance]"
SITE="$(python -c 'import sysconfig; print(sysconfig.get_path("platlib"))')"
cp "${SITE}/headroom/"_core*.so headroom/
python -c "from headroom._core import SmartCrusher; print('headroom._core OK')"
- name: Pre-fetch fastembed model (authenticated, resilient)
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
HF_HUB_DISABLE_TELEMETRY: "1"
run: |
for i in 1 2 3 4 5; do
if python -c "from fastembed import TextEmbedding; TextEmbedding('BAAI/bge-small-en-v1.5')"; then exit 0; fi
echo "::warning::fastembed fetch attempt $i failed; backing off"; sleep $((i * 20))
done
echo "::error::could not fetch fastembed model from HuggingFace"; exit 1
- name: Run relevance tests
# Offline so fastembed reads the cache the prefetch step just warmed,
# without an unauthenticated cache-validation HEAD that could 429.
env:
HF_HUB_OFFLINE: "1"
TRANSFORMERS_OFFLINE: "1"
run: pytest tests/test_relevance.py -v
test-agno:
needs: [changes, build-wheel]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PY_VERSION }}
- name: Download prebuilt wheel
uses: actions/download-artifact@v8
with:
name: headroom-wheel
path: dist
- name: Install (CPU torch + wheel[dev,agno])
run: |
python -m pip install --upgrade pip
pip install torch --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple
WHEEL="$(ls dist/*.whl)"
pip install "${WHEEL}[dev,agno]"
SITE="$(python -c 'import sysconfig; print(sysconfig.get_path("platlib"))')"
cp "${SITE}/headroom/"_core*.so headroom/
- name: Run agno tests
run: pytest tests/test_integrations/agno/ -v
test-dashboard-ui:
needs: [changes, build-wheel]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PY_VERSION }}
- name: Download prebuilt wheel
uses: actions/download-artifact@v8
with:
name: headroom-wheel
path: dist
- name: Install (CPU torch + wheel[dev] + playwright)
run: |
python -m pip install --upgrade pip
pip install torch --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple
WHEEL="$(ls dist/*.whl)"
pip install "${WHEEL}[dev]" playwright
SITE="$(python -c 'import sysconfig; print(sysconfig.get_path("platlib"))')"
cp "${SITE}/headroom/"_core*.so headroom/
- name: Install chromium
run: playwright install --with-deps chromium
- name: Run dashboard playwright tests
# Stub-based dashboard tests only (routes fully mocked, no network).
# tests/test_dashboard/test_live_feed.py needs a live proxy on
# localhost:8787 and stays excluded; the main shards keep skipping
# these via importorskip since playwright is not installed there.
env:
HEADROOM_PLAYWRIGHT_ARTIFACT_DIR: ${{ runner.temp }}/playwright-artifacts
run: pytest tests/test_dashboard_*_playwright.py -v
- name: Upload dashboard screenshots
if: always()
uses: actions/upload-artifact@v7
with:
name: dashboard-playwright-artifacts
path: ${{ runner.temp }}/playwright-artifacts
if-no-files-found: ignore
retention-days: 7
commitlint:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: wagoid/commitlint-github-action@v6
with:
configFile: .commitlintrc.json
build:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Cache pip
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-build-${{ hashFiles('pyproject.toml') }}
restore-keys: ${{ runner.os }}-pip-build-
- uses: dtolnay/rust-toolchain@1.96.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target"
# Smoke check that the SHIPPED build (release profile) + sdist are wired
# right; release.yml's matrix is what actually publishes to PyPI.
- name: Install build tools
run: |
python -m pip install --upgrade pip
pip install 'maturin>=1.5,<2.0' twine
- name: Build wheel + sdist
run: |
maturin sdist --out dist
maturin build --release --out dist
- name: Check package
run: twine check dist/*
workflow-validation:
needs: changes
if: needs.changes.outputs.workflows == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- name: Cache actionlint + act
id: tools-cache
uses: actions/cache@v6
with:
path: |
/usr/local/bin/actionlint
/usr/local/bin/act
# Key off the workflow file itself: when someone updates the
# download URLs to a newer tool version, the hash changes and
# the cache busts automatically.
key: ${{ runner.os }}-ci-tools-${{ hashFiles('.github/workflows/ci.yml') }}
- name: Install actionlint
if: steps.tools-cache.outputs.cache-hit != 'true'
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
if: steps.tools-cache.outputs.cache-hit != 'true'
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
docker-native-e2e:
needs: changes
if: needs.changes.outputs.e2e == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
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:
needs: changes
if: needs.changes.outputs.e2e == 'true'
runs-on: windows-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
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:
needs: changes
if: needs.changes.outputs.e2e == 'true'
runs-on: macos-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
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
+119
View File
@@ -0,0 +1,119 @@
name: Dev Containers
on:
push:
branches: [main]
paths:
- ".devcontainer/**"
- ".github/workflows/devcontainers.yml"
- "pyproject.toml"
- "uv.lock"
pull_request:
branches: [main]
paths:
- ".devcontainer/**"
- ".github/workflows/devcontainers.yml"
- "pyproject.toml"
- "uv.lock"
workflow_dispatch:
jobs:
validate:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: default
config: .devcontainer/devcontainer.json
- name: memory-stack
config: .devcontainer/memory-stack/devcontainer.json
steps:
- uses: actions/checkout@v7
# Both variants exhaust the GitHub runner's disk, so free space up front
# on every variant. memory-stack brings up Neo4j + Postgres + Redis +
# Qdrant (PR #495: the diagnostic log writer hit "No space left on device"
# mid-smoke-test). The default variant's post-create `uv sync` fills the
# disk installing the ML wheel set — it failed copying numpy into the venv
# with "No space left on device" (os error 28). Previously this ran only
# on memory-stack, which left the default variant with no cushion.
#
# Reclaim ~14 GB by stripping preinstalled tools none of the devcontainer
# paths use (Android SDK, .NET, Haskell).
- name: Free runner disk
uses: jlumbroso/free-disk-space@v1.3.1
with:
tool-cache: true
android: true
dotnet: true
haskell: true
large-packages: false
docker-images: false
swap-storage: false
- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: "20"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Install Dev Container CLI
run: npm install -g @devcontainers/cli@0.85.0
- name: Start ${{ matrix.name }}
run: devcontainer up --workspace-folder . --config ${{ matrix.config }} --remove-existing-container
- name: Smoke test ${{ matrix.name }}
shell: bash
run: |
if [[ "${{ matrix.name }}" == "memory-stack" ]]; then
devcontainer exec --workspace-folder . --config ${{ matrix.config }} bash -lc 'git rev-parse --show-toplevel >/dev/null && uv --version && node --version && gh --version >/dev/null && uv run python -c "import socket; socket.create_connection((\"qdrant\", 6333), 5).close(); socket.create_connection((\"neo4j\", 7687), 5).close(); from mem0 import Memory; from qdrant_client import QdrantClient; import neo4j; import headroom; print(\"memory-stack smoke test passed\")"'
else
devcontainer exec --workspace-folder . --config ${{ matrix.config }} bash -lc 'git rev-parse --show-toplevel >/dev/null && uv --version && node --version && gh --version >/dev/null && uv run python -c "import headroom; print(\"default smoke test passed\")"'
fi
validate-worktree:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# The worktree devcontainer runs the same `uv sync --extra dev` build as
# the default validate job, which now pulls transformers 5.x. Copying that
# into the venv volume exhausts the GitHub runner's disk ("No space left on
# device", see PR #495). Reclaim ~14 GB by stripping preinstalled tools the
# build never touches — mirrors the memory-stack job's existing remedy.
- name: Free runner disk
uses: jlumbroso/free-disk-space@v1.3.1
with:
tool-cache: true
android: true
dotnet: true
haskell: true
large-packages: false
docker-images: false
swap-storage: false
- name: Create linked worktree
run: git worktree add "$RUNNER_TEMP/headroom-worktree" HEAD
- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: "20"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Install Dev Container CLI
run: npm install -g @devcontainers/cli@0.85.0
- name: Start linked worktree devcontainer
run: devcontainer up --workspace-folder "$RUNNER_TEMP/headroom-worktree" --config "$RUNNER_TEMP/headroom-worktree/.devcontainer/devcontainer.json" --remove-existing-container
- name: Smoke test linked worktree
run: devcontainer exec --workspace-folder "$RUNNER_TEMP/headroom-worktree" --config "$RUNNER_TEMP/headroom-worktree/.devcontainer/devcontainer.json" bash -lc 'git rev-parse --show-toplevel && uv run python -c "import headroom; print(\"worktree smoke test passed\")"'
+428
View File
@@ -0,0 +1,428 @@
name: Docker
on:
push:
branches: [main]
workflow_call:
inputs:
version:
description: "Version to stamp into the image contents and exact image tag"
required: false
type: string
enable_ref_tags:
description: "Whether to emit branch/PR ref tags"
required: false
default: true
type: boolean
workflow_dispatch:
inputs:
version:
description: "Version to stamp into the image contents and exact image tag"
required: false
release:
types: [published]
env:
REGISTRY: ghcr.io
permissions:
contents: read
packages: write
id-token: write # For cosign keyless signing via Sigstore OIDC
jobs:
# ─── Per-arch fan-out ──────────────────────────────────────────────────────
# Build each variant on its native architecture in parallel:
# linux/amd64 → ubuntu-24.04 (native x86_64)
# linux/arm64 → ubuntu-24.04-arm (native aarch64, GA Jan 2025)
#
# Pre-#377 we ran a single matrix job per variant on `ubuntu-latest` and
# let bake's `platforms = ["linux/amd64", "linux/arm64"]` do multi-arch
# via QEMU emulation — ~1h per variant. Native arm64 runners drop QEMU
# entirely and cut each variant to ~10 min on each arch in parallel.
#
# Each per-arch build pushes by digest only (no tags). The
# `docker-manifest` job below combines the per-arch digests into the
# final multi-arch tagged manifest, which is what users pull by tag.
docker-build:
runs-on: ${{ matrix.arch.runs_on }}
timeout-minutes: 75
strategy:
fail-fast: false
matrix:
variant:
- { name: "", bake_target: runtime }
- { name: nonroot, bake_target: runtime-nonroot }
- { name: code, bake_target: runtime-code }
- { name: code-nonroot, bake_target: runtime-code-nonroot }
- { name: slim, bake_target: runtime-slim }
- { name: slim-nonroot, bake_target: runtime-slim-nonroot }
- { name: code-slim, bake_target: runtime-code-slim }
- { name: code-slim-nonroot, bake_target: runtime-code-slim-nonroot }
arch:
- { name: amd64, runs_on: ubuntu-24.04, platform: linux/amd64 }
- { name: arm64, runs_on: ubuntu-24.04-arm, platform: linux/arm64 }
steps:
- uses: actions/checkout@v7
- name: Normalize image name
id: image-name
run: |
image_name="$(printf '%s' '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
printf 'image_name=%s\n' "$image_name" >> "$GITHUB_OUTPUT"
- name: Determine image version
id: version
env:
MANUAL_VERSION: ${{ inputs.version || github.event.inputs.version }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
version="${MANUAL_VERSION#v}"
if [ -z "$version" ] && [ -n "$RELEASE_TAG" ]; then
version="${RELEASE_TAG#v}"
fi
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
- name: Set up Python
if: steps.version.outputs.version != ''
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Sync versioned files for image build
if: steps.version.outputs.version != ''
run: |
python scripts/version-sync.py --version ${{ steps.version.outputs.version }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Labels (not tags) for the per-arch image. Tags belong on the
# multi-arch index manifest and are applied in docker-manifest.
- name: Extract image labels
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}
- name: Build and push by digest (single platform)
id: bake
uses: docker/bake-action@v7
with:
files: |
./docker-bake.hcl
cwd://${{ steps.meta.outputs.bake-file-labels }}
targets: ${{ matrix.variant.bake_target }}
push: true
# `*.platform` overrides the [amd64,arm64] default in
# docker-bake.hcl. `push-by-digest=true,name-canonical=true`
# tells buildx to push the per-platform manifest with no tags
# — only the digest is recorded — so multiple per-arch builds
# can coexist in the registry until the manifest job stitches
# them. `name=<registry>/<image>` is REQUIRED here: with no
# `bake-file-tags` in scope (tags belong on the manifest, not
# per-arch), bake has no way to know the push target without
# the explicit `name=`. Removing it surfaces as the
# misleading "ERROR: tag is needed when pushing to registry"
# — see PR #378 (regression from #376). GHA cache is scoped
# per (variant, arch) so the two arches don't fight over the
# same cache key.
set: |
*.platform=${{ matrix.arch.platform }}
*.output=type=image,name=${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
*.cache-from=type=gha,scope=${{ matrix.variant.name || 'root' }}-${{ matrix.arch.name }}
*.cache-to=type=gha,mode=max,scope=${{ matrix.variant.name || 'root' }}-${{ matrix.arch.name }}
- name: Export digest
id: digest
env:
BAKE_METADATA: ${{ steps.bake.outputs.metadata }}
run: |
# Bake's metadata is one entry per target; for a single-target
# single-platform build it has exactly one digest. Pipe the
# JSON through a file (same ARG_MAX rationale as before) and
# extract that digest.
cat > "${RUNNER_TEMP}/bake_meta.json" <<'__HEADROOM_BAKE_META_EOF__'
${{ steps.bake.outputs.metadata }}
__HEADROOM_BAKE_META_EOF__
digest="$(jq -r 'to_entries[0].value."containerimage.digest" // empty' \
"${RUNNER_TEMP}/bake_meta.json")"
if [ -z "$digest" ]; then
echo "ERROR: no digest in bake metadata" >&2
cat "${RUNNER_TEMP}/bake_meta.json" >&2
exit 1
fi
printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT"
# Stage a marker file named after the bare hex digest. The
# manifest job downloads all per-arch markers for a variant
# and reconstructs `IMAGE@sha256:<digest>` references from
# the filenames.
mkdir -p "${RUNNER_TEMP}/digests"
touch "${RUNNER_TEMP}/digests/${digest#sha256:}"
# Smoke-test the built image before recording its digest. If the
# Python ABI is wrong (e.g. builder Python 3.11 vs distroless
# Python 3.13) pydantic_core._pydantic_core fails to dlopen and
# the import raises ModuleNotFoundError. Catching it here prevents
# a broken digest from reaching the manifest merge job and being
# tagged and published. Both python-slim and distroless variants
# expose python3 in PATH and honour the image's PYTHONPATH env.
- name: Smoke-test image (pydantic_core + headroom._core)
env:
IMAGE: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}
DIGEST: ${{ steps.digest.outputs.digest }}
PLATFORM: ${{ matrix.arch.platform }}
run: |
docker run --rm \
--platform "$PLATFORM" \
--entrypoint python3 \
"${IMAGE}@${DIGEST}" \
-c "
import pydantic_core
from headroom._core import DiffCompressor, SmartCrusher
print('smoke-test OK: pydantic_core', pydantic_core.__version__,
'| DiffCompressor', DiffCompressor.__name__,
'| SmartCrusher', SmartCrusher.__name__)
"
- name: Upload digest marker
uses: actions/upload-artifact@v7
with:
# Variant + arch in the artifact name so the manifest job can
# download with `pattern: digests-<variant>-*` to gather all
# arches for one variant. `root` substitutes the empty-string
# variant since GHA artifact names can't end in a hyphen.
name: digests-${{ matrix.variant.name || 'root' }}-${{ matrix.arch.name }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
# ─── Per-variant manifest merge ────────────────────────────────────────────
# One job per variant, after both arch builds for that variant complete.
# `docker buildx imagetools create` stitches the two per-arch digests
# into a single multi-arch index manifest, applies the metadata-action
# tags, and that manifest is what users pull by `:tag`.
docker-manifest:
needs: docker-build
runs-on: ubuntu-24.04
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
variant:
- { name: "", bake_target: runtime }
- { name: nonroot, bake_target: runtime-nonroot }
- { name: code, bake_target: runtime-code }
- { name: code-nonroot, bake_target: runtime-code-nonroot }
- { name: slim, bake_target: runtime-slim }
- { name: slim-nonroot, bake_target: runtime-slim-nonroot }
- { name: code-slim, bake_target: runtime-code-slim }
- { name: code-slim-nonroot, bake_target: runtime-code-slim-nonroot }
steps:
# No `actions/checkout` here: the manifest job only calls
# `docker buildx imagetools` against the registry and runs
# cosign — neither needs the repo on disk. Skipping checkout
# saves a few seconds across 8 parallel manifest jobs.
- name: Normalize image name
id: image-name
run: |
image_name="$(printf '%s' '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
printf 'image_name=%s\n' "$image_name" >> "$GITHUB_OUTPUT"
- name: Determine image version
id: version
env:
MANUAL_VERSION: ${{ inputs.version || github.event.inputs.version }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
version="${MANUAL_VERSION#v}"
if [ -z "$version" ] && [ -n "$RELEASE_TAG" ]; then
version="${RELEASE_TAG#v}"
fi
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
- name: Compute short SHA
id: short-sha
run: printf 'sha=%s\n' "${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download per-arch digests for this variant
uses: actions/download-artifact@v8
with:
pattern: digests-${{ matrix.variant.name || 'root' }}-*
path: ${{ runner.temp }}/digests
merge-multiple: true
# Same tag rules as the pre-fan-out workflow — preserve every
# tag flavor (semver, ref, sha-prefixed, version-suffixed,
# bare variant) so existing pull URLs keep working.
- name: Extract metadata (variant)
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}
tags: |
type=ref,event=branch,enable=${{ inputs.enable_ref_tags != 'false' && github.event_name != 'release' }},suffix=${{ matrix.variant.name != '' && format('-{0}', matrix.variant.name) || '' }}
type=ref,event=pr,enable=${{ inputs.enable_ref_tags != 'false' && github.event_name != 'release' }},suffix=${{ matrix.variant.name != '' && format('-{0}', matrix.variant.name) || '' }}
type=raw,value=dev,enable=${{ inputs.enable_ref_tags != 'false' && github.event_name == 'push' }},suffix=${{ matrix.variant.name != '' && format('-{0}', matrix.variant.name) || '' }}
type=raw,value=${{ steps.version.outputs.version }},enable=${{ steps.version.outputs.version != '' }},suffix=${{ matrix.variant.name != '' && format('-{0}', matrix.variant.name) || '' }}
type=raw,value=${{ steps.version.outputs.version }}-${{ steps.short-sha.outputs.sha }},enable=${{ steps.version.outputs.version != '' && matrix.variant.name == '' }}
type=raw,value=${{ steps.version.outputs.version }}-${{ matrix.variant.name }}-${{ steps.short-sha.outputs.sha }},enable=${{ steps.version.outputs.version != '' && matrix.variant.name != '' }}
type=semver,pattern={{version}},suffix=${{ matrix.variant.name != '' && format('-{0}', matrix.variant.name) || '' }}
type=semver,pattern={{major}}.{{minor}},suffix=${{ matrix.variant.name != '' && format('-{0}', matrix.variant.name) || '' }}
type=semver,pattern={{major}},suffix=${{ matrix.variant.name != '' && format('-{0}', matrix.variant.name) || '' }}
type=sha,format=short,prefix=${{ matrix.variant.name != '' && format('{0}-', matrix.variant.name) || 'sha-' }}
type=raw,value=${{ matrix.variant.name }},enable=${{ matrix.variant.name != '' }}
- name: Create multi-arch manifest
id: manifest
env:
IMAGE: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}
DIGEST_DIR: ${{ runner.temp }}/digests
run: |
# Reconstruct full image references from the digest marker
# filenames (each file is named after the bare hex digest
# of one per-arch manifest).
if ! ls "${DIGEST_DIR}"/* >/dev/null 2>&1; then
echo "ERROR: no digests downloaded for variant '${{ matrix.variant.name || 'root' }}'" >&2
exit 1
fi
digest_refs=()
for f in "${DIGEST_DIR}"/*; do
digest="$(basename "$f")"
digest_refs+=("${IMAGE}@sha256:${digest}")
done
# Build `--tag` args from the metadata-action JSON output.
# Empty tags array is valid (PR builds without ref-tags
# enabled emit nothing); skip manifest creation in that case.
tag_args=()
while IFS= read -r tag; do
[ -n "$tag" ] && tag_args+=("--tag" "$tag")
done < <(jq -r '.tags[]?' <<< '${{ steps.meta.outputs.json }}')
if [ "${#tag_args[@]}" -eq 0 ]; then
echo "No tags to apply for variant '${{ matrix.variant.name || 'root' }}'; skipping manifest."
exit 0
fi
docker buildx imagetools create \
"${tag_args[@]}" \
"${digest_refs[@]}"
# Resolve the index manifest digest of the freshly pushed
# multi-arch manifest so cosign can sign it directly. We
# ask the registry via `imagetools inspect` and read the
# `.manifest.digest` field — that's the registry's own
# record of the index digest (no client-side hashing).
first_tag="$(jq -r '.tags[0]' <<< '${{ steps.meta.outputs.json }}')"
index_digest="$(docker buildx imagetools inspect "${first_tag}" \
--format '{{ json . }}' | jq -r '.manifest.digest')"
if [ -z "$index_digest" ] || [ "$index_digest" = "null" ]; then
echo "ERROR: could not resolve index digest for ${first_tag}" >&2
exit 1
fi
printf 'index_digest=%s\n' "$index_digest" >> "$GITHUB_OUTPUT"
printf 'first_tag=%s\n' "$first_tag" >> "$GITHUB_OUTPUT"
- name: Install cosign
if: steps.manifest.outputs.index_digest != ''
uses: sigstore/cosign-installer@v3
- name: Sign multi-arch index manifest with cosign
if: steps.manifest.outputs.index_digest != ''
env:
# Same routing as before: keep signature artifacts in a
# sibling GHCR package so the main image's version listing
# stays clean. Verifiers must export the same
# COSIGN_REPOSITORY when running 'cosign verify'. See the
# pre-#377 workflow for the GHCR/OCI-1.1 referrers context.
COSIGN_REPOSITORY: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}-signatures
IMAGE: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}
INDEX_DIGEST: ${{ steps.manifest.outputs.index_digest }}
run: |
target="${IMAGE}@${INDEX_DIGEST}"
echo "Signing ${target} (signatures -> ${COSIGN_REPOSITORY})"
for attempt in 1 2 3; do
if cosign sign --yes "${target}"; then
exit 0
fi
if [ "$attempt" -eq 3 ]; then
echo "ERROR: cosign signing failed after ${attempt} attempts" >&2
exit 1
fi
sleep_for=$((attempt * 10))
echo "cosign signing failed on attempt ${attempt}; retrying in ${sleep_for}s" >&2
sleep "$sleep_for"
done
promote-latest:
# Re-push the :latest tag pointing at the root variant *after* every
# variant manifest job has finished, so GHCR's package version
# listing (sorted by created_at) shows the root image with :latest
# at the top instead of whichever variant happened to finish last.
needs: docker-manifest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Normalize image name
id: image-name
run: |
image_name="$(printf '%s' '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
printf 'image_name=%s\n' "$image_name" >> "$GITHUB_OUTPUT"
- name: Determine image version
id: version
env:
MANUAL_VERSION: ${{ inputs.version || github.event.inputs.version }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
version="${MANUAL_VERSION#v}"
if [ -z "$version" ] && [ -n "$RELEASE_TAG" ]; then
version="${RELEASE_TAG#v}"
fi
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Re-tag root image as :latest
if: steps.version.outputs.version != ''
env:
IMAGE: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}
VERSION: ${{ steps.version.outputs.version }}
run: |
# Add a unique annotation so the resulting image index manifest gets
# a new digest, which makes GHCR record a fresh package version with
# current timestamp (otherwise the existing root manifest is reused
# and stays where it was in the version listing).
promoted_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
docker buildx imagetools create \
--annotation "index:io.headroom.promoted-at=${promoted_at}" \
--tag "${IMAGE}:latest" \
"${IMAGE}:${VERSION}"
+75
View File
@@ -0,0 +1,75 @@
name: Deploy Documentation
on:
pull_request:
branches: [main]
paths:
- 'docs/**'
- 'wiki/**'
- 'mkdocs.yml'
- '.github/workflows/docs.yml'
push:
branches:
- main
paths:
- 'docs/**'
- 'mkdocs.yml'
- '.github/workflows/docs.yml'
workflow_dispatch:
permissions:
contents: read
jobs:
validate:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Cache pip
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-docs-${{ hashFiles('mkdocs.yml') }}
restore-keys: ${{ runner.os }}-pip-docs-
- name: Install dependencies
run: pip install mkdocs-material
- name: Build docs
run: mkdocs build
deploy:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Cache pip
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-docs-${{ hashFiles('mkdocs.yml') }}
restore-keys: ${{ runner.os }}-pip-docs-
- name: Install dependencies
run: pip install mkdocs-material
- name: Build and deploy
run: mkdocs gh-deploy --force
+152
View File
@@ -0,0 +1,152 @@
name: Evaluation Suite
on:
schedule:
- cron: '0 6 * * 1' # Weekly on Monday 6am UTC
workflow_dispatch: # Manual trigger
pull_request:
paths:
- 'headroom/transforms/**'
- 'headroom/evals/**'
- 'headroom/compress.py'
jobs:
# Fast smoke test on PRs touching compression code (~$0.05, ~2 min)
smoke-test:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Cache pip
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-eval-${{ hashFiles('pyproject.toml') }}
restore-keys: ${{ runner.os }}-pip-eval-
# `pip install -e .` invokes maturin (declared in pyproject.toml's
# build-system) which calls cargo to compile the Rust extension.
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.96.0
- name: Cache cargo registry + build
uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target"
- name: Install dependencies (builds Rust extension via maturin)
run: |
pip install -e ".[all]"
python -c "from headroom._core import SmartCrusher; print('headroom._core OK:', SmartCrusher)"
- name: Run CCR round-trip (zero cost)
run: |
python -c "
from headroom.evals.runners.compression_only import CompressionOnlyRunner
runner = CompressionOnlyRunner()
cases = runner.generate_ccr_test_cases(n=50)
result = runner.evaluate_ccr_lossless(cases)
print(f'CCR Round-trip: {result.passed_cases}/{result.total_cases} passed')
assert result.passed, f'CCR failures: {result.errors}'
"
- name: Run tool schema compaction integrity eval (zero cost)
run: |
python -c "
from headroom.evals.runners.compression_only import CompressionOnlyRunner
runner = CompressionOnlyRunner()
result = runner.evaluate_tool_schema_compaction()
print(f'Tool schema compaction: {result.passed_cases}/{result.total_cases} passed, {result.total_tokens_saved} annotation tokens stripped')
assert result.passed, f'Schema compaction failures: {result.errors}'
"
# OPENAI_API_KEY is intentionally not set in the public OSS repo
# (the secret list is empty). The CCR round-trip step above is the
# mandatory gate; this step only runs when an operator has wired
# OPENAI_API_KEY as a repo secret (e.g. on a downstream fork). When
# missing, emit a loud GitHub `::warning::` annotation so the skip
# is visible in the run summary — never a silent pass.
- name: Run built-in tool output eval (skipped when OPENAI_API_KEY unset)
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
if [ -z "${OPENAI_API_KEY}" ]; then
echo "::warning title=Smoke eval skipped::OPENAI_API_KEY is not configured for this repo; only the CCR round-trip gate ran. Wire the secret to enable the live OpenAI eval."
exit 0
fi
python -m headroom.evals quick -n 8 --provider openai --model gpt-4o-mini
# Full Tier 1 suite, weekly or manual (~$3-5, ~30-45 min)
weekly-suite:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Cache pip
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-eval-${{ hashFiles('pyproject.toml') }}
restore-keys: ${{ runner.os }}-pip-eval-
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.96.0
- name: Cache cargo registry + build
uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target"
- name: Install dependencies (builds Rust extension via maturin)
run: |
pip install -e ".[all]"
python -c "from headroom._core import SmartCrusher; print('headroom._core OK')"
- name: Run Tier 1 evaluation suite
run: |
if [ -z "${OPENAI_API_KEY}" ]; then
echo "::warning title=Weekly eval skipped::OPENAI_API_KEY is not configured for this repo; skipping the live Tier 1 suite."
mkdir -p eval_results
printf '%s\n\n%s\n' \
'# Weekly Evaluation Skipped' \
'OPENAI_API_KEY is not configured for this repository, so the live Tier 1 evaluation suite was skipped.' \
> eval_results/skipped.md
exit 0
fi
python -m headroom.evals suite --tier 1 --ci -o eval_results/
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# Recall-based fidelity report on the production routing path. Zero cost
# (synthetic structured cases -> Rust compressors; no model, no API, no
# secrets). Non-blocking: surfaces recall trends weekly without gating.
# The blocking per-PR fidelity gate lives in
# tests/test_compression_fidelity_regression.py (runs in the [dev] shard).
- name: Information-retention recall report (zero cost, non-blocking)
run: |
python -c "
from headroom.evals.runners.compression_only import CompressionOnlyRunner
runner = CompressionOnlyRunner()
cases = runner.generate_info_retention_cases(n=50)
result = runner.evaluate_information_retention(cases)
print(f'Information retention: {result.passed_cases}/{result.total_cases} cases >=0.9 recall, avg compression {result.avg_compression_ratio:.1%}')
if not result.passed:
print(f'::warning title=Fidelity recall::{result.failed_cases} case(s) fell below 0.9 recall: {result.errors[:3]}')
"
- name: Upload results
if: always()
uses: actions/upload-artifact@v7
with:
name: eval-results-${{ github.run_number }}
path: eval_results/
+42
View File
@@ -0,0 +1,42 @@
name: Init E2E
on:
pull_request:
branches: [main]
paths:
- 'headroom/**'
- 'crates/**'
- 'docker/**'
- 'Dockerfile'
- 'e2e/**'
- 'scripts/install*'
- 'pyproject.toml'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- 'uv.lock'
- '.claude-plugin'
- '.github/plugin/**'
- 'plugins/headroom-agent-hooks/**'
- '.github/workflows/init-e2e.yml'
push:
branches: [main]
workflow_dispatch:
concurrency:
group: init-e2e-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
docker-init-e2e:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v7
- name: Build init e2e image
run: docker build -f e2e/init/Dockerfile -t headroom-init-e2e .
- name: Run init e2e container
run: docker run --rm headroom-init-e2e
+140
View File
@@ -0,0 +1,140 @@
name: Init Native E2E
# Cross-platform (linux / macos / windows) smoke tests for the per-subcommand
# ``headroom init -g <target>`` flows. Each matrix cell drops a noop shim for
# the target agent onto PATH and asserts ``headroom init -g <target>``
# succeeds, writes the expected settings file, and (for claude/codex) places
# hooks in the right place.
#
# Deliberately scoped to pull_request + push-to-main + workflow_dispatch to
# avoid bloating CI minutes on every push to every feature branch. The Docker
# init-e2e.yml still runs on every PR and provides the deeper functional
# coverage; this workflow exists to catch platform-specific bugs (Windows
# path separators, macos keychain prompts, PowerShell-vs-bash hook matchers)
# that the single-platform Docker suite can miss.
#
# Extending to other commands (``headroom install``, ``headroom wrap``) is
# expected to be a near-copy of this file. The shared composite action at
# ``.github/actions/headroom-e2e-setup`` absorbs the Python + shim setup so
# each per-command workflow only supplies its matrix and assertion steps.
on:
pull_request:
branches: [main]
paths:
- "headroom/cli/init.py"
- "headroom/install/**"
- "e2e/_lib/**"
- "e2e/init/**"
- ".github/actions/headroom-e2e-setup/**"
- ".github/workflows/init-native-e2e.yml"
push:
branches: [main]
workflow_dispatch:
jobs:
init-native:
runs-on: ${{ matrix.os }}
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
# Windows is excluded today: upstream `esaxx-rs` (transitively from
# `tokenizers`) and `ort-sys` (onnxruntime via `fastembed`) link
# with conflicting MSVC C runtime libraries (/MT vs /MD), so the
# Rust extension cannot build for `win_amd64` until the upstream
# CRT conflict is resolved. Re-add `windows-latest` once the wheel
# builds cleanly there. Tracked in the project plan; not a blocker
# for headroom-ai installs on Linux + macOS.
os: [ubuntu-latest, macos-latest]
target: [claude, codex, copilot, openclaw]
exclude:
# openclaw delegates to ``headroom wrap openclaw`` which needs a
# running OpenClaw CLI; it can't be shimmed cheaply, so it's
# covered by the bundled Docker e2e instead.
- target: openclaw
steps:
- uses: actions/checkout@v7
- name: Setup (shim=${{ matrix.target }})
uses: ./.github/actions/headroom-e2e-setup
with:
install-mode: deps-only-proxy
python-version: "3.11"
shim-target: ${{ matrix.target }}
- name: Verify shim is on PATH (POSIX)
if: runner.os != 'Windows'
shell: bash
run: |
which "${{ matrix.target }}"
- name: Verify shim is on PATH (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
# On Windows the shim is ``<target>.cmd``; Get-Command resolves via
# PATHEXT (same as Python's ``shutil.which`` used by headroom init).
# Git Bash's ``which`` cannot find ``.cmd`` shims, so we use pwsh.
$cmd = Get-Command "${{ matrix.target }}" -ErrorAction Stop
Write-Output $cmd.Source
- name: Run headroom init -g ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
headroom init -g "${{ matrix.target }}"
- name: Assert settings file (POSIX)
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
case "${{ matrix.target }}" in
claude)
test -f "$HOME/.claude/settings.json"
grep -q "ANTHROPIC_BASE_URL" "$HOME/.claude/settings.json"
;;
codex)
test -f "$HOME/.codex/config.toml"
test -f "$HOME/.codex/hooks.json"
grep -q "headroom" "$HOME/.codex/config.toml"
;;
copilot)
test -f "$HOME/.copilot/config.json"
grep -q "SessionStart" "$HOME/.copilot/config.json"
;;
esac
- name: Assert settings file (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$home_ = $env:USERPROFILE
switch ("${{ matrix.target }}") {
"claude" {
$p = Join-Path $home_ ".claude\settings.json"
if (-not (Test-Path $p)) { throw "Missing $p" }
if (-not ((Get-Content $p -Raw) -match "ANTHROPIC_BASE_URL")) {
throw "settings.json missing ANTHROPIC_BASE_URL"
}
}
"codex" {
$c = Join-Path $home_ ".codex\config.toml"
$h = Join-Path $home_ ".codex\hooks.json"
if (-not (Test-Path $c)) { throw "Missing $c" }
if (-not (Test-Path $h)) { throw "Missing $h" }
if (-not ((Get-Content $c -Raw) -match "headroom")) {
throw "config.toml missing headroom provider"
}
}
"copilot" {
$p = Join-Path $home_ ".copilot\config.json"
if (-not (Test-Path $p)) { throw "Missing $p" }
if (-not ((Get-Content $p -Raw) -match "SessionStart")) {
throw "copilot config missing SessionStart hooks"
}
}
}
+68
View File
@@ -0,0 +1,68 @@
name: Install Native E2E
# Cross-platform smoke tests for safe ``headroom install`` paths. The goal here
# is portable CLI coverage that runs on real runners without mutating OS service
# managers or requiring Docker. Deeper lifecycle behavior remains covered by the
# existing native installer wrapper tests and install unit tests.
on:
pull_request:
branches: [main]
paths:
- "headroom/cli/install.py"
- "headroom/install/**"
- "tests/test_cli/test_install_cli.py"
- "tests/test_install/test_paths.py"
- ".github/actions/headroom-e2e-setup/**"
- ".github/workflows/install-native-e2e.yml"
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
install-native:
runs-on: ${{ matrix.os }}
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
# Windows is excluded today: upstream `esaxx-rs` (transitively from
# `tokenizers`) and `ort-sys` (onnxruntime via `fastembed`) link
# with conflicting MSVC C runtime libraries (/MT vs /MD), so the
# Rust extension cannot build for `win_amd64` until the upstream
# CRT conflict is resolved. Re-add `windows-latest` once the wheel
# builds cleanly there. Match init-native-e2e.yml so this workflow
# doesn't fail during setup before the install smoke tests run.
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v7
- name: Setup
uses: ./.github/actions/headroom-e2e-setup
with:
install-mode: deps-only-proxy
python-version: "3.11"
- name: Install pytest
shell: bash
run: |
python -m pip install --upgrade pip
python -m pip install --retries 10 --timeout 60 pytest pytest-cov
- name: Run install native smoke tests
shell: bash
run: |
pytest tests/test_cli/test_install_cli.py tests/test_install/test_paths.py --cov=headroom --cov-report=xml:coverage-install-native.xml --cov-report=term-missing -q
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: ./coverage-install-native.xml
flags: install-native
name: install-native-${{ matrix.os }}
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
+30
View File
@@ -0,0 +1,30 @@
name: Merge Conflicts
# Reject unresolved Git merge-conflict markers committed into tracked files.
#
# This lives in its own workflow ON PURPOSE: ci.yml sets
# `on.pull_request.paths-ignore: ['**/*.md', ...]`, so a Markdown/CHANGELOG-only
# PR skips that workflow entirely. The conflict markers this guard exists to
# catch landed in CHANGELOG.md, so the check must run with no `paths-ignore`.
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
merge-conflicts:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- name: Reject unresolved merge-conflict markers
run: |
if git grep -nI -E '^(<{7}|>{7}|\|{7})( |$)' -- .; then
echo "::error::Unresolved Git merge-conflict markers found in tracked files (see matches above)."
exit 1
fi
echo "No merge-conflict markers found."
+164
View File
@@ -0,0 +1,164 @@
name: Network Diff Capture
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: network-diff-capture-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
PY_VERSION: "3.12"
jobs:
offline:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PY_VERSION }}
- name: Install offline test tools
run: |
python -m pip install --upgrade pip
python -m pip install \
'tiktoken>=0.5.0' \
'pydantic>=2.0.0' \
'litellm==1.82.3' \
'click>=8.1.0' \
'rich>=13.0.0' \
'opentelemetry-api>=1.24.0' \
'ast-grep-cli>=0.30.0' \
'fastapi>=0.100.0' \
'uvicorn>=0.23.0' \
'httpx[http2]>=0.24.0' \
'openai>=2.14.0' \
'mcp>=1.0.0' \
'magika>=0.6.0' \
'zstandard>=0.20.0' \
'websockets>=13.0' \
'onnxruntime>=1.16.0' \
'transformers>=4.30.0' \
'watchdog>=4.0.0' \
'sqlite-vec>=0.1.6' \
pytest ruff mypy
- name: Lint capture code
run: ruff check headroom/capture headroom/cli/capture.py tests/test_network_diff_capture.py
- name: Format check capture code
run: ruff format --check headroom/capture headroom/cli/capture.py tests/test_network_diff_capture.py
- name: Type-check capture code
run: mypy headroom/capture/network_diff.py headroom/cli/capture.py
- name: Run capture tests
run: python -m pytest tests/test_network_diff_capture.py
- name: Validate compose model
env:
ANTHROPIC_API_KEY: dummy
run: docker compose -f docker/differential-network-capture/docker-compose.yml --profile run config
- name: Build Claude Code runner image
env:
ANTHROPIC_API_KEY: dummy
run: docker compose -f docker/differential-network-capture/docker-compose.yml --profile run build claude-direct
- name: Smoke Claude Code runner image
run: docker run --rm -e CLAUDE_COMMAND="claude --version" headroom-network-diff-claude-direct:latest
live-anthropic:
runs-on: ubuntu-latest
timeout-minutes: 90
needs: offline
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CLAUDE_PROMPT: "Summarize this repository in one sentence. Keep the answer under 30 words."
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PY_VERSION }}
- name: Install report dependencies
run: |
python -m pip install --upgrade pip
python -m pip install \
'tiktoken>=0.5.0' \
'pydantic>=2.0.0' \
'litellm==1.82.3' \
'click>=8.1.0' \
'rich>=13.0.0' \
'opentelemetry-api>=1.24.0' \
'ast-grep-cli>=0.30.0' \
'fastapi>=0.100.0' \
'uvicorn>=0.23.0' \
'httpx[http2]>=0.24.0' \
'openai>=2.14.0' \
'mcp>=1.0.0' \
'magika>=0.6.0' \
'zstandard>=0.20.0' \
'websockets>=13.0' \
'onnxruntime>=1.16.0' \
'transformers>=4.30.0' \
'watchdog>=4.0.0' \
'sqlite-vec>=0.1.6'
- name: Run live Claude Code differential capture
if: env.ANTHROPIC_API_KEY != ''
working-directory: docker/differential-network-capture
run: |
set -euo pipefail
mkdir -p captures
docker compose up -d --build mitm-direct mitm-headroom-upstream headroom-proxy mitm-headroom-client
trap 'docker compose --profile run down -v' EXIT
for i in $(seq 1 90); do
if docker compose exec -T headroom-proxy curl --fail --silent http://127.0.0.1:8787/readyz >/dev/null; then
break
fi
if [ "$i" -eq 90 ]; then
docker compose logs headroom-proxy
exit 1
fi
sleep 2
done
docker compose --profile run run --rm claude-direct
docker compose --profile run run --rm claude-headroom
- name: Report skipped live capture
if: env.ANTHROPIC_API_KEY == ''
run: |
echo "::warning title=Live network diff skipped::ANTHROPIC_API_KEY is not configured for this repository; offline harness checks ran, but live Claude Code capture was skipped."
mkdir -p docker/differential-network-capture/captures
cat > docker/differential-network-capture/captures/skipped.md <<'EOF'
# Live Network Diff Capture Skipped
`ANTHROPIC_API_KEY` is not configured for this repository.
EOF
- name: Generate network diff report
if: env.ANTHROPIC_API_KEY != ''
run: |
python -m headroom.cli capture network-diff \
--direct docker/differential-network-capture/captures/direct.jsonl \
--headroom docker/differential-network-capture/captures/headroom-client.jsonl \
--output docker/differential-network-capture/captures/report.md \
--json-output docker/differential-network-capture/captures/report.json
- name: Upload capture artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: network-diff-capture-${{ github.run_number }}
path: docker/differential-network-capture/captures/
if-no-files-found: warn
+41
View File
@@ -0,0 +1,41 @@
name: OpenCode Plugin
# The OpenCode plugin (plugins/opencode) is the routing shim that carries
# `headroom wrap opencode` traffic through the proxy, yet nothing in CI ever
# compiled it — so TypeScript / @types/node major bumps and source changes had
# zero build evidence. This gate runs the plugin's own typecheck + build + test
# whenever it (or this workflow) changes.
on:
pull_request:
branches: [main]
paths:
- "plugins/opencode/**"
- ".github/workflows/opencode-plugin.yml"
push:
branches: [main]
paths:
- "plugins/opencode/**"
- ".github/workflows/opencode-plugin.yml"
permissions:
contents: read
jobs:
build:
name: typecheck + build + test
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: plugins/opencode
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "20"
cache: npm
cache-dependency-path: plugins/opencode/package-lock.json
- run: npm ci
- run: npm run typecheck
- run: npm run build
- run: npm test
+228
View File
@@ -0,0 +1,228 @@
name: PR Governance
on:
pull_request_target:
types: [opened, edited, reopened, synchronize, ready_for_review, converted_to_draft]
schedule:
# Keep labels fresh even when base branches move or checks finish later.
- cron: '23 14 * * 1-5'
workflow_dispatch:
permissions:
contents: read
issues: write
pull-requests: write
checks: read
statuses: read
concurrency:
group: pr-health-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
template:
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Fetch current PR body
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.body // ""' > .pr-body.md
- name: Validate PR template
id: validate
run: python3 scripts/pr-governance.py --event "$GITHUB_EVENT_PATH" --body-file .pr-body.md --report .pr-governance-report.json
- name: Append governance summary
run: |
python3 - <<'PY'
import json
import os
from pathlib import Path
report = json.loads(Path(".pr-governance-report.json").read_text(encoding="utf-8"))
summary = report["summary_markdown"].strip()
with Path(os.environ["GITHUB_STEP_SUMMARY"]).open("a", encoding="utf-8") as handle:
handle.write(f"{summary}\n")
PY
- name: Ensure governance labels exist
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
gh label create "status: needs author action" \
--repo "$REPO" \
--color "d93f0b" \
--description "Pull request body or readiness checklist still needs author updates" \
--force
gh label create "status: ready for review" \
--repo "$REPO" \
--color "0e8a16" \
--description "Pull request body is complete and the author marked it ready for human review" \
--force
- name: Sync governance comment and labels
if: steps.validate.outputs.is_bot_pr != 'true'
uses: actions/github-script@v7
env:
REPORT_PATH: .pr-governance-report.json
with:
script: |
const fs = require('fs');
const report = JSON.parse(fs.readFileSync(process.env.REPORT_PATH, 'utf8'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const issue_number = context.payload.pull_request.number;
const marker = report.comment_marker;
const body = `${marker}\n${report.comment_markdown}`.trim();
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
});
const existing = comments.find(
(comment) =>
comment.user?.type === 'Bot' && typeof comment.body === 'string' && comment.body.includes(marker),
);
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});
}
if (report.labels_to_add.length > 0) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number,
labels: report.labels_to_add,
});
}
for (const label of report.labels_to_remove) {
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number,
name: label,
});
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
}
- name: Report incomplete PR body
if: steps.validate.outputs.valid != 'true'
run: |
echo "PR template validation found missing fields. The governance comment and labels identify the required author updates."
label:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Ensure maintenance labels exist
run: |
set -euo pipefail
gh label create "status: needs rebase" \
--repo "$REPO" \
--color "fbca04" \
--description "Pull request branch is behind the base branch" \
--force
gh label create "status: has conflicts" \
--repo "$REPO" \
--color "d73a4a" \
--description "Pull request has merge conflicts with the base branch" \
--force
gh label create "status: ci failing" \
--repo "$REPO" \
--color "d73a4a" \
--description "Required or reported CI checks are failing" \
--force
gh label create "status: needs author action" \
--repo "$REPO" \
--color "d93f0b" \
--description "Pull request body or readiness checklist still needs author updates" \
--force
gh label create "status: ready for review" \
--repo "$REPO" \
--color "0e8a16" \
--description "Pull request body is complete and the author marked it ready for human review" \
--force
- name: Label open pull requests
run: |
set -euo pipefail
if jq -e '.pull_request.number' "$GITHUB_EVENT_PATH" >/dev/null; then
pr_numbers="$(jq -r '.pull_request.number' "$GITHUB_EVENT_PATH")"
else
pr_numbers="$(gh pr list --repo "$REPO" --state open --limit 100 --json number --jq '.[].number')"
fi
for pr in $pr_numbers; do
data="$(gh pr view "$pr" --repo "$REPO" \
--json isDraft,labels,mergeStateStatus,reviewDecision,statusCheckRollup)"
merge_state="$(jq -r '.mergeStateStatus // "UNKNOWN"' <<<"$data")"
check_state="$(python3 .github/scripts/pr-health-labels.py --state-json "$data")"
is_draft="$(jq -r '.isDraft' <<<"$data")"
review_decision="$(jq -r '.reviewDecision // ""' <<<"$data")"
if [[ "$merge_state" == "BEHIND" ]]; then
gh pr edit "$pr" --repo "$REPO" --add-label "status: needs rebase"
elif [[ "$merge_state" != "UNKNOWN" ]]; then
gh pr edit "$pr" --repo "$REPO" --remove-label "status: needs rebase" || true
fi
if [[ "$merge_state" == "DIRTY" ]]; then
gh pr edit "$pr" --repo "$REPO" --add-label "status: has conflicts"
elif [[ "$merge_state" != "UNKNOWN" ]]; then
gh pr edit "$pr" --repo "$REPO" --remove-label "status: has conflicts" || true
fi
if [[ "$check_state" == "failing" ]]; then
gh pr edit "$pr" --repo "$REPO" --add-label "status: ci failing"
else
gh pr edit "$pr" --repo "$REPO" --remove-label "status: ci failing" || true
fi
if [[ "$merge_state" == "BEHIND" || "$merge_state" == "DIRTY" || "$check_state" == "failing" || "$is_draft" == "true" || "$review_decision" == "CHANGES_REQUESTED" ]]; then
gh pr edit "$pr" --repo "$REPO" --remove-label "status: ready for review" || true
fi
done
+57
View File
@@ -0,0 +1,57 @@
name: Publish to PyPI (manual fallback)
# DEPRECATED: This workflow is superseded by release.yml's matrix-based
# build-and-publish flow. It exists as a manual fallback in case release.yml
# is broken and a hotfix needs to be pushed without going through the
# normal tag-driven pipeline. Single-platform; produces only the linux
# x86_64 wheel + sdist. For full cross-platform release, use release.yml.
on:
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # For trusted publishing
contents: write # For uploading release assets
steps:
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.96.0
- name: Cache cargo registry + build
uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target"
- name: Install build tools
run: |
python -m pip install --upgrade pip 'maturin>=1.5,<2.0' cyclonedx-bom
- name: Build wheel + sdist
run: |
maturin sdist --out dist
maturin build --release --out dist
- name: Generate SBOM (CycloneDX)
run: |
pip install -e ".[proxy]"
cyclonedx-py environment \
--output-format json \
--outfile dist/headroom-sbom.cdx.json
- name: Upload SBOM to release
uses: softprops/action-gh-release@v3
with:
files: dist/headroom-sbom.cdx.json
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.13.0
+57
View File
@@ -0,0 +1,57 @@
name: Release Please
# What this does
# ----------------
# release-please watches `main` for conventional-commit traffic and
# maintains a single "Release vX.Y.Z" PR that aggregates everything
# released since the last tag. Merging that PR is what triggers an
# actual PyPI / npm / GitHub-Release publish (via release.yml, which
# fires on the published-release event the bot emits at merge time).
#
# This replaces the prior "every push to main is a release" pattern
# that burned PyPI's per-project storage quota by uploading a fresh
# wheel matrix (~200 MB) for each merged `fix:` / `feat:` PR.
#
# Day-to-day:
# - Merge a `fix:` PR into main -> bot updates the release PR
# - Merge a `feat:` PR into main -> bot bumps minor in release PR
# - Merge `ci:` / `docs:` / `chore:` -> no PR change (hidden)
# - Ready to ship -> merge the release PR
# (bot tags + emits release event;
# release.yml does the actual builds + publishes)
#
# Config lives in `.release-please-config.json`; current versions
# tracked in `.release-please-manifest.json`.
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
concurrency:
# Serialize bot runs on main so two pushes don't race the
# release-PR update. We never cancel mid-flight — losing a manifest
# write would mean the next push computes the wrong base version.
group: release-please-${{ github.ref }}
cancel-in-progress: false
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v5
with:
# PAT (not GITHUB_TOKEN): a release/tag created by GITHUB_TOKEN does
# NOT emit events that trigger other workflows, so release.yml
# (PyPI/npm) and docker.yml — which fire on `release: published` —
# never ran, and releases had to be cut by hand. A PAT is treated as a
# real user, so the release it creates DOES trigger those publishes; it
# also lets the bot tag past branch/tag protection. Falls back to
# GITHUB_TOKEN when the secret is unset (the release PR still opens; it
# just won't trigger the downstream publishes).
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
config-file: .release-please-config.json
manifest-file: .release-please-manifest.json
File diff suppressed because it is too large Load Diff
+168
View File
@@ -0,0 +1,168 @@
name: rust
on:
push:
branches: [ main, rust-rewrite ]
paths:
- 'crates/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- 'tests/parity/**'
- 'Makefile'
- '.github/workflows/rust.yml'
pull_request:
paths:
- 'crates/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- 'tests/parity/**'
- 'Makefile'
- '.github/workflows/rust.yml'
schedule:
# Nightly parity run at 07:17 UTC (weekdays only). Phase 0 allows failure.
- cron: '17 7 * * 1-5'
concurrency:
group: rust-${{ github.ref }}
cancel-in-progress: true
# Default permissions: read-only. Individual jobs override only what they need.
# Mitigates CodeQL/CWE-275 (missing-workflow-permissions): the GITHUB_TOKEN
# defaults to whatever the repo policy is, which can be read-write. Pinning
# this here means even if the repo default changes, this workflow stays safe.
permissions:
contents: read
jobs:
test:
name: test (ubuntu)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- name: Install stable toolchain
# Pin action code to @stable (latest fixes), toolchain version
# via input. The @1.95.0 ref shipped action code that errors on
# ubuntu-latest with `detected conflict: 'bin/cargo-clippy'`
# because the pre-installed runner Rust collides with the
# clippy-preview component install.
uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.95.0
components: rustfmt, clippy
- name: Cache cargo registry + build
uses: Swatinem/rust-cache@v2
- name: cargo fmt --check
run: cargo fmt --all -- --check
- name: cargo clippy
run: cargo clippy --workspace -- -D warnings
- name: cargo test
run: cargo test --workspace
simulator-e2e:
name: simulator e2e (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v7
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.95.0
- name: Cache cargo registry + build
uses: Swatinem/rust-cache@v2
- name: cargo test simulator-backed proxy e2e
run: cargo test -p headroom-proxy --test e2e_simulators
wheels:
name: wheels (${{ matrix.target }})
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
maturin-target: x86_64
- os: macos-14
target: aarch64-apple-darwin
maturin-target: aarch64-apple-darwin
- os: macos-15-intel
target: x86_64-apple-darwin
maturin-target: x86_64-apple-darwin
# Intel macOS uses `ort-load-dynamic` (no prebuilt ORT from ort-sys);
# Apple Silicon bundles ORT via `ort-download-binaries-rustls-tls`.
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: "Build wheel (single-wheel architecture builds headroom-ai)"
uses: PyO3/maturin-action@v1
# Maturin reads `[tool.maturin]` from the root `pyproject.toml`
# which points at `crates/headroom-py/Cargo.toml` for the cdylib.
# Output is `headroom_ai-<ver>-<py>-<py>-<platform>.whl` containing
# both Python source and the compiled `headroom/_core.so`.
with:
command: build
args: --release --out dist
target: ${{ matrix.maturin-target }}
- name: Upload wheel artifact
uses: actions/upload-artifact@v7
with:
name: wheels-${{ matrix.target }}
path: dist/*.whl
audit:
name: audit
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.95.0
- uses: Swatinem/rust-cache@v2
- name: Install cargo-audit + cargo-deny
uses: taiki-e/install-action@v2
with:
tool: cargo-audit,cargo-deny
- name: cargo audit (soft-fail)
continue-on-error: true
run: cargo audit
- name: cargo deny check licenses
continue-on-error: true
run: cargo deny check licenses
parity-nightly:
name: parity (nightly, allowed to fail during Phase 0)
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.95.0
- uses: actions/setup-python@v6
with:
python-version: '3.11'
- uses: Swatinem/rust-cache@v2
- name: Install deps
run: |
python -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install maturin
pip install -e .
- name: Run parity harness
run: |
source .venv/bin/activate
make test-parity
+120
View File
@@ -0,0 +1,120 @@
name: Security
# Security gate: dependency vulnerability scanning (SCA), static analysis
# (CodeQL/SAST), and secret scanning. Runs on every PR to main, on push to
# main, weekly (to catch newly-disclosed CVEs without a code change), and on
# demand. Each job is an independent required check.
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
# Mondays 06:00 UTC — surface CVEs disclosed since the last commit.
- cron: "0 6 * * 1"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: security-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
# ---- SCA: dependency vulnerability scan -------------------------------
# Audits the PRODUCTION dependency set ([all]) exported from uv.lock. The
# `benchmark` extra is intentionally excluded from [all] (it pulls lm-eval's
# sqlitedict/nltk, which carry unpatchable upstream High CVEs and are never
# installed in production), so this gate fails only on actionable findings.
dependency-audit:
name: Dependency audit (pip-audit)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Export production dependency set from uv.lock
run: |
uv export --frozen --no-dev --no-emit-project --no-hashes \
--extra all --format requirements-txt > requirements-prod.txt
echo "Production dependencies audited:"
wc -l requirements-prod.txt
- name: Audit dependencies (pip-audit)
uses: pypa/gh-action-pip-audit@v1.1.0
with:
inputs: requirements-prod.txt
# ---- SAST: CodeQL static analysis ------------------------------------
codeql:
name: CodeQL (${{ matrix.language }})
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
security-events: write
actions: read
strategy:
fail-fast: false
matrix:
language: [python, javascript-typescript]
steps:
- uses: actions/checkout@v7
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: security-extended
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
# ---- Secret scanning -------------------------------------------------
# Uses the gitleaks BINARY (MIT-licensed, no key) instead of
# gitleaks-action, which requires a paid GITLEAKS_LICENSE for organization
# repos. On PRs we scan only the PR's commits so pre-existing history can't
# block a PR; on push/schedule we scan the working tree. Config + allowlist
# live in .gitleaks.toml at the repo root (auto-loaded).
secret-scan:
name: Secret scan (gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install gitleaks
run: |
version=8.18.4
curl -sSfL \
"https://github.com/gitleaks/gitleaks/releases/download/v${version}/gitleaks_${version}_linux_x64.tar.gz" \
-o /tmp/gitleaks.tar.gz
tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks
sudo install /tmp/gitleaks /usr/local/bin/gitleaks
gitleaks version
- name: Scan for secrets
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if [ -n "$BASE_SHA" ]; then
echo "Scanning PR commits ${BASE_SHA}..HEAD"
gitleaks detect --source . --log-opts="${BASE_SHA}..HEAD" --redact --no-banner
else
echo "Scanning working tree"
gitleaks detect --source . --no-git --redact --no-banner
fi
+60
View File
@@ -0,0 +1,60 @@
name: Stale Triage
on:
schedule:
# Daily weekday pass during US morning hours.
- cron: '17 15 * * 1-5'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: stale-triage
cancel-in-progress: false
jobs:
stale:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Ensure stale label exists
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh label create "status: stale" \
--repo "${{ github.repository }}" \
--color "ededed" \
--description "No recent activity; may be closed if it stays inactive" \
--force
- uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
operations-per-run: 200
remove-stale-when-updated: true
exempt-all-milestones: true
exempt-issue-labels: pinned,security,good first issue,help wanted,needs reproduction
exempt-pr-labels: pinned,security,dependencies,release,do not merge
stale-issue-label: "status: stale"
days-before-issue-stale: 60
days-before-issue-close: 14
stale-issue-message: >
This issue has had no recent activity and is being marked stale.
Please comment with new context if it is still relevant.
close-issue-message: >
Closing this issue due to continued inactivity. It can be reopened
if there is new information or a clear next step.
stale-pr-label: "status: stale"
days-before-pr-stale: 30
days-before-pr-close: 14
stale-pr-message: >
This pull request has had no recent activity and is being marked
stale. Please rebase, resolve conflicts, or comment if it is still
actively being worked.
close-pr-message: >
Closing this pull request due to continued inactivity. It can be
reopened when it is ready for review again.
+41
View File
@@ -0,0 +1,41 @@
name: Wrap E2E
on:
pull_request:
branches: [main]
paths:
- 'headroom/**'
- 'crates/**'
- 'docker/**'
- 'Dockerfile'
- 'e2e/**'
- 'scripts/install*'
- 'pyproject.toml'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- 'uv.lock'
- 'sdk/typescript/**'
- 'plugins/openclaw/**'
- '.github/workflows/wrap-e2e.yml'
push:
branches: [main]
workflow_dispatch:
concurrency:
group: wrap-e2e-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
docker-wrap-e2e:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v7
- name: Build wrap e2e image
run: docker build -f e2e/wrap/Dockerfile -t headroom-wrap-e2e .
- name: Run wrap e2e container
run: docker run --rm headroom-wrap-e2e
+73
View File
@@ -0,0 +1,73 @@
name: Wrap Native E2E
# Cross-platform smoke tests for the hidden ``headroom wrap ... --prepare-only``
# flows. These reuse the existing pytest bridge cases so we exercise the real
# CLI on linux / macos without depending on agent binaries or long-lived proxy
# processes. Windows will be added once the upstream CRT conflict is resolved
# (see matrix comment below).
#
# This complements the Docker-native wrap e2e by catching host-specific issues
# such as home-directory layout and filesystem quirks in prepare-only config
# injection.
on:
pull_request:
branches: [main]
paths:
- "headroom/cli/**"
- "headroom/providers/**"
- "headroom/rtk/**"
- "tests/test_cli/test_wrap_bridge.py"
- ".github/actions/headroom-e2e-setup/**"
- ".github/workflows/wrap-native-e2e.yml"
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
wrap-native:
runs-on: ${{ matrix.os }}
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
# Windows is excluded today: upstream `esaxx-rs` (transitively from
# `tokenizers`) and `ort-sys` (onnxruntime via `fastembed`) link
# with conflicting MSVC C runtime libraries (/MT vs /MD), so the
# Rust extension cannot build for `win_amd64` until the upstream
# CRT conflict is resolved. Re-add `windows-latest` once the wheel
# builds cleanly there. Match init-native-e2e.yml so this workflow
# doesn't fail during setup before the wrap smoke tests run.
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v7
- name: Setup
uses: ./.github/actions/headroom-e2e-setup
with:
install-mode: deps-only-proxy
python-version: "3.11"
- name: Install pytest
shell: bash
run: |
python -m pip install --upgrade pip
python -m pip install --retries 10 --timeout 60 pytest pytest-cov
- name: Run wrap native bridge tests
shell: bash
run: |
pytest tests/test_cli/test_wrap_bridge.py --cov=headroom --cov-report=xml:coverage-wrap-native.xml --cov-report=term-missing -q
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: ./coverage-wrap-native.xml
flags: wrap-native
name: wrap-native-${{ matrix.os }}
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false