Skip to content

Add Hypeman Harbor environment backend - #1

Merged
rgarcia merged 3 commits into
mainfrom
hypeship/add-hypeman-backend
Aug 17, 2026
Merged

Add Hypeman Harbor environment backend#1
rgarcia merged 3 commits into
mainfrom
hypeship/add-hypeman-backend

Conversation

@rgarcia

@rgarcia rgarcia commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

summary

  • add a third-party Harbor environment backed by the Hypeman Python SDK
  • support Dockerfile builds, prebuilt images, resource limits, static network policy, command execution, and file transfer
  • add focused unit tests and package/CI configuration
  • add two pinned kernel-mcp-server Harbor tasks with deterministic hidden Bun tests and Braintrust run instructions

validation

  • uv run ruff format --check .
  • uv run ruff check .
  • uv run ty check
  • uv run pytest (13 passed)
  • uv build
  • clean-wheel import with harbor-hypeman[braintrust]
  • live Dockerfile build, no-network instance, exec, upload, and download
  • full Harbor Oracle trial through Hypeman (reward 1.0, no exceptions) with successful official Braintrust sync
  • both evaluation tests fail at their pinned baselines and pass against the corresponding completed implementations
  • actionlint .github/workflows/ci.yml

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Stale build cache after force-build
    • Cached build selection now sorts ready builds by creation time (newest first) before validating image existence, so subsequent runs reuse the latest forced build.
  • ✅ Fixed: Build context skips dockerignore rules
    • Build archive creation now applies .dockerignore patterns when collecting files, preventing ignored paths from being included in Hypeman build contexts.

Create PR

Or push these changes by commenting:

@cursor push 44ababfdfe
Preview (44ababfdfe)
diff --git a/src/harbor_hypeman/environment.py b/src/harbor_hypeman/environment.py
--- a/src/harbor_hypeman/environment.py
+++ b/src/harbor_hypeman/environment.py
@@ -8,6 +8,7 @@
 import shutil
 import tarfile
 import tempfile
+from datetime import UTC, datetime
 from pathlib import Path, PurePosixPath
 from typing import Any, override
 
@@ -34,6 +35,7 @@
     cp_to_instance_async,
     exec_async,
 )
+from pathspec import PathSpec
 
 _BUILD_TAG = "harbor.environment_id"
 _TERMINAL_BUILD_STATES = frozenset({"failed", "cancelled"})
@@ -181,9 +183,19 @@
 
     async def _cached_build_image(self) -> str | None:
         builds = await self._client.builds.list(tags={_BUILD_TAG: self.environment_id})
-        for build in builds:
-            if build.status != "ready" or build.image_ref is None:
-                continue
+        ready_builds = sorted(
+            (
+                (index, build)
+                for index, build in enumerate(builds)
+                if build.status == "ready" and build.image_ref is not None
+            ),
+            key=lambda item: (
+                self._normalize_build_created_at(getattr(item[1], "created_at", None)),
+                item[0],
+            ),
+            reverse=True,
+        )
+        for _, build in ready_builds:
             try:
                 await self._client.images.get(build.image_ref)
             except NotFoundError:
@@ -191,6 +203,14 @@
             return build.image_ref
         return None
 
+    @staticmethod
+    def _normalize_build_created_at(created_at: Any) -> datetime:
+        if not isinstance(created_at, datetime):
+            return datetime.min.replace(tzinfo=UTC)
+        if created_at.tzinfo is None:
+            return created_at.replace(tzinfo=UTC)
+        return created_at.astimezone(UTC)
+
     async def _build_image(self) -> str:
         with tempfile.TemporaryDirectory() as temp_dir:
             archive_path = Path(temp_dir) / "environment.tar.gz"
@@ -232,11 +252,28 @@
 
     @staticmethod
     def _write_build_archive(source_dir: Path, archive_path: Path) -> None:
