Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ Format: [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), [Semantic Vers

## [Unreleased]

### Behoben / Fixed

- Windows-Konsolenstarts für GitHub-Abfragen, Credential-Lookup, Git-Push und
SSH-Reviewbelege verwenden `CREATE_NO_WINDOW`, auch unter einem konsolenlosen
Elternprozess. Testhelfer schützen ihre eigenen Kindstarts ebenso. Auf POSIX
bleibt das Verhalten unverändert. / Hide console children at every explicit
CLI boundary, including test helpers; POSIX behavior is unchanged.

### Hinzugefügt / Added

- Getypte Schreibaktionen `pr.merge`, `pr.ready`, `actions.dispatch`, `actions.rerun`, `pr.create`
Expand Down
1 change: 1 addition & 0 deletions src/githubbot_core/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def token(self) -> Optional[str]:
encoding="utf-8",
errors="replace",
timeout=15,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
except (OSError, subprocess.TimeoutExpired):
return None
Expand Down
10 changes: 9 additions & 1 deletion src/githubbot_core/multibot_probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@


def query(args, *, run=subprocess.run):
result = run(["gh", *args], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=90)
result = run(
["gh", *args],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=90,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
if result.returncode:
# Do not forward arbitrary CLI stderr (may contain credential configuration).
return {"ok": False, "exit_code": result.returncode, "error": "GitHub-Abfrage fehlgeschlagen"}
Expand Down
3 changes: 3 additions & 0 deletions src/githubbot_core/review_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ def verify_ssh_signature(
input=receipt_bytes,
capture_output=True,
timeout=20,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
if result.returncode != 0:
detail = result.stderr.decode("utf-8", errors="replace").strip()
Expand Down Expand Up @@ -395,6 +396,7 @@ def sign_receipt(receipt_path: Path, key_path: Path, output_path: Path, namespac
encoding="utf-8",
errors="replace",
timeout=20,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
if result.returncode != 0:
raise ReceiptValidationError(f"receipt signing failed: {result.stderr.strip() or result.stdout.strip()}")
Expand Down Expand Up @@ -542,6 +544,7 @@ def _resolve_token() -> str:
encoding="utf-8",
errors="replace",
timeout=15,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
if result.returncode != 0 or not result.stdout.strip():
raise ReceiptValidationError("no GitHub token available")
Expand Down
1 change: 1 addition & 0 deletions src/githubbot_core/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def _invoke(self, args: list[str], stdin: Optional[str] = None) -> tuple[str, Op
encoding="utf-8",
errors="replace",
timeout=self.timeout,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
except subprocess.TimeoutExpired:
return "", "Timeout"
Expand Down
9 changes: 8 additions & 1 deletion src/githubbot_core/typed_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,14 @@ def exec_create_pr(transport, params: dict, config: dict):


def _run_git(path: str, *args: str) -> tuple[str, Optional[str]]:
result = _git(["git", "-C", path, *args], capture_output=True, text=True, encoding="utf-8", errors="replace")
result = _git(
["git", "-C", path, *args],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
if result.returncode:
return "", " ".join((result.stderr or result.stdout or "").split())[:500] or f"git exit {result.returncode}"
return result.stdout.strip(), None
Expand Down
11 changes: 10 additions & 1 deletion tests/test_consumer_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@
def _run(cmd, cwd, **kwargs):
env = {**os.environ, "PYTHONIOENCODING": "utf-8"}
env.pop("PYTHONPATH", None)
return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, encoding="utf-8", env=env, **kwargs)
return subprocess.run(
cmd,
cwd=cwd,
capture_output=True,
text=True,
encoding="utf-8",
env=env,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
**kwargs,
)


@pytest.mark.parametrize("artifact_variable", ["GITHUBBOT_CORE_TEST_WHEEL", "GITHUBBOT_CORE_TEST_SDIST"])
Expand Down
2 changes: 2 additions & 0 deletions tests/test_multibot_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def test_success_and_failure_wait_for_key_then_keep_exitcode(tmp_path, code):
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
try:
time.sleep(1)
Expand Down Expand Up @@ -65,5 +66,6 @@ def test_explicit_dry_or_no_pause_skips_wait(tmp_path, opt):
text=True,
encoding="utf-8",
timeout=10,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
assert result.returncode == 23 and "[TEST]" in result.stdout and not result.stderr
8 changes: 7 additions & 1 deletion tests/test_review_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,14 @@ def test_real_ssh_signature_is_bound_to_principal_and_receipt(tmp_path: Path) ->
subprocess.run(
["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key)],
check=True,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
receipt_path = tmp_path / "receipt.json"
receipt_path.write_bytes(canonical_receipt_bytes(_receipt()))
subprocess.run(
["ssh-keygen", "-Y", "sign", "-f", str(key), "-n", "githubbot-review-gate", str(receipt_path)],
check=True,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
allowed = tmp_path / "allowed_signers"
public_key = key.with_suffix(".pub").read_text(encoding="utf-8").strip()
Expand Down Expand Up @@ -335,7 +337,11 @@ def test_cli_create_writes_a_canonical_receipt(tmp_path: Path) -> None:
@pytest.mark.skipif(shutil.which("ssh-keygen") is None, reason="OpenSSH ssh-keygen is unavailable")
def test_cli_sign_writes_only_the_requested_signature(tmp_path: Path) -> None:
key = tmp_path / "reviewer"
subprocess.run(["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key)], check=True)
subprocess.run(
["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key)],
check=True,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
receipt = tmp_path / "receipt.json"
current = datetime.now(timezone.utc).replace(microsecond=0)
current_receipt = _receipt(
Expand Down
64 changes: 64 additions & 0 deletions tests/test_windowless_processes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""All explicit console starts stay hidden, including CLI and signing adapters."""

from datetime import datetime, timedelta, timezone
from pathlib import Path
import subprocess
from types import SimpleNamespace

import pytest

from githubbot_core import credentials, multibot_probe, review_gate, transport, typed_actions


@pytest.mark.parametrize("windows_flag", [None, 0x08000000], ids=["posix", "windows"])
def test_every_console_boundary_preserves_windowless_flags(monkeypatch, tmp_path, windows_flag):
calls = []

def run(command, **kwargs):
calls.append((command, kwargs))
if "-Y" in command and "sign" in command:
Path(command[-1] + ".sig").write_bytes(b"synthetic-signature")
text = kwargs.get("text", False)
return subprocess.CompletedProcess(command, 0, "{}" if text else b"", "" if text else b"")

adapter = SimpleNamespace(run=run, TimeoutExpired=subprocess.TimeoutExpired)
if windows_flag is not None:
adapter.CREATE_NO_WINDOW = windows_flag
for module in (credentials, multibot_probe, review_gate, transport, typed_actions):
monkeypatch.setattr(module, "subprocess", adapter)
monkeypatch.setattr(typed_actions, "_git", run)
monkeypatch.setattr(review_gate.shutil, "which", lambda _: "ssh-keygen")
monkeypatch.delenv("GH_TOKEN", raising=False)
monkeypatch.delenv("GITHUB_TOKEN", raising=False)

assert transport.GhCliTransport(runner=run).get("repos/example/repo") == ({}, None)
assert credentials.GhCliTokenProvider(runner=run).token() == "{}"
assert multibot_probe.query(["api", "user"], run=run)["ok"] is True
assert typed_actions._run_git(str(tmp_path), "status") == ("{}", None)
review_gate.verify_ssh_signature(b"{}", b"synthetic-signature", b"synthetic-signers", principal="reviewer")
assert review_gate._resolve_token() == "{}"

now = datetime.now(timezone.utc)
receipt = review_gate.create_receipt(
kind="model_review",
repository="example/repo",
pr_number=1,
head_sha="a" * 40,
verdict="approve",
reviewer="synthetic-reviewer",
artifact_sha256="b" * 64,
findings={"critical": 0, "high": 0, "medium": 0, "low": 0},
checks=["synthetic-only"],
issued_at=(now - timedelta(minutes=1)).strftime("%Y-%m-%dT%H:%M:%SZ"),
valid_until=(now + timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"),
nonce="windowless-synthetic-0001",
)
receipt_path, key_path, output_path = (tmp_path / name for name in ("receipt.json", "key", "signature"))
receipt_path.write_bytes(review_gate.canonical_receipt_bytes(receipt))
key_path.write_bytes(b"synthetic-key")
review_gate.sign_receipt(receipt_path, key_path, output_path)
assert output_path.read_bytes() == b"synthetic-signature"

assert len(calls) == 7
assert all(kwargs["creationflags"] == (windows_flag or 0) for _, kwargs in calls)
assert all(kwargs["capture_output"] is True for _, kwargs in calls)
Loading