feat(python/robomp): bootstrapped node_modules in bare worktrees

- Added `ensure_workspace_dependencies` to automate `bun install` for new worktrees where dependencies are missing.
- Configured the installer to use `--frozen-lockfile` and `--ignore-scripts` to preserve security and lockfile integrity.
- Integrated the bootstrap step into the worker startup process to ensure workspace packages are resolvable.
This commit is contained in:
can1357
2026-06-19 17:12:04 +02:00
parent 4bb7349779
commit 89e9ce8647
3 changed files with 162 additions and 0 deletions
+55
View File
@@ -43,6 +43,8 @@ from robomp.sandbox import (
log = logging.getLogger(__name__)
_PRE_PR_FIX_COMMAND = ("bun", "run", "fix")
_PRE_PR_CHECK_COMMAND = ("bun", "check")
_BUN_INSTALL_COMMAND = ("bun", "install", "--frozen-lockfile", "--ignore-scripts")
_BUN_INSTALL_TIMEOUT_SECONDS = 300.0
_REPO_COMMAND_SCRUBBED_ENV_KEYS: tuple[str, ...] = (
"GITHUB_TOKEN",
"GITHUB_WEBHOOK_SECRET",
@@ -284,6 +286,59 @@ def _format_process_output(stdout: Any, stderr: Any) -> str:
)
def ensure_workspace_dependencies(bindings: ToolBindings) -> None:
"""Bootstrap ``node_modules`` so the agent can resolve workspace packages.
A per-issue worktree is a bare source checkout (``git worktree add`` off
the shared clone pool): it has the repo's ``package.json``/``bun.lock`` but
no ``node_modules``. With bun's ``hoisted`` linker the workspace links
(``@oh-my-pi/pi-*``) only exist after an install, so without one any
``bun test``/``bun check`` the agent runs fails instantly with "Cannot find
package" — the agent then reports it could not verify. We install before
the agent starts, mirroring how the natives cache pre-populates ``.node``
artifacts. The links resolve into *this* worktree's ``packages/*`` (not the
orchestrator's read-only ``/work/pi``), so tests exercise the PR's edited
source.
``--frozen-lockfile`` keeps the lockfile pristine (no spurious diff for the
agent to commit) and ``--ignore-scripts`` skips lifecycle scripts so an
untrusted PR's ``postinstall``/``prepare`` cannot execute as the slot and
the cached native build is not redone. Runs with the same scrubbed,
slot-owned env as the other repo-owned bun commands (``bun run fix`` /
``bun check``).
Skips non-bun repos. Otherwise runs unconditionally on every launch
(including ``--continue`` resumes): a frozen install verifies an intact
tree in ~20ms and re-links anything missing, so a previous install that
timed out or crashed half-way self-heals instead of being skipped forever
on a mere ``node_modules/`` directory existing. Best-effort: any failure
(offline, or a PR that bumped deps so the frozen lockfile is stale) is
logged and swallowed — the agent can still install itself or report the gap.
"""
repo_dir = bindings.workspace.repo_dir
if not (repo_dir / "package.json").is_file() or not (repo_dir / "bun.lock").is_file():
return
try:
proc = _run_repo_command(bindings, _BUN_INSTALL_COMMAND, timeout=_BUN_INSTALL_TIMEOUT_SECONDS)
except FileNotFoundError:
log.warning("bun_install bootstrap skipped: bun not on PATH", extra={"issue": bindings.issue_key})
return
except (OSError, subprocess.SubprocessError) as exc:
log.warning("bun_install bootstrap failed", extra={"issue": bindings.issue_key, "err": str(exc)})
return
if proc.returncode != 0:
log.warning(
"bun_install bootstrap nonzero exit",
extra={
"issue": bindings.issue_key,
"code": proc.returncode,
"output": _format_process_output(proc.stdout, proc.stderr),
},
)
return
log.info("bun_install bootstrap ok", extra={"issue": bindings.issue_key})
def _run_pre_publish_bun_fix(
bindings: ToolBindings,
args: Mapping[str, Any],
+3
View File
@@ -484,6 +484,9 @@ def _run_rpc_blocking(
rpc_env.update(_prepare_slot_runtime_env(inputs.workspace, inputs.slot_uid))
rpc_env.update(_safe_directory_env(bindings.workspace.repo_dir))
rpc_env.update(_git_identity_env(inputs.settings.resolved_author_name, inputs.settings.git_author_email))
# Bare worktrees have no node_modules; install (idempotently) so the agent
# can resolve workspace packages (@oh-my-pi/pi-*) and actually run tests.
host_tools.ensure_workspace_dependencies(bindings)
resuming = _has_prior_session(bindings.workspace.session_dir)
extra_args: tuple[str, ...] = ("--continue",) if resuming else ()
log.info(