Skip to content

perf(branches): preserve snapshots and reuse verified views #1992

perf(branches): preserve snapshots and reuse verified views

perf(branches): preserve snapshots and reuse verified views #1992

Workflow file for this run

name: CI
on:
pull_request:
push:
branches:
- main
tags:
- "v*"
workflow_dispatch:
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
# Keep main runs alive: their suite result is the post-merge detection
# channel, and a cancelled job never reaches rust-cache's post-step save.
# Superseded PR runs are safe to cancel.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
# The one failpoint feature list `lint` and `test` both compile.
FAILPOINT_FEATURES: omnigraph-engine/failpoints,omnigraph-cluster/failpoints
jobs:
classify_changes:
name: Classify Changes
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
run_full_ci: ${{ steps.filter.outputs.run_full_ci }}
steps:
- name: Checkout source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
fetch-depth: 0
- name: Detect documentation-only changes
id: filter
env:
BEFORE_SHA: ${{ github.event.before }}
EVENT_NAME: ${{ github.event_name }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REF_TYPE: ${{ github.ref_type }}
run: |
set -euo pipefail
if [[ "$EVENT_NAME" == "workflow_dispatch" || "$REF_TYPE" == "tag" ]]; then
echo "run_full_ci=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "$EVENT_NAME" == "pull_request" ]]; then
base="$PR_BASE_SHA"
head="$PR_HEAD_SHA"
else
base="$BEFORE_SHA"
head="$GITHUB_SHA"
if [[ "$base" == "0000000000000000000000000000000000000000" ]]; then
base="$(git rev-parse "${head}^" 2>/dev/null || true)"
fi
fi
if [[ -z "${base:-}" ]]; then
echo "run_full_ci=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Disable rename collapsing so moving source into docs still reports
# the source-side deletion and cannot become a docs-only false skip.
mapfile -t changed < <(git diff --name-only --no-renames "$base" "$head")
if [[ "${#changed[@]}" -eq 0 ]]; then
echo "run_full_ci=true" >> "$GITHUB_OUTPUT"
exit 0
fi
run_full_ci=false
for path in "${changed[@]}"; do
case "$path" in
docs/*.md|docs/*.mdx|docs/*.rst|docs/*.adoc) ;;
README.md|AGENTS.md|CLAUDE.md|CHANGELOG.md|CONTRIBUTING.md|CODE_OF_CONDUCT.md) ;;
LICENSE|LICENSE.md) ;;
*)
run_full_ci=true
;;
esac
done
printf 'Changed files:\n'
printf ' %s\n' "${changed[@]}"
echo "run_full_ci=$run_full_ci" >> "$GITHUB_OUTPUT"
check_agents_md:
name: Check AGENTS.md Links
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
# The marker check diffs the PR merge commit against its first
# parent (the current base tip), so both parents must be present.
fetch-depth: 2
# Diff-scoped belt over check-docs.py's Markdown scan: flags only
# conflict-marker lines this pull request ADDS, in any file type, via
# git's own --check detector (which also respects conflict-marker-size
# and catches a lone `=======` on added lines). Markers already on the
# base branch never fail someone else's PR. Runs before the
# documentation checks so hits are annotated inline even when the
# Markdown scan would fail the job first.
- name: Verify the PR adds no merge-conflict markers
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
# `git diff --check` exits non-zero on findings, so the pipeline
# needs the `|| true`; grep narrows to markers (drops whitespace
# complaints). Output shape: path:line: leftover conflict marker
hits=$(git diff --check HEAD^1 HEAD | grep 'leftover conflict marker' || true)
if [ -n "$hits" ]; then
printf '%s\n' "$hits" | while IFS=: read -r file line rest; do
printf '::error file=%s,line=%s::Committed merge-conflict marker\n' "$file" "$line"
done
count=$(printf '%s\n' "$hits" | wc -l | tr -d ' ')
echo "::error::this pull request adds $count merge-conflict marker line(s)"
exit 1
fi
- name: Verify AGENTS.md ↔ docs/ cross-links
run: bash scripts/check-agents-md.sh
- name: Verify documentation structure and RFC registry
run: python3 scripts/check-docs.py
workflow_action_pins:
name: Check Workflow Action Pins
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Reject mutable external action refs
run: python3 scripts/check-workflow-action-pins.py
- name: Enforce release vocabulary gates
run: python3 scripts/check-release-vocabulary-gates.py
azure_contract_guards:
name: Azure Contract Guards
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Verify container and package binary sets match
run: python3 scripts/check-container-binary-contract.py
- name: Enforce Azure admission dependency direction
run: python3 scripts/check-azure-admission-boundary.py
graph_vocabulary_guard:
name: Graph Vocabulary Guard
# Keep this exact reporting context while it remains required by branch
# protection, but do not run the substrate-sized audit on pull requests.
# The full audit still runs after merge, on tags, and by manual dispatch.
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
env:
# Single switch for the vocabulary audit, disabled by decision on
# 2026-08-28. Set to "true" to re-enable every audit step at once (the
# public-Rust tooling and all three checks are gated on it). The job
# still runs its unit tests and reports success either way, so branch
# protection and the exact-SHA release gates stay wired.
VOCABULARY_AUDIT_ENABLED: "false"
# A cold default + relevant all-features rustdoc pass over all seven public
# library crates is substrate-sized. The first hosted run completed the
# public-Rust step in 39 minutes, while the larger breaking-contract tree
# was still compiling when a 45-minute whole-job bound cancelled it. Keep
# current/base targets isolated for correctness and retain a finite cold-run
# ceiling; the pinned-tool and build-target caches make warm runs cheaper.
timeout-minutes: 75
permissions:
contents: read
steps:
- name: Checkout source and comparison history
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
# `check` reads both the inventory and openapi.json from the exact
# comparison commit. A shallow checkout could turn that proof into a
# false missing-base failure.
fetch-depth: 0
- name: Install pinned toolchain
run: rustup toolchain install
- name: Resolve vocabulary comparison base
id: comparison_base
env:
EVENT_NAME: ${{ github.event_name }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
REF_TYPE: ${{ github.ref_type }}
run: |
set -euo pipefail
zero_sha=0000000000000000000000000000000000000000
if [[ "$EVENT_NAME" == "pull_request" ]]; then
base="$PR_BASE_SHA"
elif [[ "$EVENT_NAME" == "push" && "$REF_TYPE" != "tag" && "$PUSH_BEFORE_SHA" != "$zero_sha" ]]; then
base="$PUSH_BEFORE_SHA"
else
# Tag creation commonly reports an all-zero `before`, while a
# manual run has no event base. Compare both with the checked-out
# commit's first parent rather than inventing a mutable branch tip.
base="$(git rev-parse "${GITHUB_SHA}^")"
fi
if [[ -z "${base:-}" || "$base" == "$zero_sha" ]]; then
echo "::error::unable to resolve a vocabulary comparison base"
exit 1
fi
git cat-file -e "${base}^{commit}"
echo "sha=$base" >> "$GITHUB_OUTPUT"
- name: Install public-Rust system dependencies
if: env.VOCABULARY_AUDIT_ENABLED == 'true'
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler libprotobuf-dev
- name: Restore pinned public-Rust tooling
if: env.VOCABULARY_AUDIT_ENABLED == 'true'
id: public_rust_tool_cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.rustup/toolchains/nightly-2026-08-01-x86_64-unknown-linux-gnu
~/.rustup/update-hashes/nightly-2026-08-01-x86_64-unknown-linux-gnu
${{ runner.temp }}/cargo-public-api
key: vocabulary-public-api-${{ runner.os }}-${{ runner.arch }}-nightly-2026-08-01-cargo-public-api-0.52.0-v1
- name: Install exact public-Rust tooling
if: env.VOCABULARY_AUDIT_ENABLED == 'true'
id: public_rust_tool
run: |
set -euo pipefail
rustup toolchain install nightly-2026-08-01 --profile minimal
tool_root="$RUNNER_TEMP/cargo-public-api"
tool="$tool_root/bin/cargo-public-api"
if [[ ! -x "$tool" ]] || [[ "$($tool --version)" != "cargo-public-api 0.52.0" ]]; then
CARGO_INSTALL_ROOT="$tool_root" \
cargo install cargo-public-api --version 0.52.0 --locked
fi
[[ "$($tool --version)" == "cargo-public-api 0.52.0" ]]
echo "binary=$tool" >> "$GITHUB_OUTPUT"
- name: Cache public-Rust build data
if: env.VOCABULARY_AUDIT_ENABLED == 'true'
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: |
. -> target/vocabulary-public-api
key: vocabulary-public-api-nightly-2026-08-01-v1
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Test vocabulary guard
run: cargo test -p omnigraph-vocabulary-guard --locked
- name: Check OpenAPI and Rust-string vocabulary
if: env.VOCABULARY_AUDIT_ENABLED == 'true'
env:
BASE_SHA: ${{ steps.comparison_base.outputs.sha }}
run: |
set -euo pipefail
for surface in openapi rust-string; do
cargo run -p omnigraph-vocabulary-guard --locked -- \
check --surface "$surface" --base "$BASE_SHA" \
--inventory tools/omnigraph-vocabulary-guard/graph-vocabulary-inventory.tsv \
--openapi openapi.json
done
- name: Check public-Rust vocabulary
if: env.VOCABULARY_AUDIT_ENABLED == 'true'
env:
BASE_SHA: ${{ steps.comparison_base.outputs.sha }}
CARGO_PUBLIC_API: ${{ steps.public_rust_tool.outputs.binary }}
run: |
tool="${CARGO_PUBLIC_API:-cargo-public-api}"
cargo run -p omnigraph-vocabulary-guard --locked -- \
check --surface public-rust --base "$BASE_SHA" \
--inventory tools/omnigraph-vocabulary-guard/graph-vocabulary-inventory.tsv \
--cargo-public-api "$tool" \
--public-api-target-dir target/vocabulary-public-api
release_edge_after_vocabulary:
name: Release edge after vocabulary audit
needs: [classify_changes, graph_vocabulary_guard]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.classify_changes.outputs.run_full_ci == 'true'
permissions:
actions: read
contents: write
uses: ./.github/workflows/release-edge.yml
with:
source_ref: ${{ github.sha }}
ci_run_id: ${{ format('{0}', github.run_id) }}
release_tag_after_vocabulary:
name: Release tag after vocabulary audit
needs: graph_vocabulary_guard
if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
permissions:
actions: read
contents: write
uses: ./.github/workflows/release.yml
with:
tag: ${{ github.ref_name }}
source_ref: ${{ github.ref }}
ci_run_id: ${{ format('{0}', github.run_id) }}
secrets: inherit
publish_tag_image_after_vocabulary:
name: Publish tag image after vocabulary audit
needs: graph_vocabulary_guard
if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
permissions:
actions: read
contents: read
packages: write
uses: ./.github/workflows/publish-image.yml
with:
tag: ${{ github.ref_name }}
source_ref: ${{ github.ref }}
ci_run_id: ${{ format('{0}', github.run_id) }}
secrets: inherit
publish_tag_crates_after_vocabulary:
name: Publish tag crates after vocabulary audit
needs: graph_vocabulary_guard
if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
permissions:
actions: read
contents: read
uses: ./.github/workflows/publish-crates.yml
with:
tag: ${{ github.ref_name }}
source_ref: ${{ github.ref }}
ci_run_id: ${{ format('{0}', github.run_id) }}
secrets: inherit
entrypoint_test:
name: Container Entrypoint
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Verify omnigraph-server entrypoint arg composition
run: sh docker/entrypoint_test.sh
- name: Test non-destructive Azure validation mode
run: sh deploy/azure/validate_test.sh
- name: Test Azure bootstrap readiness and admission modes
run: sh deploy/azure/bootstrap-entrypoint_test.sh
- name: Build Azure bootstrap image from inherited non-root user
run: sh deploy/azure/bootstrap_image_test.sh
azure_deployment_validation:
name: Azure Deployment Validation
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Compile Azure Bicep templates
# Azure CLI 2.77.0 plus an explicit Bicep version. Both values are
# immutable inputs; the command performs no Azure login or mutation.
run: |
docker run --rm \
-v "$PWD:/work" \
-w /work \
mcr.microsoft.com/azure-cli:2.77.0@sha256:d41153958a61392a4c9db1591e33b533ca8ab8b59529d212d06bb6ec7e9c1f25 \
sh -eu -c '
az bicep install --version v0.46.1 >/dev/null
az bicep build --file deploy/azure/foundation.bicep --stdout >/dev/null
az bicep build --file deploy/azure/runtime.bicep --stdout >/dev/null
'
fmt:
name: Format (rustfmt)
needs: classify_changes
# Own job so a fmt failure cannot mask the clippy result; fmt compiles
# nothing, so no dep or cache steps.
if: github.event_name == 'pull_request' && needs.classify_changes.outputs.run_full_ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
# No dtolnay/rust-toolchain action (here or in `lint`): it exports
# RUSTUP_TOOLCHAIN, overriding the toml pin. Bare install reads the
# toml; explicit because rustup auto-install has changed across releases.
- name: Install pinned toolchain
run: rustup toolchain install
- name: Check formatting
run: cargo fmt --all --check
lint:
name: Lint (clippy)
needs: classify_changes
# PR gate. Lints stay `warn` in-tree; only this job denies, so a new
# toolchain's lints cannot break local builds. Main-push runs exist only
# to seed the cache (see save-if below).
if: (github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main')) && needs.classify_changes.outputs.run_full_ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
steps:
- name: Checkout source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler libprotobuf-dev
# See the toolchain note in `fmt`. Before the cache step so the cache
# key sees the pinned rustc.
- name: Install pinned toolchain
run: rustup toolchain install
- name: Cache Rust build data
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: |
. -> target
key: lint
# Save only from main: a save from any other ref (a PR branch, a
# tag) is restorable by no PR and only evicts shared entries. Save
# even when the job is red: dependency artifacts are valid whatever
# the verdict, and a red seed run would otherwise leave every PR
# cold until main is green again.
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-on-failure: true
- name: Clippy (default features)
run: cargo clippy --workspace --all-targets --locked -- -D warnings -W clippy::dbg_macro
# Feature-gated code is invisible to the default build.
- name: Clippy (failpoints superset)
run: |
cargo clippy --workspace --all-targets --locked \
--features "$FAILPOINT_FEATURES" \
-- -D warnings -W clippy::dbg_macro
test:
name: Test Workspace
needs: classify_changes
# Runs on every non-documentation pull request (fail-fast; a reporting
# context, not in `.github/branch-protection.json`: under strict checks a
# required 60-minute context would space merges an hour apart; wait for
# it before merging) and on main pushes, tags, and workflow_dispatch
# (`--no-fail-fast`: the post-merge detection channel; the main run also
# seeds the dependency cache PRs restore). openapi.json is not
# auto-regenerated: after server/API changes run
# `OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi`
# or the drift check fails this job.
if: needs.classify_changes.outputs.run_full_ci == 'true'
runs-on: ubuntu-latest
# The 2026-07-28 cold diagnostic spent 25m29s on the featureless workspace
# graph, then another 23m21s compiling the failpoints graph. Build the
# current tree once with the feature superset instead. The predecessor
# binary/fence and RustFS shards are independent parallel jobs below, so
# this job has one current-tree compile and a bounded 60-minute ceiling.
timeout-minutes: 60
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
# Floor for the whole class of debug-build async stack depth, NOT the fix
# for any one test. Unoptimized builds keep every nested async frame, so
# a deep engine path (init -> write -> publish -> recovery -> Lance) can
# approach a default 2 MiB spawned-thread stack. The depth is genuinely
# environment-sensitive: the same tree overflowed `write_cost` on this
# Linux runner while macOS stayed under. Individual offenders are still
# fixed at
# the source with `Box::pin` (see `helpers::cost::cost_harness`); this
# keeps CI deterministic across runners while that work continues.
RUST_MIN_STACK: 16777216
steps:
- name: Checkout source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler libprotobuf-dev
- name: Install pinned toolchain
run: rustup toolchain install
- name: Cache Rust build data
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: |
. -> target
# Save only from main, red or green (same rule as `lint`).
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-on-failure: true
# Own step so compile and run wall clock read apart in the log; the
# run step reuses these binaries (same flags, features, and env).
- name: Compile the workspace and failpoint test graph
run: |
cargo test --workspace --locked --no-run \
--features "$FAILPOINT_FEATURES"
- name: Install genuine v0.9 release for upgrade regression
run: |
set -euo pipefail
v09_dir="$RUNNER_TEMP/omnigraph-v09"
# The installer downloads the official archive and verifies its
# SHA256 before extraction. An exact VERSION never falls back to edge.
REPO_SLUG=ModernRelay/omnigraph VERSION=v0.9.0 INSTALL_DIR="$v09_dir" \
bash scripts/install.sh
test -x "$v09_dir/omnigraph"
[[ "$("$v09_dir/omnigraph" --version)" == "omnigraph 0.9.0" ]] \
|| { echo "::error::upgrade regression requires the genuine v0.9.0 CLI"; exit 1; }
echo "OMNIGRAPH_V09_BIN=$v09_dir/omnigraph" >> "$GITHUB_ENV"
- name: Run workspace and failpoint tests once
run: |
set -euo pipefail
test_log="$RUNNER_TEMP/workspace-tests.log"
# A pull request is a gate: stop at the first red suite. Main,
# tag, and dispatch runs are detection channels: --no-fail-fast,
# so every independent failure is reported.
no_fail_fast=()
if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then
no_fail_fast+=(--no-fail-fast)
fi
cargo test --workspace --locked ${no_fail_fast[@]+"${no_fail_fast[@]}"} \
--features "$FAILPOINT_FEATURES" \
-- --nocapture 2>&1 | tee "$test_log"
if grep -Fq "skipping v0.9 upgrade e2e:" "$test_log"; then
echo "::error::v0.9 upgrade regression skipped despite a configured release binary"
exit 1
fi
grep -Fq "v0.9 -> v0.10 upgrade e2e completed" "$test_log" \
|| { echo "::error::v0.9 upgrade regression did not complete"; exit 1; }
grep -Eq "test current_v010_upgrades_genuine_v09_graph_end_to_end \\.\\.\\. ok" "$test_log" \
|| { echo "::error::exact v0.9 upgrade regression did not pass"; exit 1; }
v5_v6_format_fence:
name: V5 ↔ V6 Format Fence
needs: classify_changes
if: github.event_name != 'pull_request' && needs.classify_changes.outputs.run_full_ci == 'true'
runs-on: ubuntu-latest
# This job owns the immutable predecessor build in parallel with the
# current-tree workspace job. The 2026-07-28 cold measurement was 25m40s
# for the predecessor CLI; exact current-CLI fence compilation reuses the same locked
# dependency graph and retains bounded headroom here.
timeout-minutes: 60
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
RUST_MIN_STACK: 16777216
steps:
- name: Checkout source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler libprotobuf-dev
- name: Install pinned toolchain
run: rustup toolchain install
- name: Cache cross-version build data
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: |
. -> target
key: crossversion-v5-v6
save-if: ${{ github.ref == 'refs/heads/main' }}
# CI pins only the immediate predecessor. Older seams remain env-gated
# and on-demand, per docs/dev/testing.md.
- name: Build genuine final-v5 CLI
env:
# Final v5-writing main commit immediately before RFC-023 activated
# the exact non-null `id` primary-key contract in v6. Keep this
# immutable: HEAD^ is not a stable format reference.
FINAL_INTERNAL_V5_COMMIT: 46b6d9084fb629b88d4ac9e8c546e0a30d213d19
run: |
set -euo pipefail
v5_source="$RUNNER_TEMP/omnigraph-final-v5"
v5_bin="$RUNNER_TEMP/omnigraph-final-v5-bin"
git fetch --no-tags --depth=1 origin "$FINAL_INTERNAL_V5_COMMIT"
git worktree add --detach "$v5_source" "$FINAL_INTERNAL_V5_COMMIT"
cargo build --locked \
--manifest-path "$v5_source/Cargo.toml" \
--package omnigraph-cli \
--bin omnigraph \
--target-dir "$GITHUB_WORKSPACE/target"
cp "$GITHUB_WORKSPACE/target/debug/omnigraph" "$v5_bin"
test -x "$v5_bin"
# Clean path-package artifacts through both workspace manifests while
# retaining shared registry dependencies. The predecessor and current
# packages have the same names+versions, so either manifest alone can
# miss a stale path identity and make the fence link the wrong rlib.
cargo clean --workspace --locked \
--manifest-path "$v5_source/Cargo.toml" \
--target-dir "$GITHUB_WORKSPACE/target"
cargo clean --workspace --locked \
--target-dir "$GITHUB_WORKSPACE/target"
echo "OMNIGRAPH_V5_BIN=$v5_bin" >> "$GITHUB_ENV"
- name: Run exact v5↔v6 refusal and rebuild fence
run: |
set -euo pipefail
test_log="$RUNNER_TEMP/v5-v6-format-fence.log"
cargo test --locked -p omnigraph-cli --test crossversion_upgrade \
current_v6_refuses_and_rebuilds_genuine_v5_and_v5_refuses_v6 \
-- --exact --nocapture 2>&1 | tee "$test_log"
if grep -Fq "skipping immediate-predecessor v5 upgrade test" "$test_log"; then
echo "::error::v5↔v6 format fence skipped despite a configured predecessor binary"
exit 1
fi
grep -Eq "test current_v6_refuses_and_rebuilds_genuine_v5_and_v5_refuses_v6 \.\.\. ok" "$test_log" \
|| { echo "::error::exact v5↔v6 format fence did not execute normally"; exit 1; }
grep -Eq "test result: ok\. 1 passed; 0 failed" "$test_log" \
|| { echo "::error::v5↔v6 format fence matched the wrong test set"; exit 1; }
test_aws_feature:
name: Test omnigraph-server --features aws
needs: classify_changes
runs-on: ubuntu-latest
# Required context whose cache saves only from main: a cold main run
# that outruns the ceiling is cancelled before the save and every PR
# stays cold (the dst.yml lesson). Sized for a cold dependency graph.
timeout-minutes: 60
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
steps:
- name: Skip for documentation-only changes
if: needs.classify_changes.outputs.run_full_ci != 'true'
run: echo "Documentation-only change detected; skipping aws feature build."
- name: Checkout source
if: needs.classify_changes.outputs.run_full_ci == 'true'
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Install system dependencies
if: needs.classify_changes.outputs.run_full_ci == 'true'
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler libprotobuf-dev
- name: Install pinned toolchain
if: needs.classify_changes.outputs.run_full_ci == 'true'
run: rustup toolchain install
- name: Cache Rust build data
if: needs.classify_changes.outputs.run_full_ci == 'true'
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: |
. -> target
key: aws-feature
# Save only from main, red or green (same rule as `lint`).
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-on-failure: true
- name: Build omnigraph-server with aws feature
if: needs.classify_changes.outputs.run_full_ci == 'true'
run: cargo build --locked -p omnigraph-server --features aws
- name: Test omnigraph-server with aws feature
if: needs.classify_changes.outputs.run_full_ci == 'true'
env:
RUST_MIN_STACK: 16777216
run: cargo test --locked -p omnigraph-server --features aws
rustfs_integration:
name: RustFS S3 Integration (${{ matrix.shard }})
# This remains push-/tag-/dispatch-only, but no longer waits for the
# independent workspace job. Starting both cold compile graphs together
# removes that dependency from the workflow's critical path.
#
# Two parallel feature graphs: the default graph compiles every selected
# engine/server/cluster/CLI test binary in one Cargo invocation, while the
# failpoints graph stays isolated. Presenting the full default DAG together
# avoids discovering it as a serial compile/link waterfall. The previous
# five-shard layout repeated the same ~26-29m cold substrate compile five
# times and maintained five target caches, pressuring GitHub's repository
# cache cap. `fail-fast: false` lets both graphs report independently.
needs: classify_changes
if: github.event_name != 'pull_request' && needs.classify_changes.outputs.run_full_ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
shard:
- default
- failpoints
permissions:
contents: read
env:
AWS_ACCESS_KEY_ID: omnigraphci
AWS_SECRET_ACCESS_KEY: omnigraphci-secret
AWS_REGION: us-east-1
AWS_ENDPOINT_URL: http://127.0.0.1:9000
AWS_ENDPOINT_URL_S3: http://127.0.0.1:9000
AWS_ALLOW_HTTP: "true"
AWS_S3_FORCE_PATH_STYLE: "true"
OMNIGRAPH_S3_TEST_BUCKET: omnigraph-ci
OMNIGRAPH_S3_TEST_PREFIX: github-actions
CARGO_TERM_COLOR: always
RUST_MIN_STACK: 16777216
steps:
- name: Checkout source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler libprotobuf-dev python3-pip
- name: Install pinned toolchain
run: rustup toolchain install
- name: Cache Rust build data
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
# One cache per feature graph avoids both feature thrash and the
# previous five copies of the shared substrate.
key: ${{ matrix.shard }}
workspaces: |
. -> target
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Start RustFS
# Pinned to 1.0.0-beta.12 (2026-07-30). beta.8 carried hyper's short
# header_read_timeout for idle keep-alive connections (rustfs#4360,
# fixed in beta.10), matching the intermittent "error sending request"
# PUT failures observed here on pooled connections. The S3 suites
# (engine s3_storage, server s3, cluster s3_cluster, engine failpoints
# s3_) were validated against the beta.12
# binary locally before this bump. Keep CI pinned to the immutable
# multi-arch index digest so upgrades are deliberate.
run: |
docker rm -f rustfs >/dev/null 2>&1 || true
docker run -d \
--name rustfs \
-p 9000:9000 \
-p 9001:9001 \
-e RUSTFS_ACCESS_KEY="${AWS_ACCESS_KEY_ID}" \
-e RUSTFS_SECRET_KEY="${AWS_SECRET_ACCESS_KEY}" \
rustfs/rustfs:1.0.0-beta.12@sha256:41fe89380f4120a337790c02af192c3fe7bb55c3edc2e6e9357b487b47c6ab21 \
/data
- name: Install AWS CLI
run: |
python3 -m pip install --user awscli
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Create RustFS test bucket
run: |
set -euo pipefail
rustfs_ready=false
for attempt in $(seq 1 30); do
if aws --endpoint-url "${AWS_ENDPOINT_URL_S3}" s3api list-buckets >/dev/null 2>&1; then
rustfs_ready=true
break
fi
echo "RustFS readiness check ${attempt}/30 failed; retrying"
sleep 2
done
if [ "$rustfs_ready" != true ]; then
echo "::error::RustFS did not become ready after 30 attempts"
exit 1
fi
aws --endpoint-url "${AWS_ENDPOINT_URL_S3}" \
s3api create-bucket \
--bucket "${OMNIGRAPH_S3_TEST_BUCKET}"
# Compile and execute the selected default targets in one Cargo resolve.
# A superset prebuild followed by narrower commands would still rebuild
# workspace crates under different feature fingerprints.
- name: Run RustFS default integration graph
if: matrix.shard == 'default'
run: |
set -euo pipefail
test_log="$RUNNER_TEMP/rustfs-default.log"
cargo test --locked \
-p omnigraph-engine \
-p omnigraph-server \
-p omnigraph-cluster \
-p omnigraph-cli \
--test s3_storage \
--test lance_surface_guards \
--test s3 \
--test s3_cluster \
--test system_local \
-- --nocapture --test-threads=1 2>&1 | tee "$test_log"
if grep -Eiq \
"SKIP public_physical_ref_token_rejects_s3_same_version_aba|skipping s3 " \
"$test_log"
then
echo "::error::a configured RustFS test emitted its unconfigured skip"
exit 1
fi
for test_name in \
s3_compatible_graph_lifecycle_works \
s3_branch_change_merge_flow_works \
s3_public_load_uses_hidden_run_and_publishes \
s3_adapter_conditional_writes_contract \
s3_schema_apply_migrates_live_graph \
s3_fresh_branch_traversal_reuses_main_graph_index_with_etags \
public_physical_ref_token_rejects_s3_same_version_aba \
server_opens_s3_graph_directly_and_serves_snapshot_and_read \
server_boots_cluster_from_bare_storage_uri_and_serves_query \
s3_cluster_full_lifecycle_import_apply_serve_evolve_delete \
local_cli_s3_end_to_end_init_load_read_flow
do
grep -Eq "test ${test_name} \.\.\. ok" "$test_log" \
|| { echo "::error::required RustFS cell '${test_name}' did not execute normally"; exit 1; }
done
# NOTE: the RFC-013 step-3a data-table opener COST gate (write_cost_s3) used
# to run here. It is a deterministic IO-count gate, not a correctness test —
# performance/cost contracts belong in a dedicated perf harness on a stable
# runner + own cadence, not on the every-merge correctness path. Moved out of
# CI pending that harness; run it on demand with a bucket set:
# OMNIGRAPH_S3_TEST_BUCKET=… cargo test -p omnigraph-engine --test write_cost_s3
- name: Run RustFS recovery-sidecar lifecycle
if: matrix.shard == 'failpoints'
# Sidecar put/list/delete through the S3 storage backend on a
# real bucket (the failpoint only wedges the publisher; the
# sidecar I/O is exercised for real). Name filter `s3_` matches
# the bucket-gated tests in the failpoints target only; the
# grep guards against the filter going vacuous (cargo passes
# with 0 tests matched) if those tests are ever renamed.
run: |
set +e
output=$(cargo test --locked -p omnigraph-engine --features failpoints --test failpoints s3_ -- --nocapture 2>&1); status=$?
set -e
echo "$output"
[ "$status" -eq 0 ] || exit "$status"
echo "$output" | grep -Eq "test result: ok\. [1-9][0-9]* passed" \
|| { echo "::error::filter 's3_' matched no tests — vacuous pass"; exit 1; }
- name: Dump RustFS diagnostics on failure
if: failure()
run: |
set +e
echo "::group::docker inspect rustfs"
# Limit inspection to runtime state: full inspect output includes the
# container environment and would disclose the test credentials.
docker inspect --format 'State={{json .State}} Image={{.Image}}' rustfs
echo "::endgroup::"
echo "::group::docker logs rustfs"
docker logs rustfs
echo "::endgroup::"
diagnostics_dir=$(mktemp -d "${RUNNER_TEMP}/rustfs-logs.XXXXXX")
echo "::group::RustFS /logs"
if docker cp rustfs:/logs "$diagnostics_dir"; then
find "$diagnostics_dir" -type f -exec sh -c '
for log_file do
echo "===== ${log_file} ====="
tail -n 2000 "$log_file"
done
' sh {} +
else
echo "Unable to copy /logs from the RustFS container"
fi
echo "::endgroup::"
azurite_integration:
name: Azurite Azure Integration
# This configured backend contract runs after merge, on tags, and by
# manual dispatch. Keep it independent of `test` so cold backend graphs
# start together and a failure reports at its real boundary.
needs: classify_changes
if: github.event_name != 'pull_request' && needs.classify_changes.outputs.run_full_ci == 'true'
runs-on: ubuntu-latest
# Cold failpoint and default-feature Lance graphs compile sequentially on
# the first cache miss; keep the timeout above the RustFS shard envelope.
timeout-minutes: 90
permissions:
contents: read
env:
AZURE_STORAGE_USE_EMULATOR: "true"
AZURE_STORAGE_ACCOUNT_NAME: devstoreaccount1
AZURITE_BLOB_STORAGE_URL: http://127.0.0.1:10000
OMNIGRAPH_AZURE_TEST_CONTAINER: omnigraph-tests
CARGO_TERM_COLOR: always
RUST_MIN_STACK: 16777216
steps:
- name: Checkout source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler libprotobuf-dev
- name: Install pinned toolchain
run: rustup toolchain install
- name: Cache Azure integration build data
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: |
. -> target
key: azurite-azure
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Start pinned Azurite Blob service
# Azurite 3.35.0, pinned to the immutable multi-arch index digest.
run: |
set -euo pipefail
docker rm -f azurite >/dev/null 2>&1 || true
docker run -d \
--name azurite \
-p 10000:10000 \
mcr.microsoft.com/azure-storage/azurite:3.35.0@sha256:647c63a91102a9d8e8000aab803436e1fc85fbb285e7ce830a82ee5d6661cf37 \
azurite-blob --blobHost 0.0.0.0 --blobPort 10000
for _ in $(seq 1 60); do
if curl --silent --show-error \
"${AZURITE_BLOB_STORAGE_URL}/${AZURE_STORAGE_ACCOUNT_NAME}?comp=list" \
>/dev/null
then
exit 0
fi
sleep 1
done
echo "::error::Azurite did not become reachable"
docker logs azurite
exit 1
- name: Create Azurite test container
# This is the public emulator key, never a production credential. The
# emulator canonicalized resource repeats the account path segment.
run: |
python3 - <<'PY'
import base64
import datetime
import email.utils
import hashlib
import hmac
import os
import urllib.request
account = os.environ['AZURE_STORAGE_ACCOUNT_NAME']
container = os.environ['OMNIGRAPH_AZURE_TEST_CONTAINER']
endpoint = os.environ['AZURITE_BLOB_STORAGE_URL']
key = base64.b64decode(
'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/'
'K1SZFPTOtr/KBHBeksoGMGw=='
)
date = email.utils.format_datetime(
datetime.datetime.now(datetime.timezone.utc), usegmt=True
)
version = '2023-11-03'
canonical_headers = f'x-ms-date:{date}\nx-ms-version:{version}\n'
canonical_resource = (
f'/{account}/{account}/{container}\nrestype:container'
)
string_to_sign = (
'PUT\n\n\n\n\n\n\n\n\n\n\n\n'
+ canonical_headers
+ canonical_resource
)
signature = base64.b64encode(
hmac.new(key, string_to_sign.encode(), hashlib.sha256).digest()
).decode()
request = urllib.request.Request(
f'{endpoint}/{account}/{container}?restype=container',
method='PUT',
headers={
'Authorization': f'SharedKey {account}:{signature}',
'Content-Length': '0',
'x-ms-date': date,
'x-ms-version': version,
},
)
with urllib.request.urlopen(request, timeout=10) as response:
if response.status != 201:
raise SystemExit(f'unexpected create-container status: {response.status}')
PY
- name: Run exact Azure integration owners
run: |
set -euo pipefail
test_log="$RUNNER_TEMP/azurite-azure.log"
: >"$test_log"
cargo test --locked -p omnigraph-storage \
tests::contract_suite_azure_when_configured \
-- --exact --nocapture 2>&1 | tee -a "$test_log"
cargo test --locked -p omnigraph-azure-admission \
tests::configured_azurite_enforces_one_lease_owner_and_explicit_break \
-- --exact --nocapture 2>&1 | tee -a "$test_log"
cargo test --locked -p omnigraph-azure-admission --test supervisor \
-- --nocapture 2>&1 | tee -a "$test_log"
cargo test --locked -p omnigraph-engine --features failpoints --test failpoints \
azure_recovery_rolls_forward_after_finalize_publisher_failure \
-- --exact --nocapture 2>&1 | tee -a "$test_log"
cargo test --locked -p omnigraph-engine --features failpoints --test failpoints \
azure_schema_apply_recovers_source_and_destination_after_partial_rename \
-- --exact --nocapture 2>&1 | tee -a "$test_log"
cargo test --locked -p omnigraph-cluster --test s3_cluster \
azure_cluster_full_lifecycle_import_apply_serve_evolve_delete \
-- --exact --nocapture 2>&1 | tee -a "$test_log"
cargo test --locked -p omnigraph-server --test s3 \
server_opens_azure_graph_directly_and_serves_snapshot_and_read \
-- --exact --nocapture 2>&1 | tee -a "$test_log"
cargo test --locked -p omnigraph-server --test s3 \
server_boots_azure_cluster_from_bare_storage_uri_and_serves_query \
-- --exact --nocapture 2>&1 | tee -a "$test_log"
cargo test --locked -p omnigraph-cli --test system_local \
local_cli_azure_end_to_end_init_load_read_flow \
-- --exact --nocapture 2>&1 | tee -a "$test_log"
if grep -Eiq 'skipping .*azure|skipping .*azurite' "$test_log"; then
echo "::error::a configured Azure test emitted its unconfigured skip"
exit 1
fi
for test_name in \
contract_suite_azure_when_configured \
configured_azurite_enforces_one_lease_owner_and_explicit_break \
held_admission_never_launches_child_then_runs_once_after_release \
held_job_terminated_before_admission_fails_without_launching_child \
graceful_server_signal_drains_process_group_before_release \
unexpected_server_exit_strands_lease_until_explicit_break \
azure_recovery_rolls_forward_after_finalize_publisher_failure \
azure_schema_apply_recovers_source_and_destination_after_partial_rename \
azure_cluster_full_lifecycle_import_apply_serve_evolve_delete \
server_opens_azure_graph_directly_and_serves_snapshot_and_read \
server_boots_azure_cluster_from_bare_storage_uri_and_serves_query \
local_cli_azure_end_to_end_init_load_read_flow
do
grep -Eq "test (tests::)?${test_name} \.\.\. ok" "$test_log" \
|| { echo "::error::required Azure cell '${test_name}' did not execute normally"; exit 1; }
done
- name: Verify control and Lance objects share the declared container
run: |
python3 - "$RUNNER_TEMP/azurite-inventory.txt" <<'PY'
import base64
import datetime
import email.utils
import hashlib
import hmac
import os
import pathlib
import urllib.request
import xml.etree.ElementTree as ET
account = os.environ['AZURE_STORAGE_ACCOUNT_NAME']
container = os.environ['OMNIGRAPH_AZURE_TEST_CONTAINER']
endpoint = os.environ['AZURITE_BLOB_STORAGE_URL']
key = base64.b64decode(
'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/'
'K1SZFPTOtr/KBHBeksoGMGw=='
)
date = email.utils.format_datetime(
datetime.datetime.now(datetime.timezone.utc), usegmt=True
)
version = '2023-11-03'
canonical_headers = f'x-ms-date:{date}\nx-ms-version:{version}\n'
canonical_resource = (
f'/{account}/{account}/{container}\ncomp:list\nrestype:container'
)
string_to_sign = (
'GET\n\n\n\n\n\n\n\n\n\n\n\n'
+ canonical_headers
+ canonical_resource
)
signature = base64.b64encode(
hmac.new(key, string_to_sign.encode(), hashlib.sha256).digest()
).decode()
request = urllib.request.Request(
f'{endpoint}/{account}/{container}?comp=list&restype=container',
headers={
'Authorization': f'SharedKey {account}:{signature}',
'x-ms-date': date,
'x-ms-version': version,
},
)
with urllib.request.urlopen(request, timeout=10) as response:
inventory = ET.fromstring(response.read())
if inventory.findtext('NextMarker'):
raise SystemExit('Azure inventory exceeded one explicit 5,000-object page')
names = [node.text or '' for node in inventory.findall('.//Name')]
pathlib.Path(__import__('sys').argv[1]).write_text('\n'.join(names) + '\n')
if not any('/__cluster/' in name for name in names):
raise SystemExit('no Azure cluster control object was observed')
if not any('.lance/' in name or '/_versions/' in name for name in names):
raise SystemExit('no Azure Lance dataset object was observed')
if not any(name.startswith('__omnigraph_azure_admission/v1/') for name in names):
raise SystemExit('no root-derived admission object was observed')
PY
- name: Upload Azurite integration evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: azurite-azure-integration-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ runner.temp }}/azurite-azure.log
${{ runner.temp }}/azurite-inventory.txt
if-no-files-found: warn
retention-days: 14
- name: Dump Azurite logs on failure
if: failure()
run: docker logs azurite