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.
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/android-action/scripts/flutter_manifest.py +18 -3
- package/android-action/scripts/test_flutter_download_budget.py +61 -0
- package/android-action/scripts/test_flutter_manifest.py +13 -5
- package/android-action/scripts/test_flutter_proxy_retry.py +12 -5
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.118
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
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
|
-
|
|
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 ==
|
|
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
|
-
|
|
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=
|
|
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
|
|
103
|
-
|
|
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), (
|
|
107
|
-
self.assertIn("attempts=
|
|
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=
|
|
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
|
|
52
|
-
|
|
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"",
|
|
56
|
-
self.assertIn(
|
|
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.
|
|
1
|
+
1.0.118
|