gowalk-cicd 1.0.68 → 1.0.70

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 CHANGED
@@ -135,6 +135,12 @@ node /path/to/gowalk-cicd/bin/cli.mjs
135
135
  localization, global activation and builds run through the existing owned relay.
136
136
  The helper's bootstrap and command modes require only Python's standard library;
137
137
  importing the Google API client must not become an unprotected bootstrap prerequisite.
138
+ - Flutter setup scopes `SEGMENT_DOWNLOAD_TIMEOUT_MINS=1` to the composite action's optional
139
+ SDK/pub cache restores. This bounds each cache segment, not the whole cache or SDK download.
140
+ A cache timeout becomes a cache miss; normal proxied SDK setup remains mandatory and its
141
+ errors still fail the job. Never use `continue-on-error` or a timeout around all SDK setup
142
+ to hide an optional cache failure. Do not alter cache keys or disable caches as part of
143
+ this timeout handling.
138
144
  - Backend runtime credentials use the single encrypted `BACKEND_RUNTIME_ENV`
139
145
  secret. The action writes it to host-side `.runtime.env` with mode 0600 and
140
146
  supplies it to Compose after the generated `.env`; never print its content.
package/README.md CHANGED
@@ -26,6 +26,12 @@ are unaffected.
26
26
 
27
27
  Re-run the same command anytime to pull the latest version.
28
28
 
29
+ Flutter's optional SDK and pub cache restores have a one-minute timeout per
30
+ download segment. A slow segment becomes a cache miss and normal SDK setup
31
+ continues through the assigned Google proxy; SDK setup errors still fail the
32
+ job. This bounds individual cache segments, not the total cache or SDK download,
33
+ and keeps existing cache keys and the successful restore/save behavior.
34
+
29
35
  ## Flutter web release
30
36
 
31
37
  The conditional `deploy-web.yml` workflow runs on changes to Flutter source,
@@ -1 +1 @@
1
- 1.0.68
1
+ 1.0.70
@@ -1 +1 @@
1
- 1.0.68
1
+ 1.0.70
@@ -121,7 +121,8 @@ runs:
121
121
  -- bash --noprofile --norc -e -o pipefail {0}
122
122
  run: |
123
123
  if [ -d test ]; then
124
- flutter test
124
+ # Apply only loopback exceptions after the shell's proxy wrapper; retain the exit for SDK traffic.
125
+ env no_proxy=localhost,127.0.0.1,::1 NO_PROXY=localhost,127.0.0.1,::1 flutter test --no-pub
125
126
  else
126
127
  echo "No test/ directory; skipping flutter test."
127
128
  fi
