name: CI
on:
push:
branches: [main]
pull_request:
jobs:
dco:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Verify DCO sign-off
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
python3 - <<'PY'
from __future__ import annotations
import os
import subprocess
import sys
base_sha = os.environ.get("BASE_SHA")
head_sha = os.environ.get("HEAD_SHA")
if not base_sha or not head_sha:
print("DCO check skipped: missing BASE_SHA/HEAD_SHA")
sys.exit(0)
def ensure_commit(sha: str) -> None:
try:
subprocess.check_call(["git", "cat-file", "-e", f"{sha}^{{commit}}"])
except subprocess.CalledProcessError:
subprocess.check_call(["git", "fetch", "--no-tags", "--depth", "1", "origin", sha])
ensure_commit(base_sha)
ensure_commit(head_sha)
merge_base = (
subprocess.check_output(["git", "merge-base", base_sha, head_sha], text=True)
.strip()
)
commits = (
subprocess.check_output(["git", "rev-list", f"{merge_base}..{head_sha}"], text=True)
.splitlines()
)
missing: list[str] = []
for sha in commits:
message = subprocess.check_output(["git", "show", "-s", "--format=%B", sha], text=True)
if "Signed-off-by:" not in message:
missing.append(sha)
if missing:
print("DCO sign-off missing in one or more commits:")
for sha in missing:
subject = subprocess.check_output(
["git", "show", "-s", "--format=%s", sha],
text=True,
).strip()
print(f" - {sha[:7]} {subject}")
print()
print("Fix:")
print(" - For a new commit: git commit -s")
print(" - For the last commit: git commit --amend -s")
print(" - For all branch commits: git rebase -i --signoff main")
sys.exit(1)
print(f"DCO sign-off OK in {len(commits)} commit(s).")
PY
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.93.0
components: rustfmt
- name: Verify lockfile
run: cargo fetch --locked
- uses: Swatinem/rust-cache@v2
- name: Format
run: cargo fmt --all --check
clippy:
needs: fmt
runs-on: ubuntu-latest
env:
CARGO_INCREMENTAL: "0"
CARGO_PROFILE_DEV_DEBUG: "0"
strategy:
fail-fast: false
matrix:
include:
- name: default
cargo_args: ""
- name: all-features
cargo_args: "--all-features"
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.93.0
components: clippy
- name: Verify lockfile
run: cargo fetch --locked
- uses: Swatinem/rust-cache@v2
- name: Clippy (${{ matrix.name }})
run: cargo clippy --workspace --all-targets --locked ${{ matrix.cargo_args }} -- -D warnings
test:
needs: fmt
runs-on: ubuntu-latest
env:
CARGO_INCREMENTAL: "0"
CARGO_PROFILE_DEV_DEBUG: "0"
CARGO_PROFILE_TEST_DEBUG: "0"
strategy:
fail-fast: false
matrix:
include:
- name: default
cargo_args: ""
cargo_jobs: ""
- name: all-features
cargo_args: "--all-features"
cargo_jobs: "--jobs 1"
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.93.0
- name: Verify lockfile
run: cargo fetch --locked
- uses: Swatinem/rust-cache@v2
- name: Tests (${{ matrix.name }})
run: cargo test --workspace --locked ${{ matrix.cargo_args }} ${{ matrix.cargo_jobs }}
policy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.93.0
- name: Verify lockfile
run: cargo fetch --locked
- uses: Swatinem/rust-cache@v2
- name: Verify SPDX headers
run: |
python3 - <<'PY'
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
expected = [
"// SPDX-License-Identifier: MIT OR Apache-2.0",
"// SPDX-FileCopyrightText: 2025-2026 ObzenFlow Contributors",
"// https://obzenflow.dev",
]
files = subprocess.check_output(["git", "ls-files", "*.rs"], text=True).splitlines()
bad: list[tuple[str, list[str]]] = []
for f in files:
lines = Path(f).read_text(encoding="utf-8").splitlines()
got = lines[:3]
if got != expected:
bad.append((f, got))
if bad:
print("SPDX header mismatch in Rust files:")
for f, got in bad:
print(f" - {f}")
for i, line in enumerate(got or ["<missing>"] * 3, start=1):
print(f" {i}: {line}")
sys.exit(1)
print(f"SPDX headers OK in {len(files)} Rust files")
PY
- name: Verify license files
run: |
python3 - <<'PY'
from __future__ import annotations
import glob
import re
import sys
from pathlib import Path
REQUIRED = ["LICENSE-MIT", "LICENSE-APACHE", "NOTICE"]
root = Path(".")
errors: list[str] = []
# --- 1. Discover crate directories dynamically ---
crate_dirs = sorted(
Path(p).parent for p in glob.glob("crates/*/Cargo.toml")
)
if not crate_dirs:
print("No crates found under crates/*/Cargo.toml")
sys.exit(1)
# --- 2. Presence check ---
for name in REQUIRED:
if not (root / name).is_file():
errors.append(f"Missing root {name}")
for crate_dir in crate_dirs:
for name in REQUIRED:
if not (crate_dir / name).is_file():
errors.append(f"Missing {crate_dir / name}")
# --- 3. Byte-identity check ---
for crate_dir in crate_dirs:
for name in REQUIRED:
root_file = root / name
crate_file = crate_dir / name
if root_file.is_file() and crate_file.is_file():
if root_file.read_bytes() != crate_file.read_bytes():
errors.append(
f"{crate_file} differs from root {name}"
)
# --- 4. Copyright year consistency ---
# Single source of truth: the SPDX header expected value (line 2 in
# the SPDX check above).
spdx_copyright = (
"// SPDX-FileCopyrightText: 2025-2026 ObzenFlow Contributors"
)
m = re.search(r"(\d{4}(?:-\d{4})?)", spdx_copyright)
if not m:
errors.append("Cannot extract year range from SPDX header")
else:
spdx_years = m.group(1)
mit = root / "LICENSE-MIT"
if mit.is_file():
mit_lines = mit.read_text(encoding="utf-8").splitlines()
if len(mit_lines) < 3:
errors.append("LICENSE-MIT has fewer than 3 lines")
else:
cm = re.search(r"(\d{4}(?:-\d{4})?)", mit_lines[2])
if not cm:
errors.append(
"Cannot extract year from LICENSE-MIT line 3"
)
elif cm.group(1) != spdx_years:
errors.append(
f"Year mismatch: SPDX header has {spdx_years}, "
f"LICENSE-MIT line 3 has {cm.group(1)}"
)
if errors:
print("License file check failed:")
for e in errors:
print(f" - {e}")
sys.exit(1)
print(
f"License files OK: {len(REQUIRED)} files verified across "
f"root + {len(crate_dirs)} crate(s)"
)
PY
- name: Verify supervised_base sealing
run: |
! grep -n '^pub mod base;' crates/obzenflow_runtime/src/supervised_base/mod.rs
! grep -n 'pub use base::Supervisor' crates/obzenflow_runtime/src/supervised_base/mod.rs
- name: Install cargo-deny
uses: taiki-e/install-action@v2
with:
tool: cargo-deny
- name: Install cargo-machete
uses: taiki-e/install-action@v2
with:
tool: cargo-machete
- name: cargo-deny
run: cargo deny --all-features check
- name: cargo-machete
run: cargo machete --skip-target-dir