Add Hypeman Harbor environment backend - #1
Merged
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
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
.dockerignorepatterns when collecting files, preventing ignored paths from being included in Hypeman build contexts.
- Build archive creation now applies
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.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.


summary
kernel-mcp-serverHarbor tasks with deterministic hidden Bun tests and Braintrust run instructionsvalidation
uv run ruff format --check .uv run ruff check .uv run ty checkuv run pytest(13 passed)uv buildharbor-hypeman[braintrust]actionlint .github/workflows/ci.yml