mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-12 04:39:51 +08:00
* ci(release): add advisory compatibility review Signed-off-by: Simon Scatton <sscatton@nvidia.com> * test(ci): remove compatibility reviewer test harness Signed-off-by: Simon Scatton <sscatton@nvidia.com> * ci(release): run compatibility SDK in the dev shell Signed-off-by: Simon Scatton <sscatton@nvidia.com> * ci(release): trigger a one-shot compatibility review Signed-off-by: Simon Scatton <sscatton@nvidia.com> * ci(release): remove one-shot review trigger Signed-off-by: Simon Scatton <sscatton@nvidia.com> --------- Signed-off-by: Simon Scatton <sscatton@nvidia.com>
183 lines
7.0 KiB
Python
183 lines
7.0 KiB
Python
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""Run an advisory, cumulative release compatibility review with Codex."""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
from importlib.metadata import version
|
|
from pathlib import Path
|
|
|
|
# Installed only in the review workflow's isolated Python environment.
|
|
import openai_codex # ty: ignore[unresolved-import]
|
|
from check_proto_compatibility import git, train_policy
|
|
from codex_compatibility_report import validate_review, write_report
|
|
|
|
ASSETS = Path(__file__).resolve().parents[2] / ".github" / "prompts"
|
|
MODEL = "openai/openai/gpt-5.6-sol"
|
|
ENDPOINT = "https://inference-api.nvidia.com/v1"
|
|
|
|
|
|
def resolve_candidate(tag: str, expected_sha: str) -> dict:
|
|
if not re.fullmatch(r"v0\.\d+\.\d+(?:-pre\.\d+)?", tag):
|
|
raise ValueError("Compatibility review requires a tagged 0.x release.")
|
|
candidate = git(
|
|
"rev-parse", "--verify", "--end-of-options", f"refs/tags/{tag}^{{commit}}"
|
|
)
|
|
if expected_sha and candidate != expected_sha:
|
|
raise ValueError("Candidate tag does not match the qualification source SHA.")
|
|
baseline, train, allows_breaks = train_policy(candidate, tag)
|
|
return {
|
|
"candidate_tag": tag,
|
|
"candidate_sha": candidate,
|
|
"baseline_tag": baseline,
|
|
"baseline_sha": git(
|
|
"rev-parse", "--verify", f"refs/tags/{baseline}^{{commit}}"
|
|
),
|
|
"train": train,
|
|
"allows_breaks": allows_breaks,
|
|
}
|
|
|
|
|
|
async def invoke_codex(context: dict, directory: Path) -> dict:
|
|
# Git objects expose both revisions without loading candidate-side agent
|
|
# instructions, configuration, skills, or hooks into the review workspace.
|
|
subprocess.run(
|
|
[
|
|
"git",
|
|
"clone",
|
|
"--bare",
|
|
"--no-hardlinks",
|
|
"--quiet",
|
|
".",
|
|
str(directory / "source.git"),
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
settings = (
|
|
"allow_login_shell=false",
|
|
'model_providers.nvidia.name="NVIDIA Inference"',
|
|
f'model_providers.nvidia.base_url="{ENDPOINT}"',
|
|
'model_providers.nvidia.env_key="NVIDIA_INFERENCE_API_KEY"',
|
|
'model_providers.nvidia.wire_api="responses"',
|
|
"model_providers.nvidia.supports_websockets=false",
|
|
'model_reasoning_effort="medium"',
|
|
'web_search="disabled"',
|
|
"project_doc_max_bytes=0",
|
|
"features.hooks=false",
|
|
"features.plugins=false",
|
|
"features.multi_agent=false",
|
|
"features.multi_agent_v2=false",
|
|
'shell_environment_policy.inherit="none"',
|
|
"shell_environment_policy.ignore_default_excludes=false",
|
|
)
|
|
prompt = (ASSETS / "compatibility-review.md").read_text()
|
|
prompt += "\nReview context:\n" + json.dumps(context)
|
|
schema = json.loads((ASSETS / "compatibility-review.schema.json").read_text())
|
|
codex = openai_codex.AsyncCodex(
|
|
openai_codex.CodexConfig(cwd=str(directory), config_overrides=settings)
|
|
)
|
|
# Raw agent output can include inspected source or tool output. Publish only
|
|
# the validated final report, never the transcript or credential environment.
|
|
try:
|
|
async with asyncio.timeout(1800):
|
|
thread = await codex.thread_start(
|
|
cwd=str(directory),
|
|
model=MODEL,
|
|
model_provider="nvidia",
|
|
sandbox=openai_codex.Sandbox.read_only,
|
|
approval_mode=openai_codex.ApprovalMode.deny_all,
|
|
ephemeral=True,
|
|
)
|
|
result = await thread.run(prompt, output_schema=schema)
|
|
except (openai_codex.CodexError, RuntimeError, ValueError) as error:
|
|
raise ValueError(
|
|
f"Codex SDK review failed ({type(error).__name__}); no assessment is available."
|
|
) from None
|
|
finally:
|
|
await codex.close()
|
|
if result.status.value != "completed" or not result.final_response:
|
|
raise ValueError("Codex did not complete the compatibility review.")
|
|
review = json.loads(result.final_response)
|
|
validate_review(review)
|
|
return review
|
|
|
|
|
|
def run_review(tag: str, expected_sha: str, output: Path) -> dict:
|
|
result = {
|
|
"schema_version": 1,
|
|
"advisory": True,
|
|
"status": "error",
|
|
"context": {},
|
|
"review": None,
|
|
"error": "",
|
|
"model": MODEL,
|
|
"reasoning_effort": "medium",
|
|
"codex_version": "",
|
|
"codex_sdk_version": "",
|
|
"workflow_sha": os.environ.get("GITHUB_WORKFLOW_SHA", ""),
|
|
"run_id": os.environ.get("GITHUB_RUN_ID", ""),
|
|
"run_attempt": os.environ.get("GITHUB_RUN_ATTEMPT", ""),
|
|
}
|
|
try:
|
|
result["context"] = resolve_candidate(tag, expected_sha)
|
|
if not os.environ.get("NVIDIA_INFERENCE_API_KEY"):
|
|
raise ValueError("NVIDIA inference credential is missing.")
|
|
result["codex_version"] = version("openai-codex-cli-bin")
|
|
result["codex_sdk_version"] = version("openai-codex")
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="openshell-compatibility-"
|
|
) as temporary:
|
|
result["review"] = asyncio.run(
|
|
invoke_codex(result["context"], Path(temporary))
|
|
)
|
|
result["status"] = (
|
|
"incomplete" if result["review"]["unreviewed_surfaces"] else "complete"
|
|
)
|
|
except TimeoutError:
|
|
result["error"] = "Compatibility review exceeded its time limit."
|
|
except subprocess.CalledProcessError as error:
|
|
result["error"] = (
|
|
f"Review tooling failed (exit {error.returncode}); no assessment is available."
|
|
)
|
|
except json.JSONDecodeError:
|
|
result["error"] = "Codex did not return a valid JSON report."
|
|
except ValueError as error:
|
|
result["error"] = str(error)
|
|
except OSError as error:
|
|
result["error"] = (
|
|
f"Review input, tool, or output unavailable ({type(error).__name__})."
|
|
)
|
|
write_report(output, result)
|
|
return result
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("candidate", help="Candidate or stable release tag.")
|
|
parser.add_argument(
|
|
"--source-sha", default="", help="Expected qualification commit."
|
|
)
|
|
parser.add_argument("--output-dir", required=True, type=Path)
|
|
args = parser.parse_args()
|
|
result = run_review(args.candidate, args.source_sha, args.output_dir)
|
|
verdict = result["review"]["verdict"] if result["review"] else "unavailable"
|
|
if output := os.environ.get("GITHUB_OUTPUT"):
|
|
with Path(output).open("a") as handle:
|
|
handle.write(f"status={result['status']}\nverdict={verdict}\n")
|
|
if result["status"] != "complete" or verdict != "no_unaddressed_breaks":
|
|
print(
|
|
f"::warning::Advisory compatibility review: {result['status']}, {verdict}. See report."
|
|
)
|
|
raise SystemExit(0 if result["status"] == "complete" else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|