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
12 changes: 7 additions & 5 deletions docker/Dockerfile.runner
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,28 @@ ARG HELM_SHA256=0093eb572e3d2380f094df162ddb525e219249de88957afe24cfbb19632acd36
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
aspell aspell-en build-essential ca-certificates curl git git-lfs unzip \
openjdk-17-jdk-headless \
libasound2 libatk-bridge2.0-0 libatk1.0-0 libcups2 libdbus-1-3 \
libgbm1 libgtk-3-0 libnspr4 libnss3 libxcomposite1 libxdamage1 \
libxfixes3 libxkbcommon0 libxrandr2 \
&& curl --fail --location --silent --show-error "$TEMURIN_8_URL" --output /tmp/temurin.tar.gz \
&& curl --fail --location --silent --show-error --retry 5 --retry-all-errors --retry-delay 2 "$TEMURIN_8_URL" --output /tmp/temurin.tar.gz \
&& echo "$TEMURIN_8_SHA256 /tmp/temurin.tar.gz" | sha256sum --check - \
&& curl --fail --location --silent --show-error \
"https://dlcdn.apache.org/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz" \
&& curl --fail --location --silent --show-error --retry 5 --retry-all-errors --retry-delay 2 \
"https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz" \
--output /tmp/maven.tar.gz \
&& echo "$MAVEN_SHA512 /tmp/maven.tar.gz" | sha512sum --check - \
&& curl --fail --location --silent --show-error \
&& curl --fail --location --silent --show-error --retry 5 --retry-all-errors --retry-delay 2 \
"https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOC_VERSION/protoc-$PROTOC_VERSION-linux-x86_64.zip" \
--output /tmp/protoc.zip \
&& echo "$PROTOC_SHA256 /tmp/protoc.zip" | sha256sum --check - \
&& curl --fail --location --silent --show-error \
&& curl --fail --location --silent --show-error --retry 5 --retry-all-errors --retry-delay 2 \
"https://get.helm.sh/helm-v$HELM_VERSION-linux-amd64.tar.gz" \
--output /tmp/helm.tar.gz \
&& echo "$HELM_SHA256 /tmp/helm.tar.gz" | sha256sum --check - \
&& mkdir -p /opt/java/temurin-8u502-b07 /opt/maven/apache-maven-$MAVEN_VERSION \
/opt/protobuf/protoc-$PROTOC_VERSION \
&& tar -xzf /tmp/temurin.tar.gz --strip-components=1 -C /opt/java/temurin-8u502-b07 \
&& ln -s /usr/lib/jvm/java-17-openjdk-amd64 /opt/java/temurin-17 \
&& tar -xzf /tmp/maven.tar.gz --strip-components=1 -C /opt/maven/apache-maven-$MAVEN_VERSION \
&& unzip -q /tmp/protoc.zip -d /opt/protobuf/protoc-$PROTOC_VERSION \
&& tar -xzf /tmp/helm.tar.gz -C /tmp \
Expand Down
50 changes: 35 additions & 15 deletions src/reposteward/github.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import hashlib
import http.client
import json
import os
import re
Expand Down Expand Up @@ -144,23 +145,42 @@ def _request(
}
if self.token:
headers["Authorization"] = f"Bearer {self.token}"
request = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
response = urllib.request.urlopen(request, timeout=30)
payload_bytes = response.read()
except urllib.error.HTTPError as exc:
error_body = exc.read().decode("utf-8", errors="replace")
message = error_body
# GET 请求在瞬时网络错误(截断响应、连接重置等)下重试;POST/PATCH 可能非幂等,不重试
attempts = 5 if method == "GET" else 1
last_error: Exception | None = None
for attempt in range(attempts):
request = urllib.request.Request(
url, data=body, headers=headers, method=method
)
try:
message = json.loads(error_body).get("message", error_body)
except json.JSONDecodeError:
pass
response = urllib.request.urlopen(request, timeout=30)
payload_bytes = response.read()
last_error = None
break
except urllib.error.HTTPError as exc:
error_body = exc.read().decode("utf-8", errors="replace")
message = error_body
try:
message = json.loads(error_body).get("message", error_body)
except json.JSONDecodeError:
pass
raise GitHubError(
f"GitHub {method} {path} failed ({exc.code}): {message}",
status_code=exc.code,
) from exc
except (
urllib.error.URLError,
http.client.IncompleteRead,
ConnectionResetError,
TimeoutError,
) as exc:
last_error = exc
if attempt + 1 < attempts:
time.sleep((attempt + 1) ** 2)
if last_error is not None:
raise GitHubError(
f"GitHub {method} {path} failed ({exc.code}): {message}",
status_code=exc.code,
) from exc
except urllib.error.URLError as exc:
raise GitHubError(f"GitHub {method} {path} failed: {exc.reason}") from exc
f"GitHub {method} {path} failed after retries: {last_error}"
) from last_error
if response.status not in set(expected):
raise GitHubError(
f"GitHub {method} {path} returned unexpected status {response.status}"
Expand Down
5 changes: 5 additions & 0 deletions src/reposteward/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,11 @@ def _run_container(
"PNPM_HOME=/reposteward-env/pnpm-home",
"-e",
"GRADLE_USER_HOME=/reposteward-env/gradle",
# 容器以 host uid 运行且镜像内无对应 passwd 条目时,部分 JDK 会把
# user.home 解析为 "?",导致 Maven 等工具把缓存写到工作区内随沙箱销毁。
# 固定 user.home 到持久卷,保证 bootstrap 下载的依赖在断网 verify 阶段可用。
"-e",
"JAVA_TOOL_OPTIONS=-Duser.home=/reposteward-env/home",
"-v",
f"{worktree.resolve()}:/workspace:rw",
"-v",
Expand Down
36 changes: 36 additions & 0 deletions tests/test_github.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import http.client
import io
import unittest
import urllib.error
Expand Down Expand Up @@ -968,6 +969,41 @@ def test_job_log_rejects_non_https_redirects(self) -> None:
):
client.workflow_job_log("owner/repo", 20, max_bytes=12)

def test_get_request_retries_transient_transport_errors(self) -> None:
class FakeResponse(io.BytesIO):
def __init__(self, payload: bytes) -> None:
super().__init__(payload)
self.status = 200

client = GitHubClient(GitHubConfig(), token="test-token")
responses = [
http.client.IncompleteRead(b'{"login": "pw', 5),
FakeResponse(b'{"login": "pwd11"}'),
]
with (
patch("urllib.request.urlopen", side_effect=responses) as urlopen_mock,
patch("time.sleep", return_value=None) as sleep_mock,
):
payload, _ = client._request("GET", "/user")

self.assertEqual(payload, {"login": "pwd11"})
self.assertEqual(urlopen_mock.call_count, 2)
sleep_mock.assert_called_once_with(1)

def test_post_request_does_not_retry_transient_transport_errors(self) -> None:
client = GitHubClient(GitHubConfig(), token="test-token")
with (
patch(
"urllib.request.urlopen",
side_effect=http.client.IncompleteRead(b"{}", 1),
),
patch("time.sleep") as sleep_mock,
self.assertRaisesRegex(GitHubError, "failed after retries"),
):
client._request("POST", "/issues", data={"title": "x"})

sleep_mock.assert_not_called()


if __name__ == "__main__":
unittest.main()
3 changes: 3 additions & 0 deletions tests/test_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,9 @@ def test_container_receives_shared_cache_and_read_only_git_mounts(self) -> None:
self.assertIn(f"{environment.resolve()}:/reposteward-env:rw", command)
self.assertIn(f"{git_dir.resolve()}:/reposteward-git:ro", command)
self.assertIn("none", command)
# JVM user.home 固定到持久卷,避免无 passwd 条目时解析为 "?" 导致缓存丢失
env_index = command.index("JAVA_TOOL_OPTIONS=-Duser.home=/reposteward-env/home")
self.assertEqual(command[env_index - 1], "-e")


if __name__ == "__main__":
Expand Down