gowalk-cicd 1.0.116 → 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/flutter-setup/action.yml +8 -0
- package/android-action/scripts/flutter_manifest.py +42 -4
- package/android-action/scripts/test_flutter_download_budget.py +103 -0
- package/android-action/scripts/test_flutter_manifest.py +13 -5
- package/android-action/scripts/test_flutter_proxy_retry.py +12 -5
- package/android-action/scripts/test_google_bootstrap_wiring.py +11 -1
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
- package/templates/deploy.yml +9 -0
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.118
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.118
|
|
@@ -49,6 +49,13 @@ inputs:
|
|
|
49
49
|
description: Git clone source
|
|
50
50
|
required: false
|
|
51
51
|
default: https://github.com/flutter/flutter.git
|
|
52
|
+
archive-budget-seconds:
|
|
53
|
+
description: >-
|
|
54
|
+
Seconds the proxied SDK archive transfer may take. Raise it in a release
|
|
55
|
+
job, whose own timeout is far longer than a validation job's; the helper
|
|
56
|
+
clamps it and the partial archive is cached either way.
|
|
57
|
+
required: false
|
|
58
|
+
default: ''
|
|
52
59
|
outputs:
|
|
53
60
|
CHANNEL:
|
|
54
61
|
value: ${{ steps.flutter-action.outputs.CHANNEL }}
|
|
@@ -196,6 +203,7 @@ runs:
|
|
|
196
203
|
${{ inputs.cache == 'true' && format('{0}/gowalk-flutter-archives', runner.temp) || '' }}
|
|
197
204
|
GOWALK_FLUTTER_RELEASE_CACHE: ${{ steps.release-metadata.outputs.path }}
|
|
198
205
|
GOWALK_FLUTTER_RELEASE_PIN: ${{ steps.release-metadata.outputs.pin }}
|
|
206
|
+
GOWALK_FLUTTER_ARCHIVE_BUDGET_S: ${{ inputs.archive-budget-seconds }}
|
|
199
207
|
with:
|
|
200
208
|
channel: ${{ steps.flutter-action.outputs.CHANNEL }}
|
|
201
209
|
flutter-version: ${{ steps.flutter-action.outputs.VERSION }}
|
|
@@ -9,11 +9,42 @@ import time
|
|
|
9
9
|
from urllib.parse import urlsplit
|
|
10
10
|
|
|
11
11
|
import flutter_download
|
|
12
|
+
|
|
13
|
+
#: Seconds an archive transfer may take before its receipt is published. The
|
|
14
|
+
#: enclosing job's own room is the only thing that bounds it usefully, and the
|
|
15
|
+
#: two differ by a factor of three: a validation job must report inside 30
|
|
16
|
+
#: minutes, while a release job has 45-60. A default-branch deploy cannot
|
|
17
|
+
#: restore the SDK cache a task branch built (GitHub scopes branch caches away
|
|
18
|
+
#: from the default branch), so it downloads the whole archive through the
|
|
19
|
+
#: account's residential exit at a few hundred KB/s — and a fixed 20-minute cap
|
|
20
|
+
#: made that a guaranteed failure: obd-scanner's release retained 428 MB of its
|
|
21
|
+
#: archive, failed on curl 28, and shipped nothing (2026-09-09).
|
|
22
|
+
ARCHIVE_BUDGET_S = 1200
|
|
23
|
+
MAX_ARCHIVE_BUDGET_S = 3000
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def archive_budget() -> int:
|
|
27
|
+
"""The archive deadline, from ``GOWALK_FLUTTER_ARCHIVE_BUDGET_S`` when the
|
|
28
|
+
workflow declares one. Clamped: never below the current default, never
|
|
29
|
+
beyond a release job's own timeout."""
|
|
30
|
+
try:
|
|
31
|
+
declared = int(os.environ.get("GOWALK_FLUTTER_ARCHIVE_BUDGET_S", "") or ARCHIVE_BUDGET_S)
|
|
32
|
+
except ValueError:
|
|
33
|
+
return ARCHIVE_BUDGET_S
|
|
34
|
+
return max(ARCHIVE_BUDGET_S, min(declared, MAX_ARCHIVE_BUDGET_S))
|
|
12
35
|
import flutter_archive_cache
|
|
13
36
|
import flutter_release_cache
|
|
14
37
|
|
|
15
38
|
RETRY_CODES = {5, 6, 7, 18, 28, 35, 52, 55, 56, 92, 95}
|
|
16
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}
|
|
17
48
|
|
|
18
49
|
|
|
19
50
|
def download(arguments: list[str], directory: Path) -> int:
|
|
@@ -23,7 +54,7 @@ def download(arguments: list[str], directory: Path) -> int:
|
|
|
23
54
|
if kind == "archive":
|
|
24
55
|
flutter_download.archive_digest()
|
|
25
56
|
child_env = {**os.environ, "NO_PROXY": "", "no_proxy": ""}
|
|
26
|
-
deadline, code, http, proxy_http = time.monotonic() + (60 if kind == "manifest" else
|
|
57
|
+
deadline, code, http, proxy_http = time.monotonic() + (60 if kind == "manifest" else archive_budget()), 1, 0, 0
|
|
27
58
|
destination = directory / "download"
|
|
28
59
|
destination.touch(mode=0o600, exist_ok=True)
|
|
29
60
|
destination.chmod(0o600)
|
|
@@ -32,7 +63,8 @@ def download(arguments: list[str], directory: Path) -> int:
|
|
|
32
63
|
if flutter_archive_cache.reuse_complete(destination, kind):
|
|
33
64
|
return 0
|
|
34
65
|
attempted = 0
|
|
35
|
-
|
|
66
|
+
limit = MAX_ATTEMPTS[kind]
|
|
67
|
+
for attempt in range(1, limit + 1):
|
|
36
68
|
remaining = deadline - time.monotonic()
|
|
37
69
|
if remaining <= 0:
|
|
38
70
|
break
|
|
@@ -48,7 +80,7 @@ def download(arguments: list[str], directory: Path) -> int:
|
|
|
48
80
|
if attempt > 1:
|
|
49
81
|
print(f"flutter_{kind}_recovered attempts={attempt}", file=sys.stderr)
|
|
50
82
|
return 0
|
|
51
|
-
if attempt ==
|
|
83
|
+
if attempt == limit or proxy_http in {401, 403, 407} or http in {401, 403}:
|
|
52
84
|
break
|
|
53
85
|
restart = kind == "archive" and code == 33 and http == 200
|
|
54
86
|
if (not restart and code not in RETRY_CODES and http not in RETRY_HTTP
|
|
@@ -58,7 +90,13 @@ def download(arguments: list[str], directory: Path) -> int:
|
|
|
58
90
|
destination.write_bytes(b"")
|
|
59
91
|
if destination.stat().st_size > flutter_download.MAX_ARCHIVE_BYTES:
|
|
60
92
|
raise ValueError("flutter_archive_size_exceeded")
|
|
61
|
-
|
|
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))
|
|
62
100
|
flutter_download.failure(kind, code, http, proxy_http, attempted, destination.stat().st_size)
|
|
63
101
|
return code or 1
|
|
64
102
|
|
|
@@ -38,5 +38,108 @@ class DownloadBudgetTests(unittest.TestCase):
|
|
|
38
38
|
self.assertNotIn("fixture-secret", error.getvalue())
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
class ArchiveBudgetTests(unittest.TestCase):
|
|
42
|
+
def test_a_release_job_may_declare_more_archive_room_than_a_validation_job(self):
|
|
43
|
+
with patch.dict(os.environ, {}, clear=False):
|
|
44
|
+
os.environ.pop("GOWALK_FLUTTER_ARCHIVE_BUDGET_S", None)
|
|
45
|
+
self.assertEqual(flutter_manifest.archive_budget(), 1200)
|
|
46
|
+
os.environ["GOWALK_FLUTTER_ARCHIVE_BUDGET_S"] = "2100"
|
|
47
|
+
self.assertEqual(flutter_manifest.archive_budget(), 2100)
|
|
48
|
+
# Never shorter than the default, never past a release job's timeout,
|
|
49
|
+
# and a malformed value is the default rather than a refusal.
|
|
50
|
+
os.environ["GOWALK_FLUTTER_ARCHIVE_BUDGET_S"] = "60"
|
|
51
|
+
self.assertEqual(flutter_manifest.archive_budget(), 1200)
|
|
52
|
+
os.environ["GOWALK_FLUTTER_ARCHIVE_BUDGET_S"] = "999999"
|
|
53
|
+
self.assertEqual(flutter_manifest.archive_budget(), 3000)
|
|
54
|
+
os.environ["GOWALK_FLUTTER_ARCHIVE_BUDGET_S"] = "not-a-number"
|
|
55
|
+
self.assertEqual(flutter_manifest.archive_budget(), 1200)
|
|
56
|
+
os.environ["GOWALK_FLUTTER_ARCHIVE_BUDGET_S"] = ""
|
|
57
|
+
self.assertEqual(flutter_manifest.archive_budget(), 1200)
|
|
58
|
+
|
|
59
|
+
def test_a_declared_budget_actually_extends_the_transfer_deadline(self):
|
|
60
|
+
elapsed = [0.0]
|
|
61
|
+
|
|
62
|
+
def transfer(_url, destination, _kind, remaining, _env):
|
|
63
|
+
elapsed[0] += min(600, remaining)
|
|
64
|
+
with destination.open("ab") as output:
|
|
65
|
+
output.write(b"partial")
|
|
66
|
+
return 28, 200, 200
|
|
67
|
+
|
|
68
|
+
with tempfile.TemporaryDirectory() as directory, contextlib.redirect_stderr(io.StringIO()):
|
|
69
|
+
with patch.dict(os.environ, HTTPS_PROXY="http://fixture:secret@localhost:1",
|
|
70
|
+
GOWALK_FLUTTER_ARCHIVE_SHA256="a" * 64,
|
|
71
|
+
GOWALK_FLUTTER_ARCHIVE_BUDGET_S="2100"):
|
|
72
|
+
with patch.object(flutter_manifest.time, "monotonic", side_effect=lambda: elapsed[0]), \
|
|
73
|
+
patch.object(flutter_manifest.time, "sleep", side_effect=lambda seconds: None), \
|
|
74
|
+
patch.object(flutter_manifest.flutter_download, "transfer", side_effect=transfer):
|
|
75
|
+
flutter_manifest.download(["--connect-timeout", "15", "--retry", "5",
|
|
76
|
+
"https://fixture.invalid/flutter_infra_release/releases/stable/linux/sdk.tar.xz"],
|
|
77
|
+
Path(directory))
|
|
78
|
+
# The declared deadline bounds the pass, not the two 600 s attempts a
|
|
79
|
+
# 1200 s default allowed: the last attempt gets the remaining 300 s.
|
|
80
|
+
self.assertEqual(elapsed[0], 2100)
|
|
81
|
+
|
|
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
|
+
|
|
41
144
|
if __name__ == "__main__":
|
|
42
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):
|
|
@@ -20,6 +20,9 @@ class BootstrapWiringTests(unittest.TestCase):
|
|
|
20
20
|
if step is install:
|
|
21
21
|
expected_env["GOWALK_FLUTTER_ARCHIVE_CACHE"] = (
|
|
22
22
|
"${{ inputs.cache == 'true' && format('{0}/gowalk-flutter-archives', runner.temp) || '' }}")
|
|
23
|
+
# Only the step that actually transfers the archive carries the
|
|
24
|
+
# caller's declared budget; the dry-run resolve must not.
|
|
25
|
+
expected_env["GOWALK_FLUTTER_ARCHIVE_BUDGET_S"] = "${{ inputs.archive-budget-seconds }}"
|
|
23
26
|
self.assertEqual(step["env"], expected_env)
|
|
24
27
|
self.assertEqual(step["with"]["cache"], "false")
|
|
25
28
|
self.assertEqual(step["with"]["pub-cache"], "false")
|
|
@@ -38,6 +41,11 @@ class BootstrapWiringTests(unittest.TestCase):
|
|
|
38
41
|
self.assertEqual(job["env"]["GOOGLE_STORE_PROXY_URL"],
|
|
39
42
|
"${{ secrets.GOOGLE_STORE_PROXY_URL }}")
|
|
40
43
|
self.assertNotIn("continue-on-error", step)
|
|
44
|
+
# A release job runs on the default branch, which cannot
|
|
45
|
+
# restore a task branch's SDK cache, so it declares the
|
|
46
|
+
# archive room its own timeout allows (2026-09-09).
|
|
47
|
+
if name == "deploy.yml" and int(job.get("timeout-minutes", 0)) >= 45:
|
|
48
|
+
self.assertEqual(step["with"]["archive-budget-seconds"], "2100")
|
|
41
49
|
self.assertEqual(checked, 3)
|
|
42
50
|
setup = yaml.safe_load((ROOT / "android-action/flutter-setup/action.yml").read_text())
|
|
43
51
|
steps = setup["runs"]["steps"]
|
|
@@ -65,7 +73,9 @@ class BootstrapWiringTests(unittest.TestCase):
|
|
|
65
73
|
setup = yaml.safe_load((ROOT / "android-action/flutter-setup/action.yml").read_text())
|
|
66
74
|
inputs = {"channel", "flutter-version", "flutter-version-file", "architecture", "cache", "cache-key",
|
|
67
75
|
"cache-path", "pub-cache", "pub-cache-key", "pub-cache-path", "dry-run", "git-source"}
|
|
68
|
-
|
|
76
|
+
# `archive-budget-seconds` is this action's own knob, not one of the
|
|
77
|
+
# official action's inputs: it reaches the transfer as an env var.
|
|
78
|
+
self.assertEqual(set(setup["inputs"]), inputs | {"archive-budget-seconds"})
|
|
69
79
|
resolve = next(step for step in setup["runs"]["steps"] if step.get("id") == "flutter-action")
|
|
70
80
|
for name in inputs - {"cache", "pub-cache", "dry-run"}:
|
|
71
81
|
self.assertEqual(resolve["with"][name], "${{ inputs." + name + " }}")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.118
|
package/package.json
CHANGED
package/templates/deploy.yml
CHANGED
|
@@ -101,6 +101,12 @@ jobs:
|
|
|
101
101
|
with:
|
|
102
102
|
channel: stable
|
|
103
103
|
cache: true
|
|
104
|
+
# A release runs on the default branch, which cannot restore the SDK
|
|
105
|
+
# cache a task branch built, so it downloads the whole archive through
|
|
106
|
+
# the account exit. This job has an hour; the helper's own 20-minute
|
|
107
|
+
# default belongs to a validation job and turned that download into a
|
|
108
|
+
# guaranteed failure (obd-scanner, 2026-09-09: 428 MB retained, curl 28).
|
|
109
|
+
archive-budget-seconds: '2100'
|
|
104
110
|
- name: Retain CocoaPods cache process scope
|
|
105
111
|
id: pods-cache-scope
|
|
106
112
|
if: ${{ steps.flutter.outputs.enabled == 'true' }}
|
|
@@ -411,6 +417,9 @@ jobs:
|
|
|
411
417
|
with:
|
|
412
418
|
channel: stable
|
|
413
419
|
cache: true
|
|
420
|
+
# 35 of this job's 45 minutes; see the iOS job for why a release needs
|
|
421
|
+
# more archive room than a validation run.
|
|
422
|
+
archive-budget-seconds: '2100'
|
|
414
423
|
# Flutter apps can depend on private git packages. Most repos in this fleet
|
|
415
424
|
# embed a PAT directly in the pubspec URL, which publishes the token into
|
|
416
425
|
# source and into every fork of the lockfile. Setting the optional
|