diff --git a/.github/workflows/bedtools-immutable-canary.yml b/.github/workflows/bedtools-immutable-canary.yml index 6658684..6c4304e 100644 --- a/.github/workflows/bedtools-immutable-canary.yml +++ b/.github/workflows/bedtools-immutable-canary.yml @@ -47,12 +47,10 @@ jobs: PILOT_RUNNER_ARCH: ${{ runner.arch }} run: python3 -m scripts.pilot_runner --output canary-output --runner-only - - name: Checkout pinned tool runtime - uses: actions/checkout@v4 - with: - repository: OmniBioAI/omnibioai-tool-runtime - ref: a4f3da56c2e6e9fd85e54ba009f6f3456de3b5b5 - path: omnibioai-tool-runtime + - name: Resolve pinned tool runtime into build context + env: + TOOL_RUNTIME_COMMIT: 34a80219335f5dbeedeb7f0e6a4439913d12e25f + run: python3 scripts/runtime_resolver.py --stage omnibioai-tool-runtime - uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/pilot-immutable-multiarch-release.yml b/.github/workflows/pilot-immutable-multiarch-release.yml index c65c56c..c961f86 100644 --- a/.github/workflows/pilot-immutable-multiarch-release.yml +++ b/.github/workflows/pilot-immutable-multiarch-release.yml @@ -87,12 +87,10 @@ jobs: PILOT_RUNNER_ARCH: ${{ runner.arch }} run: python3 -m scripts.pilot_runner --output release-output --runner-only - - name: Checkout pinned tool runtime - uses: actions/checkout@v4 - with: - repository: OmniBioAI/omnibioai-tool-runtime - ref: a4f3da56c2e6e9fd85e54ba009f6f3456de3b5b5 - path: omnibioai-tool-runtime + - name: Resolve pinned tool runtime into build context + env: + TOOL_RUNTIME_COMMIT: 34a80219335f5dbeedeb7f0e6a4439913d12e25f + run: python3 scripts/runtime_resolver.py --stage omnibioai-tool-runtime - uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/pilot-multiarch-sifs.yml b/.github/workflows/pilot-multiarch-sifs.yml index 4b29b83..a5f0ef1 100644 --- a/.github/workflows/pilot-multiarch-sifs.yml +++ b/.github/workflows/pilot-multiarch-sifs.yml @@ -62,12 +62,10 @@ jobs: PILOT_RUNNER_OS: ${{ runner.os }} PILOT_RUNNER_ARCH: ${{ runner.arch }} run: python3 -m scripts.pilot_runner --output pilot-output --runner-only - - name: Checkout pinned tool runtime - uses: actions/checkout@v4 - with: - repository: OmniBioAI/omnibioai-tool-runtime - ref: a4f3da56c2e6e9fd85e54ba009f6f3456de3b5b5 - path: omnibioai-tool-runtime + - name: Resolve pinned tool runtime into build context + env: + TOOL_RUNTIME_COMMIT: 34a80219335f5dbeedeb7f0e6a4439913d12e25f + run: python3 scripts/runtime_resolver.py --stage omnibioai-tool-runtime - uses: docker/setup-buildx-action@v3 - name: Install native Apptainer run: | diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e9b7991..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "omnibioai-tool-runtime"] - path = omnibioai-tool-runtime - url = https://github.com/OmniBioAI/omnibioai-tool-runtime.git diff --git a/README.md b/README.md index 8b58384..9aa7ee1 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,21 @@ pytest tests/ -v -k "not test_tool_runs_in_sif" --- +## Tool Runtime dependency + +Tool image builds use the canonical `omnibioai-tool-runtime` repository as an external, commit-pinned build input. The resolver in `scripts/runtime_resolver.py` accepts `TOOL_RUNTIME_DIR` for an existing clean checkout, uses the sibling ecosystem checkout when available, and otherwise stages a temporary checkout at the approved `TOOL_RUNTIME_COMMIT`. It validates the exact commit and fails closed for missing, dirty, or mismatched inputs. Dockerfiles retain their existing `omnibioai-tool-runtime/` copy paths. + +The historical nested runtime Gitlink was removed from active metadata. Historical references to its former SHA remain in audit records. The current approved runtime SHA is `34a80219335f5dbeedeb7f0e6a4439913d12e25f`. + +## ORAS dependency + +Historical vendored ORAS 1.1.0 artifacts were removed because active CI and +release code do not consume them. Manual release operations require a verified +`oras` executable available through `PATH`. CI installs ORAS 1.3.0 from the +upstream release archive, verifies the architecture-specific SHA-256 checksum, +and then installs it for the job. Generated SIF images and build logs remain +ignored and external to Git. + ## Distribution Once SIFs are built, four scripts push them off this machine to a remote @@ -73,7 +88,7 @@ store. None of these are wired into the API — they're run manually, host-side: | Script | Target | Requires | |---|---|---| | `scripts/push_to_hf.sh` | Hugging Face dataset repo `omnibioai/omnibioai-sif-images` | `huggingface-cli`, logged in | -| `scripts/push_to_ghcr.sh` | `ghcr.io/omnibioai/omnibioai-sif/:arm64` via `oras` (vendored in `scripts/`, auto-installed if missing) | `GH_USER` and `GH_TOKEN` env vars | +| `scripts/push_to_ghcr.sh` | `ghcr.io/omnibioai/omnibioai-sif/:arm64` via an `oras` executable on `PATH` | `GH_USER` and `GH_TOKEN` env vars | | `scripts/upload_sifs_s3.sh` | `s3://-/` (bucket auto-created) | AWS CLI credentials | | `scripts/upload_sifs_azure.sh` | Azure Blob container `${AZURE_SIF_CONTAINER:-omnibioai-sif}` (auto-created) | `AZURE_STORAGE_CONNECTION_STRING` | diff --git a/scripts/oras b/scripts/oras deleted file mode 100755 index aa1f139..0000000 Binary files a/scripts/oras and /dev/null differ diff --git a/scripts/oras_1.1.0_linux_arm64.tar.gz b/scripts/oras_1.1.0_linux_arm64.tar.gz deleted file mode 100644 index b7043c9..0000000 Binary files a/scripts/oras_1.1.0_linux_arm64.tar.gz and /dev/null differ diff --git a/scripts/runtime_resolver.py b/scripts/runtime_resolver.py new file mode 100644 index 0000000..8d4d36d --- /dev/null +++ b/scripts/runtime_resolver.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Resolve and stage the canonical OmniBioAI tool runtime for builds.""" +from __future__ import annotations +import argparse, os, shutil, subprocess, tempfile +from pathlib import Path + +DEFAULT_COMMIT = "34a80219335f5dbeedeb7f0e6a4439913d12e25f" +DEFAULT_REPOSITORY = "https://github.com/OmniBioAI/omnibioai-tool-runtime.git" + +class RuntimeResolutionError(RuntimeError): + pass + +def run(*args: str, cwd: Path | None = None) -> str: + p = subprocess.run(args, cwd=cwd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if p.returncode: + raise RuntimeResolutionError(f"command failed: {' '.join(args)}: {p.stderr.strip()}") + return p.stdout.strip() + +def sha(path: Path) -> str: + try: + return run("git", "-C", str(path), "rev-parse", "HEAD") + except RuntimeResolutionError as exc: + raise RuntimeResolutionError(f"runtime checkout is not a Git repository: {path}") from exc + +def validate(path: Path, expected: str) -> None: + if not path.is_dir(): + raise RuntimeResolutionError(f"runtime checkout is missing: {path}") + if run("git", "-C", str(path), "status", "--porcelain"): + raise RuntimeResolutionError(f"runtime checkout is dirty: {path}") + actual = sha(path) + if actual != expected: + raise RuntimeResolutionError(f"runtime SHA mismatch: expected {expected}, got {actual}") + +def resolve(repo: Path, expected: str) -> tuple[Path, tempfile.TemporaryDirectory[str] | None]: + override = os.environ.get("TOOL_RUNTIME_DIR") + candidates = [Path(override).expanduser()] if override else [repo.parent / "omnibioai-tool-runtime"] + for candidate in candidates: + if candidate.is_dir(): + validate(candidate, expected) + return candidate, None + td = tempfile.TemporaryDirectory(prefix="omnibioai-tool-runtime-") + target = Path(td.name) / "checkout" + commit = os.environ.get("TOOL_RUNTIME_COMMIT", expected) + run("git", "clone", "--no-checkout", DEFAULT_REPOSITORY, str(target)) + run("git", "-C", str(target), "checkout", "--detach", commit) + validate(target, expected) + return target, td + +def stage(repo: Path, destination: Path, expected: str) -> Path: + runtime, temporary = resolve(repo, expected) + try: + if destination.exists(): + raise RuntimeResolutionError(f"staging destination already exists: {destination}") + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(runtime, destination, ignore=shutil.ignore_patterns(".git")) + validate(destination, expected) if (destination / ".git").exists() else None + return destination + finally: + if temporary is not None: + temporary.cleanup() + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--repo", type=Path, default=Path(__file__).resolve().parents[1]) + ap.add_argument("--commit", default=os.environ.get("TOOL_RUNTIME_COMMIT", DEFAULT_COMMIT)) + ap.add_argument("--stage", type=Path, required=True) + args = ap.parse_args() + stage(args.repo.resolve(), args.stage.resolve(), args.commit) + print(args.commit) + return 0 + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/sif_release.py b/scripts/sif_release.py index 1b92fba..176c37b 100644 --- a/scripts/sif_release.py +++ b/scripts/sif_release.py @@ -40,7 +40,7 @@ ALLOWED_ARCHITECTURES = ("amd64", "arm64") ARTIFACT_TYPE = "application/vnd.omnibioai.sif.v1" -RUNTIME_COMMIT = "a4f3da56c2e6e9fd85e54ba009f6f3456de3b5b5" +RUNTIME_COMMIT = "34a80219335f5dbeedeb7f0e6a4439913d12e25f" SBOM_STATUSES = {"SBOM_OPTIONAL_NOT_GENERATED", "SBOM_PASS"} CANDIDATE_RECORD_SCHEMA_VERSION = "1" RELEASE_PLAN_SCHEMA_VERSION = "1" diff --git a/tests/test_bedtools_canary_oras_diagnostics.py b/tests/test_bedtools_canary_oras_diagnostics.py index 89e45c3..b4b1a2a 100644 --- a/tests/test_bedtools_canary_oras_diagnostics.py +++ b/tests/test_bedtools_canary_oras_diagnostics.py @@ -55,7 +55,7 @@ def _candidate_dict(**changes): "provenance_status": "PASS", "build_inputs": { "package_revision": "2.30.0+dfsg-3", - "runtime_commit": "a4f3da56c2e6e9fd85e54ba009f6f3456de3b5b5", + "runtime_commit": "34a80219335f5dbeedeb7f0e6a4439913d12e25f", "source_checksum": "4" * 64, }, "operational_provenance": { diff --git a/tests/test_bedtools_canary_staging_containment.py b/tests/test_bedtools_canary_staging_containment.py index f51a82a..64c5478 100644 --- a/tests/test_bedtools_canary_staging_containment.py +++ b/tests/test_bedtools_canary_staging_containment.py @@ -49,7 +49,7 @@ def candidate(**changes): "provenance_status": "PASS", "build_inputs": { "package_revision": "2.30.0+dfsg-3", - "runtime_commit": "a4f3da56c2e6e9fd85e54ba009f6f3456de3b5b5", + "runtime_commit": "34a80219335f5dbeedeb7f0e6a4439913d12e25f", "source_checksum": "4" * 64, }, "operational_provenance": { diff --git a/tests/test_contract_native_validation.py b/tests/test_contract_native_validation.py index 448cc92..eab1ee8 100644 --- a/tests/test_contract_native_validation.py +++ b/tests/test_contract_native_validation.py @@ -541,9 +541,7 @@ def test_reconciliation_failure_summary_is_durable(plan, tmp_path): assert result["release_complete"] is False and result["publication_authorized"] is False -def test_checkout_gitlink_has_registered_repository(): - configuration = configparser.ConfigParser() - configuration.read(REPO / ".gitmodules") - runtime = configuration['submodule "omnibioai-tool-runtime"'] - assert runtime["path"] == "omnibioai-tool-runtime" - assert runtime["url"] == "https://github.com/OmniBioAI/omnibioai-tool-runtime.git" +def test_runtime_is_resolved_externally_at_canonical_sha(): + from scripts.runtime_resolver import DEFAULT_COMMIT + assert not (REPO / ".gitmodules").exists() + assert DEFAULT_COMMIT == "34a80219335f5dbeedeb7f0e6a4439913d12e25f" diff --git a/tests/test_runtime_resolver.py b/tests/test_runtime_resolver.py new file mode 100644 index 0000000..d2ba110 --- /dev/null +++ b/tests/test_runtime_resolver.py @@ -0,0 +1,37 @@ +from pathlib import Path + +import pytest + +from scripts import runtime_resolver as resolver + +EXPECTED = "34a80219335f5dbeedeb7f0e6a4439913d12e25f" +RUNTIME = Path(__file__).resolve().parents[2] / "omnibioai-tool-runtime" + + +def test_sibling_runtime_resolves_and_validates(monkeypatch): + monkeypatch.delenv("TOOL_RUNTIME_DIR", raising=False) + resolved, temporary = resolver.resolve(Path(__file__).resolve().parents[1], EXPECTED) + assert resolved == RUNTIME + assert temporary is None + + +def test_wrong_sha_fails_closed(): + with pytest.raises(resolver.RuntimeResolutionError, match="SHA mismatch"): + resolver.validate(RUNTIME, "0" * 40) + + +def test_missing_runtime_fails_closed_without_network(monkeypatch, tmp_path): + monkeypatch.setenv("TOOL_RUNTIME_DIR", str(tmp_path / "missing")) + def fail_clone(*args, **kwargs): + raise resolver.RuntimeResolutionError("network disabled in test") + monkeypatch.setattr(resolver, "run", fail_clone) + with pytest.raises(resolver.RuntimeResolutionError): + resolver.resolve(Path(__file__).resolve().parents[1], EXPECTED) + + +def test_staging_excludes_git_metadata(monkeypatch, tmp_path): + monkeypatch.setenv("TOOL_RUNTIME_DIR", str(RUNTIME)) + destination = tmp_path / "omnibioai-tool-runtime" + resolver.stage(Path(__file__).resolve().parents[1], destination, EXPECTED) + assert (destination / "pyproject.toml").is_file() + assert not (destination / ".git").exists() diff --git a/tests/test_sif_identity.py b/tests/test_sif_identity.py index 98f186e..08a1088 100644 --- a/tests/test_sif_identity.py +++ b/tests/test_sif_identity.py @@ -42,7 +42,7 @@ def candidate(**changes): "provenance_status": "PASS", "build_inputs": { "package_revision": "2.30.0+dfsg-3", - "runtime_commit": "a4f3da56c2e6e9fd85e54ba009f6f3456de3b5b5", + "runtime_commit": "34a80219335f5dbeedeb7f0e6a4439913d12e25f", "source_checksum": "4" * 64, }, "operational_provenance": { diff --git a/tests/test_sif_release_factory.py b/tests/test_sif_release_factory.py index ee537e5..2a8d49e 100644 --- a/tests/test_sif_release_factory.py +++ b/tests/test_sif_release_factory.py @@ -535,7 +535,7 @@ def test_bedtools_reference_build_identity_is_unchanged_by_canonicalization(): value = candidate(tool="bedtools", version="2.30.0") assert release.canonicalize_version_evidence(value["version_evidence"]) == "bedtools v2.30.0" assert identity.build_identity_sha256(value) == ( - "5bfd3c83d10a2e68224f24ec2747545299b6c3b45bdac9b892ab21df201342fc" + "a8884fd73110f18fe4fa80f1f7a2df346d459937d631f1488c55aced7c6951c0" )