gowalk-cicd 1.0.75 → 1.0.77

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.
package/CLAUDE.md CHANGED
@@ -172,6 +172,10 @@ node /path/to/gowalk-cicd/bin/cli.mjs
172
172
  - The mobile workflow's feature-branch-only `backend-preview` job bootstraps a
173
173
  new app before GitHub registers `deploy-backend.yml` on the default branch.
174
174
  Default-branch deploys continue through the separate backend workflow.
175
+ - Mobile pushes exclude durable `.factory/progress` records and the separately deployed
176
+ backend/server/Compose paths. A mixed commit with any native/shared input still builds;
177
+ explicit workflow dispatch remains available. Keep backend deployment's own path filters
178
+ intact, and do not broadly exclude assets, root build settings or unknown app layouts.
175
179
  - Domain-backed backend deploys fail closed on certificate issuance and the
176
180
  public HTTPS health probe. `select_certbot_account.sh` chooses one existing
177
181
  Let's Encrypt ACME v2 account deterministically so multi-account hosts stay
@@ -1 +1 @@
1
- 1.0.75
1
+ 1.0.77
@@ -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.77
@@ -1 +1 @@
1
- 1.0.75
1
+ 1.0.77
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.75",
3
+ "version": "1.0.77",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,6 +6,13 @@ on:
6
6
  - '**/*.md'
7
7
  - '.gitignore'
8
8
  - 'creds/**'
9
+ # Durable task records and independently deployed backend sources do not
10
+ # change a native binary. Mixed commits still run when any mobile input changes.
11
+ - '.factory/progress/**'
12
+ - 'backend/**'
13
+ - 'server/**'
14
+ - 'docker-compose.yml'
15
+ - '.github/workflows/deploy-backend.yml'
9
16
  - '.github/actions/swift-app/**'
10
17
  - '.github/actions/android-app/**'
11
18
  workflow_dispatch: