Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions scripts/merge-proof
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT
#
# merge-proof — Resolve the environment that omnimarket local proof gates need,
# then run (or hand off to) that proof. Fails fast with the exact `export` lines
# to run when the environment cannot be resolved — it never proceeds on a silent
# wrong path. (OMN-14462 / F-20)
#
# Why: local hooks/probes (e.g. the topic-naming lint) resolve their validator
# from a sibling `omnibase_infra` checkout. Under the mandated worktree layout
# ($OMNI_HOME/omni_worktrees/<ticket>/omnimarket) the naive `../omnibase_infra`
# sibling path does not exist, so a correct change fails locally until the hidden
# environment is reconstructed by hand. This wrapper resolves that environment
# deterministically from its own on-disk location (rule #8) and, when it cannot,
# prints the exact `export` line to run rather than failing on a wrong path.
#
# Usage:
# scripts/merge-proof [--check | --print-env | -- CMD [ARGS...]]
#
# (no args) Resolve+export env, then run the local topic-lint proof.
# --check Resolve env, verify the validator exists, print resolved
# paths, exit 0. Exit non-zero with guidance if unresolved.
# --print-env Print eval-able `export` lines for the resolved env.
# -- CMD [ARGS...] Resolve+export env, then exec CMD with that env
# (e.g. `scripts/merge-proof -- pre-commit run --files X`).
#
# Environment (auto-resolved from this script's location; export to override):
# OMNI_HOME omni_home checkout root (anchor)
# OMNIBASE_INFRA_PATH sibling omnibase_infra checkout (topic-lint validator)
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." >/dev/null 2>&1 && pwd)"

RESOLVED_OMNI_HOME=""
RESOLVED_INFRA=""

_is_dir() { [ -n "${1:-}" ] && [ -d "${1}" ]; }

shell_quote() {
printf '%q' "$1"
}

usage() {
sed -n '10,32p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
}

# Derive the omni_home anchor from this checkout's location. Deterministic
# Path-relative resolution (rule #8), never a hardcoded default.
derive_omni_home() {
if _is_dir "${OMNI_HOME:-}"; then
printf '%s\n' "$(cd "${OMNI_HOME}" && pwd)"
return 0
fi
local candidate=""
case "${REPO_ROOT}" in
*/omni_worktrees/*)
# $OMNI_HOME/omni_worktrees/<ticket>/omnimarket
candidate="${REPO_ROOT%%/omni_worktrees/*}"
;;
*)
# Canonical registry layout: <omni_home>/omnimarket
candidate="$(cd "${REPO_ROOT}/.." >/dev/null 2>&1 && pwd)"
;;
esac
# Only accept a candidate that actually looks like omni_home (has the sibling).
if _is_dir "${candidate}" && _is_dir "${candidate}/omnibase_infra"; then
printf '%s\n' "${candidate}"
return 0
fi
return 1
}

resolve_env() {
if RESOLVED_OMNI_HOME="$(derive_omni_home)"; then
:
else
RESOLVED_OMNI_HOME=""
fi

if _is_dir "${OMNIBASE_INFRA_PATH:-}"; then
RESOLVED_INFRA="$(cd "${OMNIBASE_INFRA_PATH}" && pwd)"
elif _is_dir "${RESOLVED_OMNI_HOME}/omnibase_infra"; then
RESOLVED_INFRA="${RESOLVED_OMNI_HOME}/omnibase_infra"
elif _is_dir "${REPO_ROOT}/../omnibase_infra"; then
RESOLVED_INFRA="$(cd "${REPO_ROOT}/../omnibase_infra" >/dev/null 2>&1 && pwd)"
fi
}

guidance_and_die() {
{
echo "error: merge-proof could not resolve the environment omnimarket proof gates need."
echo
echo "The topic-naming lint hook resolves its validator from a sibling omnibase_infra"
echo "checkout. Set the environment explicitly, then re-run:"
echo
if [ -n "${RESOLVED_OMNI_HOME}" ]; then
echo " export OMNI_HOME=$(shell_quote "${RESOLVED_OMNI_HOME}")"
else
echo " export OMNI_HOME=/path/to/omni_home"
fi
echo " export OMNIBASE_INFRA_PATH=\"\$OMNI_HOME/omnibase_infra\""
echo
echo "Then re-run: scripts/merge-proof --check"
} >&2
exit 3
}

main() {
local mode="run"
local -a passthrough=()
case "${1:-}" in
--check) mode="check" ;;
--print-env) mode="print-env" ;;
-h | --help)
usage
exit 0
;;
--)
mode="exec"
shift
passthrough=("$@")
;;
"") mode="run" ;;
*)
echo "merge-proof: unknown option: ${1}" >&2
usage >&2
exit 2
;;
esac

resolve_env

# OMNIBASE_INFRA_PATH is the load-bearing var for local proof; without a real
# sibling checkout the topic-lint validator cannot be found.
if [ -z "${RESOLVED_INFRA}" ]; then
guidance_and_die
fi

export OMNIBASE_INFRA_PATH="${RESOLVED_INFRA}"
if [ -n "${RESOLVED_OMNI_HOME}" ]; then
export OMNI_HOME="${RESOLVED_OMNI_HOME}"
fi
Comment thread
jonahgabriel marked this conversation as resolved.
unset GIT_DIR GIT_INDEX_FILE GIT_WORK_TREE

case "${mode}" in
check)
local lint="${OMNIBASE_INFRA_PATH}/scripts/validation/lint_topic_names.py"
echo "merge-proof: OMNI_HOME=${OMNI_HOME:-<unresolved>}"
echo "merge-proof: OMNIBASE_INFRA_PATH=${OMNIBASE_INFRA_PATH}"
if [ ! -f "${lint}" ]; then
echo "error: topic-lint validator not found at ${lint}" >&2
echo " (is OMNIBASE_INFRA_PATH pointing at a real omnibase_infra checkout?)" >&2
exit 4
fi
echo "merge-proof: environment OK"
;;
print-env)
echo "export OMNI_HOME=$(shell_quote "${OMNI_HOME:-}")"
echo "export OMNIBASE_INFRA_PATH=$(shell_quote "${OMNIBASE_INFRA_PATH}")"
;;
exec)
if [ "${#passthrough[@]}" -eq 0 ]; then
echo "merge-proof: -- requires a command to run" >&2
exit 2
fi
exec "${passthrough[@]}"
;;
run)
cd "${REPO_ROOT}"
exec bash "${REPO_ROOT}/scripts/validation/run_topic_lint.sh"
;;
esac
}

main "$@"
206 changes: 206 additions & 0 deletions tests/scripts/test_merge_proof.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT
"""Tests for scripts/merge-proof (OMN-14462 / F-20).

The wrapper resolves the environment omnimarket local proof gates need
(OMNI_HOME + OMNIBASE_INFRA_PATH) deterministically from its own on-disk
location, and — when it cannot — fails fast with the exact ``export`` lines the
operator must run, rather than proceeding on a silent wrong path.

These tests drive the real script via subprocess in an isolated tmp location so
derivation deterministically fails unless the environment is supplied. The git
env vars are stripped from the child environment per the OMN-14746/14744
worktree-safety lesson (the wrapper runs no git, but callers must never inherit
GIT_DIR/GIT_INDEX_FILE/GIT_WORK_TREE into subprocesses).
"""

from __future__ import annotations

import os
import shutil
import subprocess
from pathlib import Path

import pytest

REPO_ROOT = Path(__file__).resolve().parents[2]
WRAPPER = REPO_ROOT / "scripts" / "merge-proof"


def _clean_env() -> dict[str, str]:
env = {
k: v
for k, v in os.environ.items()
if k
not in {
"OMNI_HOME",
"OMNIBASE_INFRA_PATH",
"GIT_DIR",
"GIT_INDEX_FILE",
"GIT_WORK_TREE",
}
}
return env


@pytest.fixture
def isolated_wrapper(tmp_path: Path) -> Path:
"""Copy the wrapper into a location where env derivation cannot succeed.

Not under ``omni_worktrees/`` and with no sibling ``omnibase_infra``.
"""
scripts = tmp_path / "isolated" / "scripts"
scripts.mkdir(parents=True)
dest = scripts / "merge-proof"
shutil.copy2(WRAPPER, dest)
dest.chmod(0o755)
return dest


def _run(
wrapper: Path, *args: str, env: dict[str, str]
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["bash", str(wrapper), *args],
env=env,
capture_output=True,
text=True,
timeout=60,
)


def test_wrapper_exists_and_is_executable() -> None:
assert WRAPPER.is_file(), f"missing wrapper: {WRAPPER}"
assert os.access(WRAPPER, os.X_OK), "wrapper must be executable"


def test_no_hardcoded_absolute_paths() -> None:
"""Rule #6: no local absolute path prefixes in the wrapper."""
text = WRAPPER.read_text()
forbidden_prefixes = ["/" + "Users/", "/" + "Volumes/"]
for prefix in forbidden_prefixes:
assert prefix not in text, (
f"hardcoded local absolute path in merge-proof: {prefix}"
)


def test_unresolved_env_fails_with_export_guidance(isolated_wrapper: Path) -> None:
"""Env UNSET and no derivable sibling → non-zero exit AND exact export lines.

Asserting on the *presence* of the guidance (not merely non-zero exit) so a
silent wrong-path failure cannot pass this test.
"""
result = _run(isolated_wrapper, "--check", env=_clean_env())
assert result.returncode != 0, f"expected failure, got 0\nstdout={result.stdout}"
combined = result.stdout + result.stderr
assert "export OMNI_HOME=" in combined, combined
assert "export OMNIBASE_INFRA_PATH=" in combined, combined


def test_resolved_env_check_passes(isolated_wrapper: Path, tmp_path: Path) -> None:
"""Env SET to a real omnibase_infra checkout → --check exits 0."""
fake_infra = tmp_path / "fakeinfra"
(fake_infra / "scripts" / "validation").mkdir(parents=True)
(fake_infra / "scripts" / "validation" / "lint_topic_names.py").write_text("")
env = _clean_env()
env["OMNIBASE_INFRA_PATH"] = str(fake_infra)
result = _run(isolated_wrapper, "--check", env=env)
assert result.returncode == 0, f"stdout={result.stdout}\nstderr={result.stderr}"
assert "environment OK" in result.stdout


def test_check_fails_when_infra_lacks_validator(
isolated_wrapper: Path, tmp_path: Path
) -> None:
"""OMNIBASE_INFRA_PATH points at a dir with no validator → non-zero exit."""
empty_infra = tmp_path / "emptyinfra"
empty_infra.mkdir()
env = _clean_env()
env["OMNIBASE_INFRA_PATH"] = str(empty_infra)
result = _run(isolated_wrapper, "--check", env=env)
assert result.returncode != 0
assert "validator not found" in (result.stdout + result.stderr)


def test_print_env_emits_evalable_exports(
isolated_wrapper: Path, tmp_path: Path
) -> None:
fake_home = tmp_path / "omni home; echo bad"
fake_home.mkdir()
fake_infra = tmp_path / "fake infra; echo bad"
fake_infra.mkdir()
env = _clean_env()
env["OMNI_HOME"] = str(fake_home)
env["OMNIBASE_INFRA_PATH"] = str(fake_infra)
result = _run(isolated_wrapper, "--print-env", env=env)
assert result.returncode == 0
eval_result = subprocess.run(
[
"bash",
"-c",
(
f"{result.stdout}\n"
'printf "%s\\n%s\\n" "$OMNI_HOME" "$OMNIBASE_INFRA_PATH"'
),
],
env=_clean_env(),
capture_output=True,
text=True,
timeout=60,
)
assert eval_result.returncode == 0, eval_result.stderr
assert eval_result.stdout.splitlines() == [str(fake_home), str(fake_infra)]


def test_delegated_command_does_not_inherit_git_worktree_bindings(
isolated_wrapper: Path, tmp_path: Path
) -> None:
fake_infra = tmp_path / "fakeinfra"
fake_infra.mkdir()
env = _clean_env()
env.update(
{
"OMNIBASE_INFRA_PATH": str(fake_infra),
"GIT_DIR": "/tmp/wrong.git",
"GIT_INDEX_FILE": "/tmp/wrong.index",
"GIT_WORK_TREE": "/tmp/wrong-worktree",
}
)
result = _run(
isolated_wrapper,
"--",
"bash",
"-c",
'printf "%s|%s|%s" "${GIT_DIR-unset}" "${GIT_INDEX_FILE-unset}" "${GIT_WORK_TREE-unset}"',
env=env,
)
assert result.returncode == 0, result.stderr
assert result.stdout == "unset|unset|unset"


def test_omni_worktree_layout_autoresolves_without_env(tmp_path: Path) -> None:
"""From the mandated worktree layout the wrapper derives env with no exports.

This is the ergonomic win: a correct change proves locally without the
operator reconstructing hidden environment. The topology is built
hermetically instead of relying on CI to have a sibling omnibase_infra
checkout next to the test checkout.
"""
fake_home = tmp_path / "omni_home"
worktree_repo = fake_home / "omni_worktrees" / "OMN-14462" / "omnimarket"
scripts = worktree_repo / "scripts"
scripts.mkdir(parents=True)
wrapper = scripts / "merge-proof"
shutil.copy2(WRAPPER, wrapper)
wrapper.chmod(0o755)

fake_infra_validator = (
fake_home / "omnibase_infra" / "scripts" / "validation" / "lint_topic_names.py"
)
fake_infra_validator.parent.mkdir(parents=True)
fake_infra_validator.write_text("")

result = _run(wrapper, "--check", env=_clean_env())
assert result.returncode == 0, f"stdout={result.stdout}\nstderr={result.stderr}"
assert f"OMNI_HOME={fake_home}" in result.stdout
assert f"OMNIBASE_INFRA_PATH={fake_home / 'omnibase_infra'}" in result.stdout
Loading