gowalk-cicd 1.0.75 → 1.0.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- 1.0.75
1
+ 1.0.76
@@ -155,21 +155,37 @@ def _request_timeouts() -> tuple[float, float]:
155
155
  )
156
156
 
157
157
 
158
+ def _network_error_category(exc: BaseException) -> str:
159
+ """Closed diagnostic labels only; exception text can contain private proxy URLs."""
160
+ for error_type, category in (
161
+ (requests.exceptions.ProxyError, "proxy"),
162
+ (requests.exceptions.SSLError, "tls"),
163
+ (requests.exceptions.ConnectTimeout, "connect_timeout"),
164
+ (requests.exceptions.ReadTimeout, "read_timeout"),
165
+ (requests.exceptions.Timeout, "timeout"),
166
+ (requests.exceptions.ConnectionError, "connection"),
167
+ ):
168
+ if isinstance(exc, error_type):
169
+ return category
170
+ return "request"
171
+
172
+
158
173
  def _retry_network_error(
159
174
  method: str, path: str, attempt: int, total: int,
160
175
  backoffs: tuple[int, ...], exc: BaseException,
161
176
  ) -> None:
162
177
  """Network error path: sleep and return when retries remain, else
163
178
  raise SystemExit (terminal). Caller ``continue``s after return."""
179
+ category = _network_error_category(exc)
164
180
  if attempt >= total - 1:
165
181
  raise SystemExit(
166
182
  f"ASC {method} {path} network error after {total} "
167
- "attempts through the configured proxy"
183
+ f"attempts through the configured proxy (category={category})"
168
184
  )
169
185
  delay = backoffs[attempt]
170
186
  print(
171
187
  f"ASC {method} {path} network error through the configured proxy; "
172
- f"retrying in {delay}s ({attempt + 1}/{total - 1})",
188
+ f"retrying in {delay}s ({attempt + 1}/{total - 1}; category={category})",
173
189
  file=sys.stderr,
174
190
  )
175
191
  time.sleep(delay)
