gowalk-cicd 1.0.117 → 1.0.118

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.117
1
+ 1.0.118
@@ -1 +1 @@
1
- 1.0.117
1
+ 1.0.118
@@ -37,6 +37,14 @@ import flutter_release_cache
37
37
 
38
38
  RETRY_CODES = {5, 6, 7, 18, 28, 35, 52, 55, 56, 92, 95}
39
39
  RETRY_HTTP = {408, 429, 500, 502, 503, 504}
40
+ #: Attempts each kind of transfer may make. The archive's deadline already
41
+ #: bounds its pass, so four stands there. The MANIFEST is the one that gave up
42
+ #: early: the account exit refused four TLS handshakes in ten seconds (curl 35,
43
+ #: proxy_status 200) and the iOS release failed with fifty seconds of its own
44
+ #: sixty-second budget unspent — an hour after the same fetch had succeeded
45
+ #: (obd-scanner, 2026-09-09). It is a few KB, so riding out an exit that is
46
+ #: briefly refusing costs one small request each time and nothing else.
47
+ MAX_ATTEMPTS = {"manifest": 12, "archive": 4}
40
48
 
41
49
 
42
50
  def download(arguments: list[str], directory: Path) -> int:
@@ -55,7 +63,8 @@ def download(arguments: list[str], directory: Path) -> int:
55
63
  if flutter_archive_cache.reuse_complete(destination, kind):
56
64
  return 0
57
65
  attempted = 0
58
- for attempt in range(1, 5):
66
+ limit = MAX_ATTEMPTS[kind]
67
+ for attempt in range(1, limit + 1):
59
68
  remaining = deadline - time.monotonic()
60
69
  if remaining <= 0:
61
70
  break
@@ -71,7 +80,7 @@ def download(arguments: list[str], directory: Path) -> int:
71
80
  if attempt > 1:
72
81
  print(f"flutter_{kind}_recovered attempts={attempt}", file=sys.stderr)
73
82
  return 0
74
- if attempt == 4 or proxy_http in {401, 403, 407} or http in {401, 403}:
83
+ if attempt == limit or proxy_http in {401, 403, 407} or http in {401, 403}:
75
84
  break
76
85
  restart = kind == "archive" and code == 33 and http == 200
77
86
  if (not restart and code not in RETRY_CODES and http not in RETRY_HTTP
@@ -81,7 +90,13 @@ def download(arguments: list[str], directory: Path) -> int:
81
90
  destination.write_bytes(b"")
82
91
  if destination.stat().st_size > flutter_download.MAX_ARCHIVE_BYTES:
83
92
  raise ValueError("flutter_archive_size_exceeded")
84
- time.sleep(min(2 ** (attempt - 1), max(0, deadline - time.monotonic())))
93
+ left = max(0.0, deadline - time.monotonic())
94
+ # The manifest waits a flat second between tries: it is riding out an
95
+ # exit that is refusing right now, and an exponential backoff would
96
+ # spend its sixty-second window sleeping instead of retrying. The
97
+ # archive keeps the growing wait — its attempts are minutes long.
98
+ pause = 1 if kind == "manifest" else 2 ** (attempt - 1)
99
+ time.sleep(min(pause, left))
85
100
  flutter_download.failure(kind, code, http, proxy_http, attempted, destination.stat().st_size)
86
101
  return code or 1
87
102
 
@@ -80,5 +80,66 @@ class ArchiveBudgetTests(unittest.TestCase):
80
80
  self.assertEqual(elapsed[0], 2100)
81
81
 
82
82
 
83
+
84
+
85
+ class ManifestRetryTests(unittest.TestCase):
86
+ def test_a_briefly_refusing_exit_is_retried_until_the_deadline(self):
87
+ """The iOS release lost its manifest to four TLS refusals in ten seconds
88
+ (curl 35, proxy_status 200) with fifty seconds of budget unspent, an hour
89
+ after the same fetch succeeded."""
90
+ elapsed, attempts = [0.0], []
91
+
92
+ def transfer(_url, destination, _kind, _remaining, _env):
93
+ attempts.append(elapsed[0])
94
+ elapsed[0] += 2.5 # a TLS refusal fails fast
95
+ destination.write_bytes(b"")
96
+ return 35, 0, 200
97
+
98
+ with tempfile.TemporaryDirectory() as directory, contextlib.redirect_stderr(io.StringIO()) as error:
99
+ with patch.dict(os.environ, HTTPS_PROXY="http://fixture:secret@localhost:1"):
100
+ with patch.object(flutter_manifest.time, "monotonic", side_effect=lambda: elapsed[0]), \
101
+ patch.object(flutter_manifest.time, "sleep",
102
+ side_effect=lambda seconds: elapsed.__setitem__(0, elapsed[0] + seconds)), \
103
+ patch.object(flutter_manifest.flutter_download, "transfer", side_effect=transfer):
104
+ result = flutter_manifest.download(["--silent", "--connect-timeout", "15", "--retry", "5",
105
+ "https://fixture.invalid/flutter_infra_release/releases/releases_macos.json"],
106
+ Path(directory))
107
+ self.assertEqual(result, 35)
108
+ # It used the manifest's whole 60 s window instead of giving up at 10 s.
109
+ self.assertGreater(len(attempts), 4)
110
+ self.assertLessEqual(len(attempts), flutter_manifest.MAX_ATTEMPTS["manifest"])
111
+ self.assertGreater(elapsed[0], 30)
112
+ # The deadline bounds the pass; the last attempt may end just past it.
113
+ self.assertLess(elapsed[0], 70)
114
+ self.assertIn("attempts=%d" % len(attempts), error.getvalue())
115
+ self.assertNotIn("secret", error.getvalue())
116
+
117
+ def test_an_exit_that_recovers_mid_window_still_publishes(self):
118
+ elapsed, calls = [0.0], []
119
+
120
+ def transfer(_url, destination, _kind, _remaining, _env):
121
+ calls.append(1)
122
+ elapsed[0] += 2.5
123
+ if len(calls) < 6:
124
+ destination.write_bytes(b"")
125
+ return 35, 0, 200
126
+ destination.write_bytes(b'{"releases": []}')
127
+ return 0, 200, 200
128
+
129
+ with tempfile.TemporaryDirectory() as directory, contextlib.redirect_stderr(io.StringIO()) as error:
130
+ with patch.dict(os.environ, HTTPS_PROXY="http://fixture:secret@localhost:1"):
131
+ with patch.object(flutter_manifest.time, "monotonic", side_effect=lambda: elapsed[0]), \
132
+ patch.object(flutter_manifest.time, "sleep",
133
+ side_effect=lambda seconds: elapsed.__setitem__(0, elapsed[0] + seconds)), \
134
+ patch.object(flutter_manifest.flutter_download, "publish", side_effect=lambda *a: None), \
135
+ patch.object(flutter_manifest.flutter_download, "transfer", side_effect=transfer):
136
+ result = flutter_manifest.download(["--silent", "--connect-timeout", "15", "--retry", "5",
137
+ "https://fixture.invalid/flutter_infra_release/releases/releases_macos.json"],
138
+ Path(directory))
139
+ self.assertEqual(result, 0)
140
+ self.assertEqual(len(calls), 6)
141
+ self.assertIn("flutter_manifest_recovered attempts=6", error.getvalue())
142
+
143
+
83
144
  if __name__ == "__main__":
84
145
  unittest.main()
@@ -5,6 +5,8 @@ import subprocess
5
5
  import tempfile
6
6
  import unittest
7
7
 
8
+ import flutter_manifest
9
+
8
10
  from flutter_manifest_fixture import HOST, MANIFEST, official_setup, tls_proxy
9
11
 
10
12
  ROOT = Path(__file__).parent
@@ -47,7 +49,7 @@ class ManifestRecoveryTests(unittest.TestCase):
47
49
  env.update(HOME=str(self.root / "absent-home"), NO_PROXY="", no_proxy="")
48
50
  args = ["bash", str(self.setup), "-p", "-n", "3.44.0", "-a", "x64", "-k", "custom-:version:-:hash:"]
49
51
  result = subprocess.run([*args, *(["-t"] if test_mode else []), "stable"], env=env,
50
- capture_output=True, text=True, timeout=20)
52
+ capture_output=True, text=True, timeout=120)
51
53
  self.assertNotIn("fixture-secret", result.stdout + result.stderr)
52
54
  self.assertFalse(list(self.root.glob("flutter-manifest-*")))
53
55
  return result
