From 0ae6d757dc4a37cd815dc50ce5db19dbf0ac69a0 Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Sun, 3 May 2026 19:01:35 +0800 Subject: [PATCH] refactor(scripts): extract shared utilities into common module - Move load_env_file, parse_timeout_seconds, build_remote_script, and build_ssh_command to scripts/common.py - Update remote_docker_run.py to import from common module - Improves code organization and reusability --- .justfile | 18 +----- .opencode/opencode.json | 11 +++- .safety-net.json | 8 ++- scripts/common.py | 110 +++++++++++++++++++++++++++++++++++ scripts/remote_docker_run.py | 99 +++---------------------------- 5 files changed, 136 insertions(+), 110 deletions(-) diff --git a/.justfile b/.justfile index fe58d9b..065eef3 100644 --- a/.justfile +++ b/.justfile @@ -10,18 +10,10 @@ rsync_flags := "-avLh --progress --stats --itemize-changes" upload_excludes := "--exclude-from=.rsyncignore" upload: - if command -v cygpath >/dev/null 2>&1 || test -n "${MSYSTEM:-}" || test -n "${CYGWIN:-}"; then \ - env MSYS2_ARG_CONV_EXCL='*' rsync {{ rsync_flags }} {{ upload_excludes }} . {{ remote_root }}/; \ - else \ - rsync {{ rsync_flags }} {{ upload_excludes }} . {{ remote_root }}/; \ - fi + rsync {{ rsync_flags }} {{ upload_excludes }} . {{ remote_root }}/; \ download: - if command -v cygpath >/dev/null 2>&1 || test -n "${MSYSTEM:-}" || test -n "${CYGWIN:-}"; then \ - env MSYS2_ARG_CONV_EXCL='*' rsync {{ rsync_flags }} {{ remote_root }}/outputs .; \ - else \ - rsync {{ rsync_flags }} {{ remote_root }}/outputs .; \ - fi + rsync {{ rsync_flags }} {{ remote_root }}/outputs .; \ sync-pkgs: uv sync --inexact @@ -61,9 +53,3 @@ remote-check: remote-dry cmd: uv run python scripts/remote_docker_run.py run --dry-run -- {{ quote(cmd) }} - -remote-test: - uv run python scripts/remote_docker_run.py run -- "uv run pytest -q" - -remote-test-one path: - uv run python scripts/remote_docker_run.py run -- "uv run pytest -q {{ path }}" diff --git a/.opencode/opencode.json b/.opencode/opencode.json index e1f1a1f..9511065 100644 --- a/.opencode/opencode.json +++ b/.opencode/opencode.json @@ -2,8 +2,17 @@ "$schema": "https://opencode.ai/config.json", "permission": { "bash": { + "python": "deny", + "python *": "deny", "make": "deny", - "make *": "deny" + "make *": "deny", + "just remote *": "ask" + }, + "read": { + ".env": "deny" + }, + "edit": { + ".env": "deny" } } } diff --git a/.safety-net.json b/.safety-net.json index 283724b..9b14779 100644 --- a/.safety-net.json +++ b/.safety-net.json @@ -18,7 +18,13 @@ "-h", "-v" ], - "reason": "uv run is blocked in this project. Use direct command execution instead." + "reason": "Local Environment is not avaiable. Please use \"just remote \" to run commands in the remote docker. And remember to run \"just upload\" before run commands in the remote docker." + }, + { + "name": "block-uv-sync", + "command": "uv", + "block_args": ["sync"], + "reason": "The project is managed by micromamba and uv. Do not sync packages directly by uv. Use \"just sync-pkgs\" or \"uv sync --inexact\" instead." } ] } diff --git a/scripts/common.py b/scripts/common.py index e69de29..8c90bd5 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Shared utilities for remote Docker / SSH scripts.""" + +from __future__ import annotations + +import os +import shlex +from pathlib import Path + + +def load_env_file(path: Path) -> None: + if not path.exists(): + return + + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + + if not line or line.startswith("#") or "=" not in line: + continue + + key, value = line.split("=", 1) + key = key.strip() + value = value.strip() + + if not key or key in os.environ: + continue + + if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}: + value = value[1:-1] + + os.environ[key] = value + + +def parse_timeout_seconds(value: str) -> float: + value = value.strip().lower() + + if value.endswith("ms"): + return float(value[:-2]) / 1000 + if value.endswith("s"): + return float(value[:-1]) + if value.endswith("m"): + return float(value[:-1]) * 60 + if value.endswith("h"): + return float(value[:-1]) * 3600 + + return float(value) + + +def build_remote_script(container: str, workdir: str, command: str) -> str: + q_container = shlex.quote(container) + q_workdir = shlex.quote(workdir) + + inner_command = ( + "set -Eeuo pipefail; " + 'if [ -f "$HOME/.bashrc" ]; then source "$HOME/.bashrc"; fi; ' + "if command -v direnv >/dev/null 2>&1 && [ -f .envrc ]; then " + " direnv allow . >/dev/null 2>&1 || true; " + ' eval "$(direnv export bash)"; ' + "fi; " + f"{command}" + ) + q_command = shlex.quote(inner_command) + + return f""" +set -Eeuo pipefail + +if ! command -v docker >/dev/null 2>&1; then + echo 'error: docker not found on remote host' >&2 + exit 127 +fi + +if ! docker inspect {q_container} >/dev/null 2>&1; then + echo 'error: docker container not found: {container}' >&2 + exit 127 +fi + +if [ "$(docker inspect -f '{{{{.State.Running}}}}' {q_container})" != "true" ]; then + echo 'error: docker container is not running: {container}' >&2 + exit 127 +fi + +docker exec -i \\ + -w {q_workdir} \\ + {q_container} \\ + bash -lc {q_command} +""".strip() + + +def build_ssh_command( + *, + ssh_target: str, + ssh_port: str, + ssh_opts: str, +) -> list[str]: + ssh_cmd = [ + "ssh", + "-p", + ssh_port, + "-o", + "BatchMode=yes", + "-o", + "StrictHostKeyChecking=accept-new", + ] + + if ssh_opts: + ssh_cmd.extend(shlex.split(ssh_opts)) + + ssh_cmd.extend([ssh_target, "bash", "-s", "--"]) + + return ssh_cmd diff --git a/scripts/remote_docker_run.py b/scripts/remote_docker_run.py index 48e9dac..bb36a6e 100644 --- a/scripts/remote_docker_run.py +++ b/scripts/remote_docker_run.py @@ -8,35 +8,19 @@ from pathlib import Path import typer +from common import ( + build_remote_script, + build_ssh_command, + load_env_file, + parse_timeout_seconds, +) + app = typer.Typer( help="Run commands inside a Docker container on a remote host over SSH.", no_args_is_help=True, ) -def load_env_file(path: Path) -> None: - if not path.exists(): - return - - for raw_line in path.read_text(encoding="utf-8").splitlines(): - line = raw_line.strip() - - if not line or line.startswith("#") or "=" not in line: - continue - - key, value = line.split("=", 1) - key = key.strip() - value = value.strip() - - if not key or key in os.environ: - continue - - if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}: - value = value[1:-1] - - os.environ[key] = value - - def required_env(name: str) -> str: value = os.environ.get(name) if not value: @@ -49,75 +33,6 @@ def required_env(name: str) -> str: return value -def parse_timeout_seconds(value: str) -> float: - value = value.strip().lower() - - if value.endswith("ms"): - return float(value[:-2]) / 1000 - if value.endswith("s"): - return float(value[:-1]) - if value.endswith("m"): - return float(value[:-1]) * 60 - if value.endswith("h"): - return float(value[:-1]) * 3600 - - return float(value) - - -def build_remote_script(container: str, workdir: str, command: str) -> str: - q_container = shlex.quote(container) - q_workdir = shlex.quote(workdir) - q_command = shlex.quote(command) - - return f""" -set -Eeuo pipefail - -if ! command -v docker >/dev/null 2>&1; then - echo 'error: docker not found on remote host' >&2 - exit 127 -fi - -if ! docker inspect {q_container} >/dev/null 2>&1; then - echo 'error: docker container not found: {container}' >&2 - exit 127 -fi - -if [ "$(docker inspect -f '{{{{.State.Running}}}}' {q_container})" != "true" ]; then - echo 'error: docker container is not running: {container}' >&2 - exit 127 -fi - -docker exec -i \\ - -w {q_workdir} \\ - {q_container} \\ - bash -lc {q_command} -""".strip() - - -def build_ssh_command( - *, - ssh_target: str, - ssh_port: str, - ssh_opts: str, -) -> list[str]: - ssh_cmd = [ - "ssh", - "-p", - ssh_port, - "-o", - "BatchMode=yes", - "-o", - "StrictHostKeyChecking=accept-new", - ] - - if ssh_opts: - ssh_cmd.extend(shlex.split(ssh_opts)) - - ssh_cmd.extend([ssh_target, "bash", "-s", "--"]) - - return ssh_cmd - - def build_user_command(command_parts: list[str]) -> str: if command_parts and command_parts[0] == "--": command_parts = command_parts[1:]