@@ -0,0 +1,103 @@
1
+ """Real ASC helper and Requests preparation; only socket dispatch and sleeps are replaced."""
2
+ import contextlib
3
+ import io
4
+ import os
5
+ from pathlib import Path
6
+ import sys
7
+ import unittest
8
+ from unittest import mock
9
+
10
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
11
+ import asc_common
12
+
13
+ PRIVATE = "SYNTHETIC_PRIVATE_MARKER"
14
+ PROXY = f"http://account:{PRIVATE}@assigned.proxy.test:1234"
15
+ ENV = {"APPLE_STORE_PROXY_URL": PROXY, "NO_PROXY": "*", "no_proxy": "*",
16
+ "HTTPS_PROXY": "http://unrelated.proxy.test:9876", "REQUESTS_CA_BUNDLE": "",
17
+ "CURL_CA_BUNDLE": "", "ASC_REQUEST_TIMEOUT_CONNECT_SEC": "", "ASC_REQUEST_TIMEOUT_READ_SEC": ""}
18
+
19
+
20
+ class PrivateFailure(asc_common.requests.RequestException):
21
+ def __str__(self):
22
+ raise AssertionError("Exception text must never be inspected")
23
+
24
+
25
+ @mock.patch.dict(os.environ, ENV)
26
+ class NetworkDiagnosticTests(unittest.TestCase):
27
+ def test_terminal_categories_preserve_transport_and_privacy(self):
28
+ errors = asc_common.requests.exceptions
29
+ cases = [(errors.ProxyError, "proxy"), (errors.SSLError, "tls"),
30
+ (errors.ConnectTimeout, "connect_timeout"), (errors.ReadTimeout, "read_timeout"),
31
+ (errors.Timeout, "timeout"), (errors.ConnectionError, "connection"),
32
+ (errors.RequestException, "request"), (PrivateFailure, "request")]
33
+ for error_type, category in cases:
34
+ with self.subTest(category=category, error_type=error_type.__name__):
35
+ stderr, stdout = io.StringIO(), io.StringIO()
36
+ with mock.patch.object(asc_common.requests.Session, "send", autospec=True,
37
+ side_effect=error_type(f"{PRIVATE} {PROXY}")) as sender, \
38
+ mock.patch.object(asc_common.time, "sleep") as sleep, \
39
+ contextlib.redirect_stderr(stderr), contextlib.redirect_stdout(stdout):
40
+ with self.assertRaises(SystemExit) as caught:
41
+ asc_common.request("GET", "/builds", PRIVATE, params={"limit": "200"})
42
+ self.assertEqual(sender.call_count, 3)
43
+ self.assertEqual(sleep.call_args_list, [mock.call(2), mock.call(8)])
44
+ self.assertEqual(stderr.getvalue().count(f"category={category}"), 2)
45
+ self.assertIn(f"after 3 attempts through the configured proxy (category={category})",
46
+ str(caught.exception))
47
+ self.assertNotIn(PRIVATE, stdout.getvalue() + stderr.getvalue() + str(caught.exception))
48
+ self.assertEqual(stdout.getvalue(), "")
49
+ for call in sender.call_args_list:
50
+ self.assertEqual(call.args[1].url, asc_common.ASC_BASE + "/builds?limit=200")
51
+ self.assertEqual(call.args[1].headers["Authorization"], f"Bearer {PRIVATE}")
52
+ self.assertEqual(call.kwargs["proxies"]["https"], PROXY)
53
+ self.assertEqual(call.kwargs["proxies"]["http"], PROXY)
54
+ self.assertEqual(call.kwargs["timeout"], (10.0, 30.0))
55
+ self.assertIs(call.kwargs["verify"], True)
56
+
57
+ def test_transient_failure_returns_success_without_extra_sender_calls(self):
58
+ response = asc_common.requests.Response()
59
+ response.status_code = 200
60
+ stderr = io.StringIO()
61
+ with mock.patch.object(asc_common.requests.Session, "send", autospec=True,
62
+ side_effect=[asc_common.requests.exceptions.ProxyError(PRIVATE), response]) as sender, \
63
+ mock.patch.object(asc_common.time, "sleep") as sleep, contextlib.redirect_stderr(stderr):
64
+ self.assertIs(asc_common.request("GET", "/builds", PRIVATE), response)
65
+ self.assertEqual(sender.call_count, 2)
66
+ sleep.assert_called_once_with(2)
67
+ self.assertIn("category=proxy", stderr.getvalue())
68
+ self.assertNotIn(PRIVATE, stderr.getvalue())
69
+
70
+ def test_single_attempt_does_not_sleep(self):
71
+ with mock.patch.object(asc_common.requests.Session, "send", autospec=True,
72
+ side_effect=asc_common.requests.exceptions.ReadTimeout(PRIVATE)) as sender, \
73
+ mock.patch.object(asc_common.time, "sleep") as sleep:
74
+ with self.assertRaisesRegex(SystemExit, "after 1 attempts.*category=read_timeout"):
75
+ asc_common.request("GET", "/builds", PRIVATE, max_attempts=1)
76
+ self.assertEqual(sender.call_count, 1)
77
+ sleep.assert_not_called()
78
+
79
+ def test_missing_or_invalid_proxy_refuses_before_sender(self):
80
+ for proxy in ["", "not-a-proxy"]:
81
+ with self.subTest(proxy=proxy), mock.patch.dict(os.environ, {"APPLE_STORE_PROXY_URL": proxy}), \
82
+ mock.patch.object(asc_common.requests.Session, "send") as sender, \
83
+ mock.patch.object(asc_common.time, "sleep") as sleep:
84
+ with self.assertRaisesRegex(SystemExit, "direct requests are refused"):
85
+ asc_common.request("GET", "/builds", PRIVATE)
86
+ sender.assert_not_called()
87
+ sleep.assert_not_called()
88
+
89
+ def test_http_refusal_is_not_reclassified_as_network_failure(self):
90
+ response = asc_common.requests.Response()
91
+ response.status_code = 401
92
+ response._content = b"unauthorized"
93
+ with mock.patch.object(asc_common.requests.Session, "send", return_value=response) as sender, \
94
+ mock.patch.object(asc_common.time, "sleep") as sleep:
95
+ with self.assertRaises(SystemExit) as caught:
96
+ asc_common.request("GET", "/builds", PRIVATE)
97
+ self.assertEqual(sender.call_count, 1)
98
+ sleep.assert_not_called()
99
+ self.assertEqual(str(caught.exception), "ASC GET /builds failed: 401\nunauthorized")
100
+
101
+
102
+ if __name__ == "__main__":
103
+ unittest.main()
@@ -1 +1 @@
1
- 1.0.75
1
+ 1.0.76
@@ -1 +1 @@
1
- 1.0.75
1
+ 1.0.76
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.75",
3
+ "version": "1.0.76",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {