gowalk-cicd 1.0.117 → 1.0.119
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/action/scripts/native_pods_diagnostics.py +18 -1
- package/action/scripts/test_native_pods_diagnostics.py +20 -0
- 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.119
|
|
@@ -37,6 +37,23 @@ def excerpt(text: str) -> str:
|
|
|
37
37
|
return primary + "\n...\n" + tail
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
#: A long run that is a lowercase snake_case NAME rather than an opaque value.
|
|
41
|
+
#: Thirteen of this helper's own error codes are 32 characters or more —
|
|
42
|
+
#: `native_podfile_lock_commit_required` is 35, `native_google_transport_helper_required`
|
|
43
|
+
#: is 39 — so the catch-all below erased the one token the classifier reads back
|
|
44
|
+
#: out of this text: task 1185's iOS failure published `"code": "[long value
|
|
45
|
+
#: redacted]"` with an empty `signals` list, and every pods failure in the fleet
|
|
46
|
+
#: was diagnosed blind (2026-09-09). Requiring an underscore keeps hex digests and
|
|
47
|
+
#: base32/base64 material opaque, and every environment value has already been
|
|
48
|
+
#: replaced verbatim above.
|
|
49
|
+
_NAME_RUN = re.compile(r"[a-z]+(?:_[a-z0-9]+)+")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _opaque_run(match: "re.Match[str]") -> str:
|
|
53
|
+
run = match.group(0)
|
|
54
|
+
return run if _NAME_RUN.fullmatch(run) else "[long value redacted]"
|
|
55
|
+
|
|
56
|
+
|
|
40
57
|
def diagnostic(log, environment: dict[str, str] | None = None) -> str:
|
|
41
58
|
log.flush()
|
|
42
59
|
log.seek(0, os.SEEK_END)
|
|
@@ -57,7 +74,7 @@ def diagnostic(log, environment: dict[str, str] | None = None) -> str:
|
|
|
57
74
|
r'\1"[redacted]"', text)
|
|
58
75
|
text = re.sub(r"\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+", "[token redacted]", text)
|
|
59
76
|
text = re.sub(r"https?://[^\s\"'<>]+", "[URL redacted]", text)
|
|
60
|
-
text = re.sub(r"[A-Za-z0-9_+/=-]{32,}",
|
|
77
|
+
text = re.sub(r"[A-Za-z0-9_+/=-]{32,}", _opaque_run, text)
|
|
61
78
|
return excerpt(text)
|
|
62
79
|
|
|
63
80
|
|
|
@@ -23,6 +23,26 @@ class DiagnosticsTests(unittest.TestCase):
|
|
|
23
23
|
'cHJpdmF0ZTpwYXNzd29yZA==', 'PRIVATE_JSON_MARKER', 'PRIVATE_ACCESS_MARKER', 'SECRET'):
|
|
24
24
|
self.assertNotIn(private, result)
|
|
25
25
|
|
|
26
|
+
def test_error_codes_survive_while_opaque_runs_of_the_same_length_do_not(self):
|
|
27
|
+
"""The classifier reads the plugin's own code back out of this text, and
|
|
28
|
+
thirteen of the twenty-seven are 32 characters or more. Task 1185's iOS
|
|
29
|
+
failure published `"code": "[long value redacted]"` with an empty
|
|
30
|
+
`signals` list because the catch-all erased it (2026-09-09)."""
|
|
31
|
+
body = ('{"code": "native_podfile_lock_commit_required", "ok": false}\n'
|
|
32
|
+
'native_google_transport_helper_required native_pods_operation_unverified\n'
|
|
33
|
+
'digest 9f2b7c1e4a8d3b6f0c5e2a9d7b4f1c8e3a6d0b5f were removed\n'
|
|
34
|
+
'MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQ==\n'
|
|
35
|
+
'a_secret_looking_run_of_lowercase_words_kept\n')
|
|
36
|
+
result = diagnostic(io.BytesIO(body.encode()), {})
|
|
37
|
+
for code in ('native_podfile_lock_commit_required', 'native_google_transport_helper_required',
|
|
38
|
+
'native_pods_operation_unverified'):
|
|
39
|
+
self.assertIn(code, result)
|
|
40
|
+
# A hex digest and base64 material carry no underscore and stay opaque.
|
|
41
|
+
for opaque in ('9f2b7c1e4a8d3b6f0c5e2a9d7b4f1c8e3a6d0b5f',
|
|
42
|
+
'MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQ=='):
|
|
43
|
+
self.assertNotIn(opaque, result)
|
|
44
|
+
self.assertIn('[long value redacted]', result)
|
|
45
|
+
|
|
26
46
|
def test_truncated_first_line_and_small_proxy_credentials_cannot_escape(self):
|
|
27
47
|
body = b"x" * 17000 + b"TAIL_SECRET\n[!] Unable to satisfy dependency version 12.3.\n"
|
|
28
48
|
self.assertNotIn("TAIL_SECRET", diagnostic(io.BytesIO(body), {}))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.119
|
|
@@ -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.119
|