@@ -0,0 +1,58 @@
1
+ // Owned sockets only: exercise Dart's real HTTP/WebSocket proxy behavior under the CI wrapper.
2
+ import 'dart:convert';
3
+ import 'dart:io';
4
+
5
+ Future<void> serve(HttpRequest request) async {
6
+ if (WebSocketTransformer.isUpgradeRequest(request)) {
7
+ final socket = await WebSocketTransformer.upgrade(request);
8
+ socket.listen(socket.add);
9
+ } else {
10
+ request.response.write('owned-loopback');
11
+ await request.response.close();
12
+ }
13
+ }
14
+
15
+ void checkProviderRoutes() {
16
+ final expected = 'PROXY ${Uri.parse(Platform.environment['HTTPS_PROXY']!).authority}';
17
+ for (final host in ['storage.googleapis.com', 'pub.dev', 'oauth2.googleapis.com',
18
+ 'firebaseappcheck.googleapis.com', 'api.appstoreconnect.apple.com']) {
19
+ if (HttpClient.findProxyFromEnvironment(Uri.parse('https://$host/a')) != expected) {
20
+ throw StateError('provider route bypassed');
21
+ }
22
+ }
23
+ }
24
+
25
+ Future<void> main(List<String> args) async {
26
+ if (args.isEmpty || args.first != 'test' || !args.contains('--no-pub')) {
27
+ throw StateError('the rendered command must skip repeated dependency resolution');
28
+ }
29
+ final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
30
+ server.listen(serve);
31
+ final client = HttpClient()..connectionTimeout = const Duration(seconds: 3);
32
+ WebSocket? socket;
33
+ try {
34
+ checkProviderRoutes();
35
+ socket = await WebSocket.connect('ws://127.0.0.1:${server.port}/socket')
36
+ .timeout(const Duration(seconds: 3));
37
+ socket.add('owned-frame');
38
+ if (await socket.first.timeout(const Duration(seconds: 3)) != 'owned-frame') {
39
+ throw StateError('local frame was not echoed');
40
+ }
41
+ final local = await (await client.getUrl(Uri.parse('http://127.0.0.1:${server.port}/http'))).close();
42
+ if (await local.transform(utf8.decoder).join() != 'owned-loopback') {
43
+ throw StateError('local HTTP reached the exit');
44
+ }
45
+ final remote = await (await client.getUrl(Uri.parse('http://unresolvable.invalid/pub'))).close();
46
+ if (await remote.transform(utf8.decoder).join() != 'through pinned proxy') {
47
+ throw StateError('external HTTP did not reach the owned exit');
48
+ }
49
+ print(jsonEncode({'ok': true, 'loopback': ['http', 'websocket'], 'external': 'assigned_proxy'}));
50
+ } catch (error) {
51
+ print(jsonEncode({'ok': false, 'error': error.runtimeType.toString()}));
52
+ exitCode = 7;
53
+ } finally {
54
+ await socket?.close();
55
+ client.close(force: true);
56
+ await server.close(force: true);
57
+ }
58
+ }
@@ -0,0 +1,73 @@
1
+ """The rendered Flutter test step keeps external proxying while its owned sockets stay local."""
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ import shlex
6
+ import subprocess
7
+ import tempfile
8
+ import unittest
9
+
10
+ import yaml
11
+
12
+ from test_jvm_proxy_relay import upstream
13
+
14
+ ROOT = Path(__file__).resolve().parents[2]
15
+ DART = os.environ.get("DART_SDK_BIN", "")
16
+ STEP = next(step for step in yaml.safe_load((ROOT / "android-action/action.yml").read_text())["runs"]["steps"]
17
+ if step["name"] == "Test Flutter app")
18
+ LOOPBACK = "env no_proxy=localhost,127.0.0.1,::1 NO_PROXY=localhost,127.0.0.1,::1 "
19
+
20
+
21
+ def run_step(step, *, without_exception=False):
22
+ """Run the real wrapper and rendered shell, substituting only a socket-owning Dart probe for Flutter."""
23
+ with tempfile.TemporaryDirectory() as directory, upstream() as (proxy, seen):
24
+ cwd = Path(directory)
25
+ (cwd / "test").mkdir()
26
+ (cwd / "test/probe_test.dart").write_text("// owned fixture\n")
27
+ subprocess.run(["git", "init", "-q", directory], check=True)
28
+ subprocess.run(["git", "add", "test"], cwd=cwd, check=True)
29
+ probe = ROOT / "android-action/scripts/fixtures/flutter_test_loopback.dart"
30
+ flutter = cwd / "flutter"
31
+ flutter.write_text(f'#!/bin/sh\nexec {shlex.quote(DART)} --disable-dart-dev {shlex.quote(str(probe))} "$@"\n')
32
+ flutter.chmod(0o700)
33
+ script = cwd / "step.sh"
34
+ run = step["run"].replace(LOOPBACK, "") if without_exception else step["run"]
35
+ script.write_text(run)
36
+ command = step["shell"].replace("${{ github.action_path }}", str(ROOT / "android-action"))
37
+ command = shlex.split(command.replace("{0}", str(script)))
38
+ env = {**os.environ, "GOOGLE_STORE_PROXY_URL": proxy, "PATH": str(cwd) + os.pathsep + os.environ["PATH"],
39
+ "NO_PROXY": "*", "no_proxy": "*", "HTTPS_PROXY": "http://wrong.invalid:1"}
40
+ result = subprocess.run(command, cwd=cwd, env=env, capture_output=True, text=True, timeout=15)
41
+ return result, seen
42
+
43
+
44
+ class FlutterLoopbackTests(unittest.TestCase):
45
+ def test_exception_is_command_scoped_inside_the_validated_wrapper(self):
46
+ self.assertIn("play_store_proxy.py -- bash", STEP["shell"])
47
+ command = next(line.strip() for line in STEP["run"].splitlines() if line.strip().startswith("env "))
48
+ self.assertEqual(shlex.split(command), ["env", "no_proxy=localhost,127.0.0.1,::1",
49
+ "NO_PROXY=localhost,127.0.0.1,::1", "flutter", "test", "--no-pub"])
50
+ self.assertNotIn("env", STEP)
51
+ self.assertNotRegex(STEP["run"], r"(?:unset|env\s+-u)")
52
+
53
+ @unittest.skipUnless(DART and Path(DART).is_file(), "set DART_SDK_BIN to an installed native Dart executable")
54
+ def test_real_dart_http_and_websocket_survive_while_external_http_uses_the_exit(self):
55
+ result, seen = run_step(STEP)
56
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
57
+ self.assertEqual(json.loads(result.stdout),
58
+ {"ok": True, "loopback": ["http", "websocket"], "external": "assigned_proxy"})
59
+ self.assertEqual([(method, url) for method, url, _auth in seen],
60
+ [("GET", "http://unresolvable.invalid/pub")])
61
+ self.assertNotIn("fixture-pass", result.stdout + result.stderr)
62
+
63
+ @unittest.skipUnless(DART and Path(DART).is_file(), "set DART_SDK_BIN to an installed native Dart executable")
64
+ def test_original_empty_bypass_routes_the_websocket_to_the_exit_and_fails(self):
65
+ result, seen = run_step(STEP, without_exception=True)
66
+ self.assertEqual(result.returncode, 7, result.stdout + result.stderr)
67
+ self.assertEqual(json.loads(result.stdout), {"ok": False, "error": "WebSocketException"})
68
+ self.assertEqual(len(seen), 1)
69
+ self.assertRegex(seen[0][1], r"^http://127\.0\.0\.1:\d+/socket$")
70
+
71
+
72
+ if __name__ == "__main__":
73
+ unittest.main()
@@ -1 +1 @@
1
- 1.0.68
1
+ 1.0.70
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.68",
3
+ "version": "1.0.70",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,6 +34,8 @@ jobs:
34
34
  shell: bash
35
35
  run: python3 .github/actions/android-app/scripts/play_store_proxy.py --bootstrap-env
36
36
  - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
37
+ env:
38
+ SEGMENT_DOWNLOAD_TIMEOUT_MINS: "1"
37
39
  with:
38
40
  channel: stable
39
41
  cache: true
@@ -84,6 +84,9 @@ jobs:
84
84
  - name: Set up Flutter
85
85
  if: ${{ steps.flutter.outputs.enabled == 'true' }}
86
86
  uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
87
+ env:
88
+ # Optional SDK/pub cache segments must finish promptly; normal SDK setup still fails on errors.
89
+ SEGMENT_DOWNLOAD_TIMEOUT_MINS: "1"
87
90
  with:
88
91
  channel: stable
89
92
  cache: true
@@ -339,6 +342,8 @@ jobs:
339
342
  - name: Set up Flutter
340
343
  if: ${{ steps.mode.outputs.mode == 'local' && steps.flutter.outputs.enabled == 'true' }}
341
344
  uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
345
+ env:
346
+ SEGMENT_DOWNLOAD_TIMEOUT_MINS: "1"
342
347
  with:
343
348
  channel: stable
344
349
  cache: true