gowalk-cicd 1.0.107 → 1.0.109
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/CLAUDE.md +6 -5
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/android-action/scripts/flutter_manifest.py +2 -1
- package/android-action/scripts/flutter_manifest_fixture.py +4 -2
- package/android-action/scripts/test_flutter_proxy_retry.py +76 -0
- package/backend-action/.daemux-version +1 -1
- package/docs/GRADLE_CACHE.md +32 -0
- package/package.json +3 -2
- package/templates/deploy.yml +7 -2
package/CLAUDE.md
CHANGED
|
@@ -161,7 +161,8 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
161
161
|
It retains the account proxy and allows four attempts within 60 seconds for manifests or 20 minutes
|
|
162
162
|
for archives, with an 8 GiB archive bound. Interrupted archives resume private bytes with HTTP Range; an origin ignoring
|
|
163
163
|
Range permits a bounded full restart. Only complete JSON or an archive matching the selected official
|
|
164
|
-
manifest's SHA-256 enters action outputs.
|
|
164
|
+
manifest's SHA-256 enters action outputs. Proxy CONNECT 502/503/504 share that retry budget even
|
|
165
|
+
when curl exits 22 with origin status 0. Authentication/certificate refusals remain terminal.
|
|
165
166
|
`flutter_download_failed` emits fixed transport codes and retained-byte counts, never curl stderr.
|
|
166
167
|
Archive progress emits retained byte counts every minute and safe results after each attempt;
|
|
167
168
|
the transfer deadline leaves room for a failure receipt before a 30-minute validation job ends.
|
|
@@ -188,10 +189,10 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
188
189
|
URL/pin/schema/release fields are checked before reuse. Blank, wildcard and version-file inputs
|
|
189
190
|
resolve online. Missing or invalid metadata falls back to the required proxied GET; missing
|
|
190
191
|
account transport still refuses even with a warm cache. SDK keys and checksum checks stay unchanged.
|
|
191
|
-
- Gradle caches use
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
- Gradle caches use checksum-pinned restore/save actions, each optional and bounded to two minutes.
|
|
193
|
+
Preserve setup-java's complete keys, inputs and absolute paths; restore the same OS/architecture prefix.
|
|
194
|
+
Failed releases save distinct partial keys unless cancelled; completed keys require success. Builds remain required.
|
|
195
|
+
Only pinned cache entrypoints get cache transport; see [cache behavior](docs/GRADLE_CACHE.md) before adoption.
|
|
195
196
|
- GitHub artifact uploads have their own checksum-pinned `artifact_env.cjs` process scope.
|
|
196
197
|
It reuses the cache transport's approved HTTPS request guard, then removes inherited proxy
|
|
197
198
|
variables only inside the official `upload-artifact` process. Unknown programs/origins refuse;
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.109
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.109
|
|
@@ -51,7 +51,8 @@ def download(arguments: list[str], directory: Path) -> int:
|
|
|
51
51
|
if attempt == 4 or proxy_http in {401, 403, 407} or http in {401, 403}:
|
|
52
52
|
break
|
|
53
53
|
restart = kind == "archive" and code == 33 and http == 200
|
|
54
|
-
if not restart and code not in RETRY_CODES and http not in RETRY_HTTP
|
|
54
|
+
if (not restart and code not in RETRY_CODES and http not in RETRY_HTTP
|
|
55
|
+
and proxy_http not in {502, 503, 504}):
|
|
55
56
|
break
|
|
56
57
|
if kind == "manifest" or restart:
|
|
57
58
|
destination.write_bytes(b"")
|
|
@@ -60,8 +60,10 @@ class Proxy(socketserver.StreamRequestHandler):
|
|
|
60
60
|
with self.server.lock:
|
|
61
61
|
self.server.connects += 1
|
|
62
62
|
mode = self.server.modes.pop(0)
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
proxy_status = {"proxy_auth": 407, "proxy_401": 401, "proxy_403": 403,
|
|
64
|
+
"proxy_502": 502, "proxy_503": 503, "proxy_504": 504}.get(mode)
|
|
65
|
+
if proxy_status:
|
|
66
|
+
self.wfile.write(f"HTTP/1.1 {proxy_status} Fixture refusal\r\nContent-Length: 0\r\n\r\n".encode())
|
|
65
67
|
return
|
|
66
68
|
self.wfile.write(b"HTTP/1.1 200 Connection established\r\n\r\n")
|
|
67
69
|
self.wfile.flush()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Real curl retries transient CONNECT failures without sending or publishing partial origin data."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
from flutter_manifest_fixture import HOST, MANIFEST, tls_proxy
|
|
11
|
+
|
|
12
|
+
ROOT = Path(__file__).parent
|
|
13
|
+
ARCHIVE = bytes(range(256)) * 512
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProxyRetryTests(unittest.TestCase):
|
|
17
|
+
def setUp(self):
|
|
18
|
+
directory = tempfile.TemporaryDirectory()
|
|
19
|
+
self.addCleanup(directory.cleanup)
|
|
20
|
+
self.root = Path(directory.name)
|
|
21
|
+
|
|
22
|
+
def invoke(self, server, cert, kind="manifest"):
|
|
23
|
+
proxy = f"http://fixture-user:fixture-secret@127.0.0.1:{server.server_address[1]}"
|
|
24
|
+
env = {**os.environ, "HOME": str(self.root), "RUNNER_TEMP": str(self.root),
|
|
25
|
+
"HTTPS_PROXY": proxy, "https_proxy": proxy, "NO_PROXY": "*", "no_proxy": "*",
|
|
26
|
+
"GOWALK_FLUTTER_ARCHIVE_SHA256": hashlib.sha256(ARCHIVE).hexdigest()}
|
|
27
|
+
for key in ("CURL_CA_BUNDLE", "SSL_CERT_FILE", "SSL_CERT_DIR", "GOWALK_FLUTTER_ARCHIVE_CACHE",
|
|
28
|
+
"GOWALK_FLUTTER_RELEASE_CACHE"):
|
|
29
|
+
env.pop(key, None)
|
|
30
|
+
env["CURL_CA_BUNDLE"] = str(cert)
|
|
31
|
+
(self.root / ".curlrc").write_text('noproxy = "*"\ninsecure\n')
|
|
32
|
+
path = "releases_linux.json" if kind == "manifest" else "stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
|
33
|
+
args = ["--connect-timeout", "15", "--retry", "5"]
|
|
34
|
+
if kind == "manifest":
|
|
35
|
+
args.insert(0, "--silent")
|
|
36
|
+
result = subprocess.run([sys.executable, str(ROOT / "flutter_manifest.py"), *args,
|
|
37
|
+
f"https://{HOST}/flutter_infra_release/releases/{path}"],
|
|
38
|
+
env=env, capture_output=True, timeout=20)
|
|
39
|
+
self.assertNotIn(b"fixture-secret", result.stdout + result.stderr)
|
|
40
|
+
self.assertFalse(list(self.root.glob("flutter-manifest-*")))
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
def test_transient_connect_failures_recover_through_the_same_proxy(self):
|
|
44
|
+
for status in (502, 503, 504):
|
|
45
|
+
with self.subTest(status=status), tls_proxy(self.root, [f"proxy_{status}", "ok"]) as (server, cert):
|
|
46
|
+
result = self.invoke(server, cert)
|
|
47
|
+
self.assertEqual((result.returncode, result.stdout), (0, MANIFEST), result.stderr)
|
|
48
|
+
self.assertEqual((server.connects, len(server.requests)), (2, 1))
|
|
49
|
+
self.assertIn(b"flutter_manifest_recovered attempts=2", result.stderr)
|
|
50
|
+
|
|
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
|
+
result = self.invoke(server, cert)
|
|
54
|
+
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)
|
|
57
|
+
|
|
58
|
+
def test_connect_failure_preserves_partial_archive_for_a_verified_range_resume(self):
|
|
59
|
+
with tls_proxy(self.root, ["partial", "proxy_502", "range"], ARCHIVE) as (server, cert):
|
|
60
|
+
result = self.invoke(server, cert, "archive")
|
|
61
|
+
self.assertEqual((result.returncode, result.stdout), (0, ARCHIVE), result.stderr)
|
|
62
|
+
self.assertEqual(server.connects, 3)
|
|
63
|
+
self.assertEqual(server.ranges, ["", "bytes=31-"])
|
|
64
|
+
self.assertIn(b"flutter_archive_recovered attempts=3", result.stderr)
|
|
65
|
+
|
|
66
|
+
def test_proxy_authorization_refusals_remain_terminal(self):
|
|
67
|
+
for mode in ("proxy_401", "proxy_403", "proxy_auth"):
|
|
68
|
+
with self.subTest(mode=mode), tls_proxy(self.root, [mode]) as (server, cert):
|
|
69
|
+
result = self.invoke(server, cert)
|
|
70
|
+
self.assertNotEqual(result.returncode, 0)
|
|
71
|
+
self.assertEqual((result.stdout, server.connects, server.requests), (b"", 1, []))
|
|
72
|
+
self.assertIn(b"attempts=1", result.stderr)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
unittest.main()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.109
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Gradle dependency retention
|
|
2
|
+
|
|
3
|
+
The mobile release workflow retains completed dependency downloads even when its build or a
|
|
4
|
+
later store operation fails. Both cache operations remain optional and bounded to two minutes;
|
|
5
|
+
a cache result cannot turn a failed build or upload green. Cancelled jobs and workflows reusing
|
|
6
|
+
a confirmed upload do not save Gradle caches.
|
|
7
|
+
|
|
8
|
+
Successful jobs preserve the original setup-java complete key, dependency inputs and absolute
|
|
9
|
+
cache paths. Failed jobs save a distinct `-partial-<run_id>-<attempt>` suffix instead, so a
|
|
10
|
+
partial download cannot occupy the immutable completed key or prevent a later full save.
|
|
11
|
+
Restoration first uses the complete key, then the existing same-OS/architecture key prefix.
|
|
12
|
+
Required Gradle work resolves any still-missing dependencies through the assigned account exit.
|
|
13
|
+
The pinned cache preloader remains confined to the official cache processes.
|
|
14
|
+
|
|
15
|
+
GitHub caches are branch-scoped: task branches cannot restore sibling branches' caches, while
|
|
16
|
+
default-branch caches can serve subsequent task branches. The normal default-branch release
|
|
17
|
+
therefore retains useful downloads after failure as well as after successful delivery. It
|
|
18
|
+
adds no warming workflow, extra main validation, upload retry or release dispatch.
|
|
19
|
+
|
|
20
|
+
On adoption, preserve app-specific dependency inputs, platforms and existing restore identities.
|
|
21
|
+
Candidate and release workflows need compatible keys, paths and compression versions to share
|
|
22
|
+
default-branch caches. An app's custom prefix needs an explicit compatible fallback; changing a
|
|
23
|
+
package pin alone does not update custom workflow bytes. Keep the current task branch and its
|
|
24
|
+
cache while repairing a failed candidate; do not rotate branches merely to retry it.
|
|
25
|
+
|
|
26
|
+
Verify the actual cache's branch, key, size and restore/save result. A successful cache action
|
|
27
|
+
does not itself prove a hit or a complete dependency graph. Never transfer task-branch caches
|
|
28
|
+
into a trusted default-branch scope by bypassing GitHub's restrictions.
|
|
29
|
+
|
|
30
|
+
See [GitHub's cache restrictions and key matching][cache-rules].
|
|
31
|
+
|
|
32
|
+
[cache-rules]: https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gowalk-cicd",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.109",
|
|
4
4
|
"description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"!backend-action/**/*.py[cod]",
|
|
21
21
|
"templates/",
|
|
22
22
|
"README.md",
|
|
23
|
-
"CLAUDE.md"
|
|
23
|
+
"CLAUDE.md",
|
|
24
|
+
"docs/GRADLE_CACHE.md"
|
|
24
25
|
],
|
|
25
26
|
"keywords": [
|
|
26
27
|
"ios",
|
package/templates/deploy.yml
CHANGED
|
@@ -403,6 +403,8 @@ jobs:
|
|
|
403
403
|
${{ format('setup-java-{0}-{1}-gradle-{2}', runner.os, steps.gradle_scope.outputs.arch,
|
|
404
404
|
hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'buildSrc/**/Versions.kt',
|
|
405
405
|
'buildSrc/**/Dependencies.kt', 'gradle/*.versions.toml', '**/versions.properties')) }}
|
|
406
|
+
restore-keys: >-
|
|
407
|
+
${{ format('setup-java-{0}-{1}-gradle-', runner.os, steps.gradle_scope.outputs.arch) }}
|
|
406
408
|
- name: Set up Flutter
|
|
407
409
|
if: ${{ steps.mode.outputs.mode == 'local' && steps.flutter.outputs.enabled == 'true' }}
|
|
408
410
|
uses: ./.github/actions/android-app/flutter-setup
|
|
@@ -581,7 +583,7 @@ jobs:
|
|
|
581
583
|
|
|
582
584
|
- name: Save Gradle dependencies
|
|
583
585
|
if: >-
|
|
584
|
-
${{
|
|
586
|
+
${{ !cancelled() && steps.mode.outputs.mode == 'local'
|
|
585
587
|
&& steps.gradle_cache.outputs.cache-primary-key != ''
|
|
586
588
|
&& steps.gradle_cache.outputs.cache-hit != 'true' }}
|
|
587
589
|
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25
|
|
@@ -593,7 +595,10 @@ jobs:
|
|
|
593
595
|
path: |
|
|
594
596
|
${{ steps.gradle_scope.outputs.home }}/.gradle/caches
|
|
595
597
|
${{ steps.gradle_scope.outputs.home }}/.gradle/wrapper
|
|
596
|
-
key:
|
|
598
|
+
key: >-
|
|
599
|
+
${{ job.status == 'success' && steps.gradle_cache.outputs.cache-primary-key
|
|
600
|
+
|| format('{0}-partial-{1}-{2}', steps.gradle_cache.outputs.cache-primary-key,
|
|
601
|
+
github.run_id, github.run_attempt) }}
|
|
597
602
|
|
|
598
603
|
# GitHub only registers a newly added workflow after it reaches the default
|
|
599
604
|
# branch. A brand-new app's feature branch therefore cannot dispatch the
|