Skip to content

Recover missing asset-only delta identities #997

Recover missing asset-only delta identities

Recover missing asset-only delta identities #997

Workflow file for this run

name: Rust CI
on:
pull_request:
push:
branches: [main]
# Cancel an in-flight CI run when a new push lands on the same PR.
# Only the latest commit matters; older runs would just burn minutes.
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
RUST_STABLE_TOOLCHAIN: "1.94.1"
RUST_NIGHTLY_TOOLCHAIN: "nightly-2026-05-15"
# Treat any compiler warning as an error to keep the codebase warning-free
RUSTFLAGS: "-Dwarnings"
jobs:
# Detects which categories of files changed in the PR. Downstream jobs
# gate on these outputs to skip when irrelevant. Workflow or script
# changes flip every flag on - those touch CI itself, so re-run everything
# to validate the change doesn't break unrelated jobs.
# Pushes to main run all non-PR-only checks.
detect:
name: Detect changes
runs-on: ubuntu-latest
outputs:
code: ${{ steps.filter.outputs.code }}
cargo: ${{ steps.filter.outputs.cargo }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- id: filter
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
EVENT_NAME: ${{ github.event_name }}
run: |
set -euo pipefail
if [[ "$EVENT_NAME" != "pull_request" ]]; then
echo "$EVENT_NAME event -> running all non-PR-only jobs"
{
echo "code=true"
echo "cargo=true"
} >> "$GITHUB_OUTPUT"
exit 0
fi
files=$(git diff --name-only "${BASE_SHA}"...HEAD)
echo "Changed files:"
printf ' %s\n' $files
echo
if printf '%s\n' "$files" | grep -qE '^\.github/(workflows/|scripts/)'; then
echo "Workflow / scripts change detected -> running all jobs"
{
echo "code=true"
echo "cargo=true"
} >> "$GITHUB_OUTPUT"
exit 0
fi
code=false; cargo=false
if printf '%s\n' "$files" | grep -qE '^(src/|tests/|build\.rs$|Cargo\.toml$|Cargo\.lock$)'; then
code=true
fi
if printf '%s\n' "$files" | grep -qE '^(Cargo\.toml$|Cargo\.lock$|\.cargo/audit\.toml$)'; then
cargo=true
fi
{
echo "code=$code"
echo "cargo=$cargo"
} >> "$GITHUB_OUTPUT"
# Enforces consistent code style across the project
# Fails if any code isn't formatted with rustfmt
fmt:
name: Format
needs: detect
if: needs.detect.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: ${{ env.RUST_STABLE_TOOLCHAIN }}
components: rustfmt
- run: cargo fmt --all --check
# Fails when a serializer changes in src/ without a corresponding
# round-trip test. Mirrors `just gate`'s check-roundtrip-gate.sh.
roundtrip:
name: Roundtrip gate
needs: detect
if: needs.detect.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- run: bash scripts/check-roundtrip-gate.sh
# Catches common mistakes, unidiomatic code, and potential bugs
# Acts as an additional layer of static analysis beyond the compiler
clippy:
name: Clippy
needs: detect
if: needs.detect.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: ${{ env.RUST_STABLE_TOOLCHAIN }}
components: clippy
- run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev
# Caches compiled dependencies to speed up subsequent runs
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- run: cargo clippy --all-targets --all-features
# Keeps the `--no-default-features` build honest so FreeBSD and any other
# platform without a working C++ toolchain for xmp_toolkit (see #256) can
# still build kei. Runs clippy + tests on the narrower surface; tests
# make sure we also catch runtime regressions, not just compile breaks.
test_no_default:
name: Test (no-default-features)
needs: detect
if: needs.detect.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: ${{ env.RUST_STABLE_TOOLCHAIN }}
components: clippy
- run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- run: cargo clippy --all-targets --no-default-features -- -D warnings
- run: cargo test --no-default-features
# Runs the test suite on all major platforms to catch platform-specific bugs
# fail-fast: false ensures all platforms run even if one fails
test:
name: Test (${{ matrix.os }})
needs: detect
if: needs.detect.outputs.code == 'true'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: ${{ env.RUST_STABLE_TOOLCHAIN }}
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- run: cargo test --all-features
# Ensures documentation builds without warnings
# Important for maintaining quality docs, especially for published crates
docs:
name: Docs
needs: detect
if: needs.detect.outputs.code == 'true'
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: "-Dwarnings"
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: ${{ env.RUST_STABLE_TOOLCHAIN }}
- run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Check live wiki command documentation
run: |
git clone --depth=1 https://github.com/rhoopr/kei.wiki.git /tmp/kei-wiki
cargo build --bin kei
scripts/check-wiki-commands.sh target/debug/kei /tmp/kei-wiki
- run: cargo doc --no-deps --all-features
# Detects dependencies declared in Cargo.toml but not actually used
# Keeps the dependency tree lean and reduces compile times
# Requires nightly because cargo-udeps uses unstable features
unused-deps:
name: Unused Dependencies
needs: detect
if: needs.detect.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly
with:
toolchain: ${{ env.RUST_NIGHTLY_TOOLCHAIN }}
- run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev
- uses: taiki-e/install-action@7170fe35ac006b4d8f0bcdfba8168cf5c431a122 # cargo-udeps
- run: cargo +nightly udeps --all-targets
# Verifies Cargo.lock is committed and in sync with Cargo.toml
# Ensures reproducible builds and prevents "works on my machine" issues
lockfile:
name: Lockfile
needs: detect
if: needs.detect.outputs.cargo == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: ${{ env.RUST_STABLE_TOOLCHAIN }}
- run: cargo fetch --locked
# PR head coverage. Runs in parallel with coverage-base.
# Output artifact feeds into the Coverage aggregator job.
coverage-head:
name: Coverage (head)
needs: detect
if: github.event_name == 'pull_request' && needs.detect.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: ${{ env.RUST_STABLE_TOOLCHAIN }}
components: llvm-tools-preview
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: coverage-head
- run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev
- uses: taiki-e/install-action@9180ba819ebd9a5f78ccc0e64cb6b06f5917fdff # cargo-llvm-cov
- name: Run coverage
run: |
cargo llvm-cov --all-features --no-report
# Human-readable report for the Actions log
cargo llvm-cov report
# Structured data for parsing - cargo-llvm-cov's text format is
# column-position-fragile and the test summary goes to stderr.
cargo llvm-cov report --json --output-path coverage.json
# Per-line hit counts for patch coverage computation.
cargo llvm-cov report --lcov --output-path coverage.lcov
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: coverage-head
path: |
coverage.json
coverage.lcov
retention-days: 1
# Base-branch coverage for the delta. Runs in parallel with coverage-head
# on a separate runner. continue-on-error so a broken main only suppresses
# the delta - it doesn't block the PR.
coverage-base:
name: Coverage (base)
needs: detect
if: github.event_name == 'pull_request' && needs.detect.outputs.code == 'true'
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.event.pull_request.base.sha }}
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: ${{ env.RUST_STABLE_TOOLCHAIN }}
components: llvm-tools-preview
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: coverage-base
- run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev
- uses: taiki-e/install-action@9180ba819ebd9a5f78ccc0e64cb6b06f5917fdff # cargo-llvm-cov
- name: Run coverage
run: |
cargo llvm-cov --all-features --no-report
cargo llvm-cov report --json --output-path base_coverage.json
cargo llvm-cov report --lcov --output-path base_coverage.lcov
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: coverage-base
path: |
base_coverage.json
base_coverage.lcov
retention-days: 1
# Aggregates head + base, computes patch coverage, and builds the sticky
# comment body. The comment is posted by a separate workflow triggered via
# workflow_run so cross-fork PRs (which run with a read-only GITHUB_TOKEN)
# can still get comments - this job only builds the artifact.
coverage:
name: Coverage
runs-on: ubuntu-latest
needs: [coverage-head, coverage-base]
if: always() && needs.coverage-head.result == 'success'
permissions:
contents: read
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
# Need full history for git diff against base in patch_coverage.py.
fetch-depth: 0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: coverage-head
- name: Download base coverage
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: coverage-base
continue-on-error: true
- name: Compute patch coverage
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
args=(--base "$BASE_SHA" --lcov coverage.lcov --workspace .)
if [ -f base_coverage.lcov ]; then
args+=(--base-lcov base_coverage.lcov)
fi
python3 .github/scripts/patch_coverage.py "${args[@]}" > patch_coverage.json
cat patch_coverage.json
- name: Build coverage comment
env:
MARKER: "<!-- coverage-comment -->"
run: |
set -euo pipefail
# Strip absolute workspace prefix from filenames in JSON.
WORKSPACE="$(pwd)/"
# Reusable jq function defining 2-decimal percent formatting.
PCT_DEF='def pct: (. * 100 | round) as $x | "\($x / 100 | floor).\($x % 100 | tostring | if length == 1 then "0\(.)" else . end)";'
# --- Build headline (line coverage % + delta + patch coverage) ---
HEAD_PCT=$(jq -r '.data[0].totals.lines.percent' coverage.json)
HEADLINE="**Line coverage:** $(printf '%.2f' "$HEAD_PCT")%"
if [ -f base_coverage.json ]; then
BASE_PCT=$(jq -r '.data[0].totals.lines.percent' base_coverage.json)
DELTA=$(awk "BEGIN { printf \"%+.2f\", ${HEAD_PCT} - ${BASE_PCT} }")
HEADLINE="${HEADLINE} (${DELTA}% from main)"
fi
PATCH_TOTAL=$(jq -r '.total_eligible' patch_coverage.json)
if [ "$PATCH_TOTAL" -gt 0 ]; then
PATCH_PCT=$(jq -r '.percent' patch_coverage.json)
PATCH_COVERED=$(jq -r '.total_covered' patch_coverage.json)
HEADLINE="${HEADLINE} · **Patch coverage:** $(printf '%.2f' "$PATCH_PCT")% (${PATCH_COVERED} / ${PATCH_TOTAL} changed lines)"
fi
# --- Build the comment ---
{
echo "$MARKER"
echo "## Coverage Report"
echo ""
echo "$HEADLINE"
echo ""
# Per-file patch coverage, if the PR touched any covered .rs lines.
# Patch column = % of changed lines covered.
# File column = whole-file coverage on PR head.
# Δ column = whole-file delta vs base (blank for new files).
if [ "$PATCH_TOTAL" -gt 0 ]; then
echo "| File | Patch | File Coverage | Δ from main |"
echo "|------|-------|---------------|-------------|"
jq -r "${PCT_DEF}"'
# Format a signed delta as +X.YY / -X.YY.
def signed: if . >= 0 then "+\(. | pct)" else "-\(-1 * . | pct)" end;
.files | sort_by(.percent) | .[] |
"| \(.file) | \(.percent | pct)% (\(.covered) / \(.total)) | " +
(if .file_percent_head == null then "—" else "\(.file_percent_head | pct)%" end) +
" | " +
(if .file_delta == null then (if .file_percent_head != null then "new" else "—" end) else "\(.file_delta | signed)%" end) +
" |"
' patch_coverage.json
echo ""
fi
echo "### Project totals"
echo ""
echo "| Metric | Coverage | Covered / Total |"
echo "|--------|----------|-----------------|"
jq -r "${PCT_DEF}"'
.data[0].totals as $t |
[
["Lines", $t.lines],
["Functions", $t.functions],
["Regions", $t.regions],
["Branches", $t.branches]
]
| map(select(.[1].count > 0))
| .[]
| "| \(.[0]) | \(.[1].percent | pct)% | \(.[1].covered) / \(.[1].count) |"
' coverage.json
echo ""
# Lowest-covered files (line coverage ascending), excluding 100%.
LOWEST=$(jq -r --arg ws "$WORKSPACE" "${PCT_DEF}"'
.data[0].files
| map(select(.summary.lines.percent < 100))
| sort_by(.summary.lines.percent)
| .[:10]
| .[]
| "| \(.filename | sub($ws; "")) | \(.summary.lines.percent | pct)% | \(.summary.lines.covered) / \(.summary.lines.count) |"
' coverage.json)
if [ -n "$LOWEST" ]; then
N=$(printf '%s\n' "$LOWEST" | wc -l)
echo "<details><summary>${N} lowest-covered files (line coverage, project-wide)</summary>"
echo ""
echo "| File | Lines | Covered / Total |"
echo "|------|-------|-----------------|"
printf '%s\n' "$LOWEST"
echo ""
echo "</details>"
echo ""
fi
# Files at 100% line coverage.
FULL=$(jq -r --arg ws "$WORKSPACE" '
.data[0].files
| map(select(.summary.lines.percent == 100))
| sort_by(.filename)
| .[]
| "- \(.filename | sub($ws; ""))"
' coverage.json)
if [ -n "$FULL" ]; then
N=$(printf '%s\n' "$FULL" | wc -l)
echo "<details><summary>${N} files at 100% line coverage</summary>"
echo ""
printf '%s\n' "$FULL"
echo ""
echo "</details>"
fi
} > comment.md
# Mirror to the Actions step summary so the report is visible in the run UI.
cat comment.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload coverage comment
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: coverage-comment
path: comment.md
retention-days: 1
# PR metadata for the coverage-comment workflow. workflow_run events
# don't carry PR context, and looking it up from the head SHA is
# unreliable for fork PRs, so we pass it via artifact.
- name: Record PR metadata
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
printf '%s\n' "$PR_NUMBER" > pr_number.txt
printf '%s\n' "$PR_HEAD_SHA" > pr_head_sha.txt
- name: Upload PR metadata
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: coverage-comment-meta
path: |
pr_number.txt
pr_head_sha.txt
retention-days: 1
# Checks dependencies against the RustSec Advisory Database
# Catches known security vulnerabilities in your dependency tree
security:
name: Security Audit
needs: detect
if: needs.detect.outputs.cargo == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: taiki-e/install-action@b7d831ec1a6ef0d81d27cf61932d92140b2271d4 # cargo-audit
# Ignore list + rationale: .cargo/audit.toml.
- run: cargo audit --deny warnings
# Guards the Actions supply-chain hardening rules that keep PRs
# from reintroducing mutable action refs or privileged manual publishes.
workflow-hardening:
name: Workflow hardening
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- run: python3 .github/scripts/check_workflow_hardening.py
- run: |
mkdir -p /tmp/codex/kei/pycache
PYTHONPYCACHEPREFIX=/tmp/codex/kei/pycache python3 -m py_compile .github/scripts/*.py
# Cheap deterministic syntax check for repository helper scripts. Heavier
# shellcheck/shfmt/ruff/actionlint checks are exposed through local
# `just lint-scripts` / `just lint-workflows` recipes and full-test prereq
# reporting, but this job keeps obvious syntax breaks out of protected CI.
script-lint:
name: Script lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Check shell and Python helper syntax
run: |
set -euo pipefail
mapfile -t shell_files < <(find scripts tests/shell docker -maxdepth 3 -type f \( -name '*.sh' -o -name 'entrypoint.sh' -o -name 'check-contracts' \) -print | sort)
mapfile -t python_files < <(find scripts .github/scripts -maxdepth 3 -type f -name '*.py' -print | sort)
bash -n "${shell_files[@]}"
mkdir -p /tmp/codex/kei/pycache
PYTHONPYCACHEPREFIX=/tmp/codex/kei/pycache python3 -m py_compile "${python_files[@]}"
# Catches spelling mistakes in code, comments, and documentation
# Small thing but helps maintain a professional codebase. Always runs
# since typos can land in any kind of file.
typos:
name: Typos
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: crate-ci/typos@5374cbf686e897b15713110e233094e2874de7ef # master
# Single required check that gates merging. Skipped jobs (path-filtered
# out) count as success; only real failures or cancellations block.
# The branch ruleset should require this name and nothing else.
ci:
name: CI
if: always()
needs:
- detect
- fmt
- roundtrip
- clippy
- test_no_default
- test
- docs
- unused-deps
- lockfile
- coverage
- security
- workflow-hardening
- script-lint
- typos
runs-on: ubuntu-latest
steps:
- name: Verify all required jobs passed or were skipped
env:
NEEDS: ${{ toJSON(needs) }}
run: |
set -euo pipefail
echo "$NEEDS" | jq .
if echo "$NEEDS" | jq -e 'all(.[]; .result == "success" or .result == "skipped")' > /dev/null; then
echo "All required jobs passed or were skipped."
else
echo "One or more required jobs failed or were cancelled." >&2
exit 1
fi