@@ -99,12 +101,18 @@ class ManifestRecoveryTests(unittest.TestCase):
99
101
  self.assertIn("attempts=1", result.stderr)
100
102
  self.assertEqual(result.stdout, "")
101
103
 
102
- def test_repeated_tls_resets_stop_after_four_private_attempts(self):
103
- with tls_proxy(self.root, ["reset"] * 4) as (server, cert):
104
+ def test_repeated_tls_resets_are_retried_across_the_manifest_window(self):
105
+ """An exit that refuses TLS is retried until the manifest's own deadline,
106
+ not abandoned after four handshakes: obd-scanner's iOS release lost its
107
+ manifest to four refusals in ten seconds with fifty seconds of its
108
+ sixty-second budget unspent, an hour after the same fetch succeeded
109
+ (2026-09-09). Nothing is ever sent to the origin."""
110
+ limit = flutter_manifest.MAX_ATTEMPTS["manifest"]
111
+ with tls_proxy(self.root, ["reset"] * limit) as (server, cert):
104
112
  result = self.invoke(server, cert)
105
113
  self.assertEqual(result.returncode, 35)
106
- self.assertEqual((server.connects, server.requests), (4, []))
107
- self.assertIn("attempts=4", result.stderr)
114
+ self.assertEqual((server.connects, server.requests), (limit, []))
115
+ self.assertIn(f"attempts={limit}", result.stderr)
108
116
  self.assertEqual(result.stdout, "")
109
117
 
110
118
  def test_missing_proxy_refuses_before_any_connection(self):
@@ -7,6 +7,8 @@ import sys
7
7
  import tempfile
8
8
  import unittest
9
9
 
10
+ import flutter_manifest
11
+
10
12
  from flutter_manifest_fixture import HOST, MANIFEST, tls_proxy
11
13
 
12
14
  ROOT = Path(__file__).parent
@@ -35,7 +37,7 @@ class ProxyRetryTests(unittest.TestCase):
35
37
  args.insert(0, "--silent")
36
38
  result = subprocess.run([sys.executable, str(ROOT / "flutter_manifest.py"), *args,
37
39
  f"https://{HOST}/flutter_infra_release/releases/{path}"],
38
- env=env, capture_output=True, timeout=20)
40
+ env=env, capture_output=True, timeout=120)
39
41
  self.assertNotIn(b"fixture-secret", result.stdout + result.stderr)
40
42
  self.assertFalse(list(self.root.glob("flutter-manifest-*")))
41
43
  return result
@@ -48,12 +50,17 @@ class ProxyRetryTests(unittest.TestCase):
48
50
  self.assertEqual((server.connects, len(server.requests)), (2, 1))
49
51
  self.assertIn(b"flutter_manifest_recovered attempts=2", result.stderr)
50
52
 
51
- def test_repeated_connect_failure_stops_after_four_attempts_without_origin_request(self):
52
- with tls_proxy(self.root, ["proxy_502"] * 4) as (server, cert):
53
+ def test_repeated_connect_failure_never_reaches_the_origin(self):
54
+ """A refusing proxy is retried across the manifest's own window and the
55
+ origin request is never sent — the attempt count follows the manifest
56
+ limit, not a fixed four (see test_flutter_manifest)."""
57
+ limit = flutter_manifest.MAX_ATTEMPTS["manifest"]
58
+ with tls_proxy(self.root, ["proxy_502"] * limit) as (server, cert):
53
59
  result = self.invoke(server, cert)
54
60
  self.assertNotEqual(result.returncode, 0)
55
- self.assertEqual((result.stdout, server.connects, server.requests), (b"", 4, []))
56
- self.assertIn(b"http_status=0 proxy_status=502 attempts=4 retained_bytes=0", result.stderr)
61
+ self.assertEqual((result.stdout, server.connects, server.requests), (b"", limit, []))
62
+ self.assertIn(f"http_status=0 proxy_status=502 attempts={limit} retained_bytes=0".encode(),
63
+ result.stderr)
57
64
 
58
65
  def test_connect_failure_preserves_partial_archive_for_a_verified_range_resume(self):
59
66
  with tls_proxy(self.root, ["partial", "proxy_502", "range"], ARCHIVE) as (server, cert):
@@ -1 +1 @@
1
- 1.0.117
1
+ 1.0.118
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.117",
3
+ "version": "1.0.118",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {