Repository navigation
feat(OMN-14462): scripts/merge-proof local-proof env wrapper (F-20) #1805
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
5dbcfc3
feat(OMN-14462): add scripts/merge-proof local-proof env wrapper (F-20)
fe22190
test(OMN-14462): avoid literal path prefixes in merge-proof test
2cc9651
fix(OMN-14462): harden merge-proof exported environment
8127bb1
test(OMN-14462): make merge-proof topology hermetic
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| 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 "$@" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.