+        dockerignore = source_dir / ".dockerignore"
+        dockerignore_spec: PathSpec | None = None
+        if dockerignore.is_file():
+            dockerignore_spec = PathSpec.from_lines(
+                "gitignore", dockerignore.read_text().splitlines()
+            )
+
         with tarfile.open(archive_path, "w:gz") as archive:
             for path in sorted(source_dir.rglob("*")):
                 relative = path.relative_to(source_dir)
                 if {".git", "__pycache__"} & set(relative.parts):
                     continue
+                relative_posix = relative.as_posix()
+                if (
+                    dockerignore_spec is not None
+                    and relative_posix not in {"Dockerfile", ".dockerignore"}
+                ):
+                    pattern_path = (
+                        f"{relative_posix}/" if path.is_dir() else relative_posix
+                    )
+                    if dockerignore_spec.match_file(pattern_path):
+                        continue
                 archive.add(path, arcname=relative.as_posix(), recursive=False)
 
     def _instance_name(self) -> str:

diff --git a/tests/test_environment.py b/tests/test_environment.py
--- a/tests/test_environment.py
+++ b/tests/test_environment.py
@@ -3,6 +3,7 @@
 import io
 import json
 import tarfile
+from datetime import UTC, datetime
 from pathlib import Path
 from types import SimpleNamespace
 from typing import Any, cast
@@ -241,6 +242,60 @@
     )
 
 
+async def test_start_reuses_newest_ready_build(tmp_path: Path) -> None:
+    client = _client()
+    client.builds.list.return_value = [
+        SimpleNamespace(
+            created_at=datetime(2026, 1, 1, tzinfo=UTC),
+            status="ready",
+            image_ref="registry.local/builds/oldest",
+        ),
+        SimpleNamespace(
+            created_at=datetime(2026, 1, 2, tzinfo=UTC),
+            status="ready",
+            image_ref="registry.local/builds/newest",
+        ),
+    ]
+    environment = _environment(
+        tmp_path,
+        task_config=EnvironmentConfig(),
+        client=client,
+        dockerfile="FROM alpine:3.22\n",
+    )
+
+    await environment.start(force_build=False)
+
+    client.images.get.assert_awaited_once_with("registry.local/builds/newest")
+    client.builds.create.assert_not_awaited()
+    assert client.instances.create.await_args.kwargs["image"] == (
+        "registry.local/builds/newest"
+    )
+
+
+async def test_write_build_archive_honors_dockerignore(tmp_path: Path) -> None:
+    source_dir = tmp_path / "environment"
+    source_dir.mkdir()
+    (source_dir / "Dockerfile").write_text("FROM alpine:3.22\n")
+    (source_dir / ".dockerignore").write_text("secret.txt\nignored_dir/\n")
+    (source_dir / "keep.txt").write_text("keep")
+    (source_dir / "secret.txt").write_text("secret")
+    (source_dir / "ignored_dir").mkdir()
+    (source_dir / "ignored_dir" / "ignored.txt").write_text("ignore")
+    archive_path = tmp_path / "environment.tar.gz"
+
+    HypemanEnvironment._write_build_archive(source_dir, archive_path)
+
+    with tarfile.open(archive_path, "r:gz") as archive:
+        archive_names = set(archive.getnames())
+
+    assert "Dockerfile" in archive_names
+    assert ".dockerignore" in archive_names
+    assert "keep.txt" in archive_names
+    assert "secret.txt" not in archive_names
+    assert "ignored_dir" not in archive_names
+    assert "ignored_dir/ignored.txt" not in archive_names
+
+
 async def test_exec_uses_shell_workdir_environment_and_user(
     tmp_path: Path, monkeypatch: pytest.MonkeyPatch
 ) -> None:

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 61a3a92. Configure here.

Comment thread src/harbor_hypeman/environment.py
Comment thread src/harbor_hypeman/environment.py Outdated
@rgarcia
rgarcia merged commit bf94a04 into main Aug 17, 2026
2 checks passed
@rgarcia
rgarcia deleted the hypeship/add-hypeman-backend branch August 17, 2026 22:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant