diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3338f8e09..b6a677f526 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2444,8 +2444,16 @@ jobs: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository env: + # OMN-16076: no hardcoded fallback SHA. The old pin was a squash + # commit reachable only through a since-deleted stacked branch, so + # the first push-event run of this step (post-merge on main, which + # never has a pull_request/merge_group base) could not resolve it + # and crashed. push event.before is the previous main tip -- for a + # promotion merge that is exactly the promotion PR's base, so the + # push verdict reproduces the proven PR verdict. An empty or + # unreachable base fails loudly inside the script with remediation. APPLICATION_SQL_BASE_REVISION: >- - ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || '7228ce0c0934ae096dd6effd0f84ff1913fec6c0' }} + ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before }} run: >- uv run python scripts/ci/check_application_database_sql.py --base-revision "$APPLICATION_SQL_BASE_REVISION" diff --git a/docker/runners/runner-image.lock.json b/docker/runners/runner-image.lock.json index feafe0e9fe..839aefd8f1 100644 --- a/docker/runners/runner-image.lock.json +++ b/docker/runners/runner-image.lock.json @@ -1,12 +1,12 @@ { "base_image_digest": "sha256:3ba65aa20f86a0fad9df2b2c259c613df006b2e6d0bfcc8a146afb8c525a9751", "gh_version": "2.67.0", - "identity_digest": "17b97156cc173de6faa32489b88ac961", + "identity_digest": "eee99c0dd59126bbc2751066b31ff191", "image_version": 7, "kubectl_version": "1.32.1", "python_version": "3.12", "runner_version": "2.336.0", - "shared_env_digest": "bffad7795606a13c66aefdac", + "shared_env_digest": "577478fed88afe4bec35906c", "shared_env_install_args": "--frozen --all-extras --all-groups --no-install-project", "uv_version": "0.6.14" } diff --git a/pyproject.toml b/pyproject.toml index d88d377204..fadd784d24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "omnibase_infra" -version = "0.38.6" +version = "0.38.7" description = "ONEX Infrastructure - Service integration and database infrastructure tools" authors = [{ name = "OmniNode.ai", email = "contact@omninode.ai" }] license = {text = "MIT"} diff --git a/scripts/ci/check_application_database_sql.py b/scripts/ci/check_application_database_sql.py index c3e219da01..0b2eac8adf 100644 --- a/scripts/ci/check_application_database_sql.py +++ b/scripts/ci/check_application_database_sql.py @@ -204,12 +204,45 @@ def _is_legacy_default_schema_sql_path(relative_path: Path) -> bool: return relative_path in _LEGACY_DEFAULT_SCHEMA_SQL_EXACT_PATHS +_ZERO_REVISION = "0" * 40 + +# The trusted CI step resolves the diff base from workflow event context: +# pull_request.base.sha, merge_group.base_sha, or push event.before. A pinned +# fallback SHA is forbidden -- a commit reachable only through a since-deleted +# stacked branch is absent from every checkout, so the first push-event run of +# this gate crashed on a raw git fatal instead of a diagnosable verdict +# (OMN-16076). Validate the base up front and fail with the remediation. + + +def _assert_base_revision_resolvable(repository: Path, base_revision: str) -> None: + if not base_revision or base_revision == _ZERO_REVISION: + raise RuntimeError( + "no usable base revision for changed-SQL linting: got " + f"{base_revision!r}; the workflow event context must supply " + "pull_request.base.sha, merge_group.base_sha, or push event.before" + ) + probe = subprocess.run( + ["git", "rev-parse", "--verify", "--quiet", f"{base_revision}^{{commit}}"], + cwd=repository, + capture_output=True, + check=False, + ) + if probe.returncode != 0: + raise RuntimeError( + f"base revision {base_revision} is not reachable in this checkout; " + "it must come from the workflow event context " + "(pull_request.base.sha, merge_group.base_sha, or push " + "event.before), never a hardcoded pin" + ) + + def changed_sql_paths( repository: Path, base_revision: str, head_revision: str, ) -> tuple[Path, ...]: """Return changed deployable SQL, excluding the exact ephemeral proof seed.""" + _assert_base_revision_resolvable(repository, base_revision) result = subprocess.run( [ "git", diff --git a/tests/ci/test_application_database_sql_gate.py b/tests/ci/test_application_database_sql_gate.py index 4e97eb8b40..20b3593841 100644 --- a/tests/ci/test_application_database_sql_gate.py +++ b/tests/ci/test_application_database_sql_gate.py @@ -4,12 +4,14 @@ from __future__ import annotations +import re import subprocess from pathlib import Path import pytest from scripts.ci.check_application_database_sql import ( + changed_sql_paths, validate_changed_sql, violation_key, ) @@ -563,3 +565,82 @@ def test_violation_key_is_content_addressed_not_positional() -> None: assert violation_key(same) == violation_key(same) assert violation_key(same) != violation_key(same.replace("a.sql", "b.sql")) assert violation_key(same) != violation_key(same.replace("'x'", "'y'")) + + +# --------------------------------------------------------------------------- +# OMN-16076: push-event base-revision resolution. The trusted CI step has no +# pull_request/merge_group base on a push event; its fallback must be push +# event.before, never a hardcoded SHA. The old pin was reachable only through +# a since-deleted stacked branch, so the first main-push run of the gate died +# on a raw git fatal ("Invalid symmetric difference expression") instead of a +# verdict. These tests hold both the script's error contract and the workflow +# expression itself. +# --------------------------------------------------------------------------- + + +def _two_commit_repository(tmp_path: Path) -> tuple[Path, str, str]: + repository = tmp_path / "repository" + repository.mkdir() + _git(repository, "init", "--initial-branch=main") + (repository / "seed.sql").write_text( + "CREATE TABLE tenant.seed_table (id uuid);\n", encoding="utf-8" + ) + base = _commit(repository, "baseline") + (repository / "next.sql").write_text( + "CREATE TABLE tenant.next_table (id uuid);\n", encoding="utf-8" + ) + head = _commit(repository, "next") + return repository, base, head + + +def test_push_event_previous_commit_base_resolves_changed_sql( + tmp_path: Path, +) -> None: + repository, base, head = _two_commit_repository(tmp_path) + changed = changed_sql_paths(repository, base, head) + assert [path.name for path in changed] == ["next.sql"] + + +def test_an_empty_base_revision_fails_with_remediation(tmp_path: Path) -> None: + repository, _base, head = _two_commit_repository(tmp_path) + with pytest.raises(RuntimeError, match="event context"): + changed_sql_paths(repository, "", head) + + +def test_a_zero_base_revision_fails_with_remediation(tmp_path: Path) -> None: + repository, _base, head = _two_commit_repository(tmp_path) + with pytest.raises(RuntimeError, match="event context"): + changed_sql_paths(repository, "0" * 40, head) + + +def test_an_unreachable_base_revision_fails_with_remediation( + tmp_path: Path, +) -> None: + repository, _base, head = _two_commit_repository(tmp_path) + unreachable = "7228ce0c0934ae096dd6effd0f84ff1913fec6c0" + with pytest.raises(RuntimeError) as excinfo: + changed_sql_paths(repository, unreachable, head) + message = str(excinfo.value) + assert "not reachable in this checkout" in message + assert "hardcoded pin" in message + assert "Invalid symmetric difference" not in message + + +def test_ci_workflow_base_revision_has_no_hardcoded_pin() -> None: + workflow = (_ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") + lines = workflow.splitlines() + expression_lines: list[str] = [] + for index, line in enumerate(lines): + if "APPLICATION_SQL_BASE_REVISION" not in line: + continue + expression_lines.extend(lines[index : index + 2]) + assert expression_lines, "trusted step no longer sets the base revision" + for line in expression_lines: + assert not re.search(r"[0-9a-f]{40}", line), ( + "APPLICATION_SQL_BASE_REVISION must resolve from event context, " + f"never a hardcoded SHA: {line.strip()}" + ) + assert any("github.event.before" in line for line in expression_lines), ( + "push events have no pull_request/merge_group base; the fallback " + "must be github.event.before" + ) diff --git a/uv.lock b/uv.lock index 09feaf3334..1d21014ce8 100644 --- a/uv.lock +++ b/uv.lock @@ -2135,7 +2135,7 @@ wheels = [ [[package]] name = "omnibase-infra" -version = "0.38.6" +version = "0.38.7" source = { editable = "." } dependencies = [ { name = "a2a-sdk" },