gowalk-cicd 1.0.116 → 1.0.117

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.116
1
+ 1.0.117
@@ -1 +1 @@
1
- 1.0.116
1
+ 1.0.117
@@ -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,6 +9,29 @@ 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
 
@@ -23,7 +46,7 @@ def download(arguments: list[str], directory: Path) -> int:
23
46
  if kind == "archive":
24
47
  flutter_download.archive_digest()
25
48
  child_env = {**os.environ, "NO_PROXY": "", "no_proxy": ""}
26
- deadline, code, http, proxy_http = time.monotonic() + (60 if kind == "manifest" else 1200), 1, 0, 0
49
+ deadline, code, http, proxy_http = time.monotonic() + (60 if kind == "manifest" else archive_budget()), 1, 0, 0
27
50
  destination = directory / "download"
28
51
  destination.touch(mode=0o600, exist_ok=True)
29
52
  destination.chmod(0o600)
@@ -38,5 +38,47 @@ 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
+
41
83
  if __name__ == "__main__":
42
84
  unittest.main()
@@ -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
- self.assertEqual(set(setup["inputs"]), inputs)
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.116
1
+ 1.0.117
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.116",
3
+ "version": "1.0.117",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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