gowalk-cicd 1.0.62 → 1.0.63

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
@@ -123,7 +123,9 @@ node /path/to/gowalk-cicd/bin/cli.mjs
123
123
  `certificate-cap-policy` accepts only `fail`; a full cap requires registry reconciliation.
124
124
  - Keep preinstalled Android SDK/NDKs. Every Gradle test/build and SDK download runs
125
125
  under `play_store_proxy.py --`, whose owned loopback relay gives Java an explicit
126
- authenticated account exit without credentials in JVM options. HTTPS is tunneled
126
+ authenticated account exit without credentials in JVM options. The child HTTP_PROXY
127
+ and HTTPS_PROXY variables also name the credential-free loopback relay because
128
+ Crashlytics Buildtools reads them first and rejects authenticated proxy URLs. HTTPS is tunneled
127
129
  without TLS interception; no upstream failure may fall back to the destination.
128
130
  The wrapper disables Gradle daemons and closes its relay when the command ends.
129
131
  - Backend runtime credentials use the single encrypted `BACKEND_RUNTIME_ENV`
@@ -1 +1 @@
1
- 1.0.62
1
+ 1.0.63
@@ -1 +1 @@
1
- 1.0.62
1
+ 1.0.63
@@ -27,12 +27,11 @@ from __future__ import annotations
27
27
  import argparse
28
28
  import os
29
29
  import re
30
- import subprocess
31
30
  import sys
32
31
  import tempfile
33
32
  from pathlib import Path
34
33
 
35
- from play_store_proxy import environment
34
+ from play_store_proxy import run as run_proxied
36
35
 
37
36
  # Exact pin, not a range: this job holds the upload keystore and the Play
38
37
  # service account, so no floating third-party code runs in it. Bump on purpose.
@@ -99,10 +98,9 @@ def _default_run(cmd: list[str]) -> int:
99
98
  # A scratch cwd: the Crashlytics buildtools drop a .crashlytics/ directory
100
99
  # (dump_syms.bin, ~4 MB) into the working directory, which must not be the
101
100
  # consumer's checkout.
102
- env = environment()
103
101
  try:
104
102
  with tempfile.TemporaryDirectory(prefix="crashlytics-upload-") as workdir:
105
- return subprocess.run(cmd, check=False, cwd=workdir, env=env).returncode
103
+ return run_proxied(cmd, cwd=workdir)
106
104
  except FileNotFoundError:
107
105
  print(
108
106
  "::error::npx is not on PATH, so the Firebase CLI cannot run. "
@@ -161,7 +159,7 @@ def main(argv: list[str] | None = None, environ: dict[str, str] | None = None, r
161
159
  print(f"::error::no Dart symbol files in {args.symbols_dir}; nothing to upload to Crashlytics")
162
160
  return 1
163
161
 
164
- cmd = upload_command(app_id, args.symbols_dir, args.firebase_tools)
162
+ cmd = upload_command(app_id, args.symbols_dir.resolve(), args.firebase_tools)
165
163
  print(f"Uploading {len(symbols)} Dart symbol file(s) to Crashlytics app {app_id}:")
166
164
  print(" " + " ".join(cmd))
167
165
  rc = run(cmd)
@@ -109,6 +109,13 @@ def running(proxy: str):
109
109
 
110
110
 
111
111
  def environment(env: dict, port: int) -> dict:
112
+ # Crashlytics Buildtools prefers HTTP_PROXY to JVM options and rejects URLs
113
+ # with userinfo. All child clients use the same owned relay; authentication
114
+ # is added only on its connection to the configured account exit.
115
+ local = f"http://127.0.0.1:{port}"
116
+ for name in ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"):
117
+ env[name] = local
118
+ env.update(NO_PROXY="", no_proxy="")
112
119
  options = (f"-Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort={port} "
113
120
  f"-Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort={port} "
114
121
  "-Dhttp.nonProxyHosts= -Dhttps.nonProxyHosts= -Djava.net.useSystemProxies=false")
@@ -4,7 +4,7 @@ Three entry points, all from the one validated ``GOOGLE_STORE_PROXY_URL``:
4
4
 
5
5
  * ``python3 play_store_proxy.py`` refuses a missing or malformed proxy.
6
6
  * ``python3 play_store_proxy.py -- <command …>`` runs a build tool with the explicit
7
- proxy in both spellings and both ``NO_PROXY`` spellings cleared. Setting the pairs
7
+ owned relay in both proxy spellings and both ``NO_PROXY`` spellings cleared. Setting the pairs
8
8
  as YAML ``env`` keys is not an option: GitHub compares env keys case-insensitively
9
9
  and rejects the workflow or action file as invalid.
10
10
  * ``python3 play_store_proxy.py --github-env`` clears the lower-case ambient bypass
@@ -49,7 +49,7 @@ def session() -> requests.Session:
49
49
  return client
50
50
 
51
51
 
52
- def run(argv: list[str]) -> int:
52
+ def run(argv: list[str], *, cwd: str | None = None) -> int:
53
53
  """Run ``argv`` with the explicit proxy environment; the parent process is untouched."""
54
54
  if not argv:
55
55
  raise SystemExit("usage: play_store_proxy.py -- <command …>")
@@ -57,7 +57,7 @@ def run(argv: list[str]) -> int:
57
57
 
58
58
  env = environment()
59
59
  with running(proxy_url()) as port:
60
- return subprocess.run(argv, env=jvm_environment(env, port), check=False).returncode
60
+ return subprocess.run(argv, env=jvm_environment(env, port), cwd=cwd, check=False).returncode
61
61
 
62
62
 
63
63
  def clear_github_env_bypass(path: str | None = None) -> list[str]:
@@ -130,7 +130,7 @@ class MainTest(unittest.TestCase):
130
130
  self.assertEqual(
131
131
  self.calls,
132
132
  [["npx", "--yes", cs.FIREBASE_TOOLS, "crashlytics:symbols:upload",
133
- f"--app={ANDROID}", str(self.symbols)]],
133
+ f"--app={ANDROID}", str(self.symbols.resolve())]],
134
134
  )
135
135
  self.assertIn("Uploaded", out)
136
136
  self.assertNotIn("::error::", out)
@@ -0,0 +1,52 @@
1
+ """Exercise the actual symbol-uploader child process and its owned proxy lifetime."""
2
+ import base64
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ import socket
7
+ import sys
8
+ import tempfile
9
+ import unittest
10
+ from unittest import mock
11
+
12
+ import crashlytics_symbols as symbols
13
+ from test_jvm_proxy_relay import upstream
14
+
15
+
16
+ class SymbolsProxyTests(unittest.TestCase):
17
+ def test_real_upload_child_uses_relay_and_removes_scratch_after_failure(self):
18
+ probe = """
19
+ import json, os, sys, urllib.request
20
+ from pathlib import Path
21
+ from urllib.parse import urlsplit
22
+ proxy = urlsplit(os.environ['HTTPS_PROXY'])
23
+ assert proxy.hostname == '127.0.0.1' and proxy.username is None
24
+ assert os.environ['NO_PROXY'] == os.environ['no_proxy'] == ''
25
+ reply = urllib.request.urlopen('http://unresolvable.invalid/symbols', timeout=5).read()
26
+ assert reply == b'through pinned proxy'
27
+ Path(sys.argv[1]).write_text(json.dumps({'cwd': os.getcwd(), 'port': proxy.port}))
28
+ sys.exit(7)
29
+ """
30
+ with tempfile.TemporaryDirectory() as directory, upstream() as (proxy, seen):
31
+ result_path = Path(directory) / "result.json"
32
+ with mock.patch.dict(os.environ, {"GOOGLE_STORE_PROXY_URL": proxy, "NO_PROXY": "*"}):
33
+ rc = symbols._default_run([sys.executable, "-c", probe, str(result_path)])
34
+ result = json.loads(result_path.read_text())
35
+ self.assertEqual(rc, 7)
36
+ self.assertFalse(Path(result["cwd"]).exists())
37
+ self.assertNotEqual(result["cwd"], os.getcwd())
38
+ with self.assertRaises(OSError):
39
+ socket.create_connection(("127.0.0.1", result["port"]), timeout=0.2)
40
+ auth = "Basic " + base64.b64encode(b"fixture-user:fixture-pass").decode()
41
+ self.assertEqual(seen, [("GET", "http://unresolvable.invalid/symbols", auth)])
42
+
43
+ def test_missing_proxy_refuses_before_spawning_the_uploader(self):
44
+ with mock.patch.dict(os.environ, {"GOOGLE_STORE_PROXY_URL": ""}), \
45
+ mock.patch("play_store_proxy.subprocess.run") as run:
46
+ with self.assertRaises(SystemExit):
47
+ symbols._default_run(["npx", "--yes", symbols.FIREBASE_TOOLS])
48
+ run.assert_not_called()
49
+
50
+
51
+ if __name__ == "__main__":
52
+ unittest.main()
@@ -49,6 +49,28 @@ def upstream():
49
49
 
50
50
 
51
51
  class JvmProxyTests(unittest.TestCase):
52
+ @unittest.skipUnless(shutil.which("java"), "Java runtime unavailable")
53
+ def test_environment_first_java_client_receives_a_credential_free_relay(self):
54
+ source = ('import java.net.*; public class Probe { public static void main(String[] args) '
55
+ 'throws Exception { var p = new URI(System.getenv("HTTPS_PROXY")); '
56
+ 'if(p.getUserInfo()!=null) throw new IllegalArgumentException("proxy userinfo unsupported"); '
57
+ 'var proxy = new Proxy(Proxy.Type.HTTP,new InetSocketAddress(p.getHost(),p.getPort())); '
58
+ 'var c = new URL("http://unresolvable.invalid/symbols").openConnection(proxy); '
59
+ 'c.setConnectTimeout(3000); c.setReadTimeout(3000); '
60
+ 'System.out.print(new String(c.getInputStream().readAllBytes())); }}')
61
+ with tempfile.TemporaryDirectory() as directory, upstream() as (proxy, seen):
62
+ target = Path(directory) / "Probe.java"
63
+ target.write_text(source)
64
+ with relay.running(proxy) as port:
65
+ env = relay.environment({**os.environ, "HTTPS_PROXY": proxy, "NO_PROXY": "*"}, port)
66
+ result = subprocess.run(["java", str(target)], env=env, capture_output=True, text=True, timeout=20)
67
+ self.assertEqual(result.returncode, 0, result.stderr)
68
+ self.assertEqual(result.stdout, "through pinned proxy")
69
+ auth = "Basic " + base64.b64encode(b"fixture-user:fixture-pass").decode()
70
+ self.assertEqual(seen, [("GET", "http://unresolvable.invalid/symbols", auth)])
71
+ self.assertNotIn("fixture-pass", result.stderr)
72
+ self.assertEqual(env["NO_PROXY"], "")
73
+
52
74
  def test_http_and_connect_only_reach_the_authenticated_upstream_and_cleanup(self):
53
75
  with upstream() as (proxy, seen):
54
76
  with relay.running(proxy) as port:
@@ -44,8 +44,9 @@ class PlayProxyTests(unittest.TestCase):
44
44
  run.return_value.returncode = 0
45
45
  self.assertEqual(play_store_proxy.run([sys.executable, "-c", probe]), 0)
46
46
  child = run.call_args.kwargs["env"]
47
- self.assertEqual(child["https_proxy"], value)
48
- self.assertEqual(child["HTTP_PROXY"], value)
47
+ self.assertTrue(child["https_proxy"].startswith("http://127.0.0.1:"))
48
+ self.assertEqual(child["HTTP_PROXY"], child["https_proxy"])
49
+ self.assertEqual(child["GOOGLE_STORE_PROXY_URL"], value)
49
50
  self.assertEqual(child["no_proxy"], "")
50
51
  self.assertEqual(child["NO_PROXY"], "")
51
52
  self.assertEqual(os.environ["no_proxy"], "*")
@@ -1 +1 @@
1
- 1.0.62
1
+ 1.0.63
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.62",
3
+ "version": "1.0.63",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {