gowalk-cicd 1.0.61 → 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 +8 -2
- package/README.md +13 -0
- package/action/.daemux-version +1 -1
- package/action/scripts/test_upload_build.py +2 -3
- package/action/scripts/test_upload_checksums.py +74 -0
- package/action/scripts/upload_build.py +11 -6
- package/action/scripts/upload_checksums.py +24 -0
- package/android-action/.daemux-version +1 -1
- package/android-action/scripts/crashlytics_symbols.py +3 -5
- package/android-action/scripts/jvm_proxy_relay.py +7 -0
- package/android-action/scripts/play_store_proxy.py +3 -3
- package/android-action/scripts/test_crashlytics_symbols.py +1 -1
- package/android-action/scripts/test_crashlytics_symbols_proxy.py +52 -0
- package/android-action/scripts/test_jvm_proxy_relay.py +22 -0
- package/android-action/scripts/test_play_store_proxy.py +3 -2
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -108,7 +108,11 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
108
108
|
Python clients enforce explicit proxies; Play upload steps set the proxy for the
|
|
109
109
|
pinned Google client. Missing configuration must stop before a provider call.
|
|
110
110
|
Apple IPA uploads use `action/scripts/upload_build.py`, not native upload tools.
|
|
111
|
-
It retains provider IDs/
|
|
111
|
+
It retains provider IDs/local SHA-256 and verifies matching IPA identity before resume.
|
|
112
|
+
Finalization sends `uploaded:true` as Apple's documented BuildUpload flow does; the
|
|
113
|
+
IPA endpoint rejected the optional SHA_256 checksum attribute despite the generic
|
|
114
|
+
schema advertising it. Completed-file reuse requires the content-derived filename
|
|
115
|
+
and size; any checksum returned by Apple is verified against the local file.
|
|
112
116
|
- Signing material should use encrypted Actions secrets. `deploy.yml`
|
|
113
117
|
materializes `ASC_KEY_P8` + its ID/issuer, the Apple distribution identity
|
|
114
118
|
(`IOS_DISTRIBUTION_P12_BASE64`, `IOS_DISTRIBUTION_CERT_META_BASE64`,
|
|
@@ -119,7 +123,9 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
119
123
|
`certificate-cap-policy` accepts only `fail`; a full cap requires registry reconciliation.
|
|
120
124
|
- Keep preinstalled Android SDK/NDKs. Every Gradle test/build and SDK download runs
|
|
121
125
|
under `play_store_proxy.py --`, whose owned loopback relay gives Java an explicit
|
|
122
|
-
authenticated account exit without credentials in JVM options.
|
|
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
|
|
123
129
|
without TLS interception; no upstream failure may fall back to the destination.
|
|
124
130
|
The wrapper disables Gradle daemons and closes its relay when the command ends.
|
|
125
131
|
- Backend runtime credentials use the single encrypted `BACKEND_RUNTIME_ENV`
|
package/README.md
CHANGED
|
@@ -962,3 +962,16 @@ even that first auto-bootstrap.
|
|
|
962
962
|
## License
|
|
963
963
|
|
|
964
964
|
MIT
|
|
965
|
+
|
|
966
|
+
### Recovering an Apple REST upload failure
|
|
967
|
+
|
|
968
|
+
The uploader retains the local SHA-256, version and provider upload/file IDs. It commits
|
|
969
|
+
completed part transfers with `uploaded: true`, following Apple's
|
|
970
|
+
[BuildUpload walkthrough](https://developer.apple.com/videos/play/wwdc2025/324/).
|
|
971
|
+
The IPA endpoint rejected an optional `sourceFileChecksums` SHA_256 object even though
|
|
972
|
+
the generic schema lists that algorithm; the uploader no longer sends that attribute.
|
|
973
|
+
A completed file is reused only when its content-derived filename and size match, and
|
|
974
|
+
any returned file checksum is verified. A failed upload emits the typed
|
|
975
|
+
`apple_build_upload_failed` error annotation with schema
|
|
976
|
+
`gowalk-cicd/apple-build-upload-failed.v1`; retain the exact IPA/receipt and read back
|
|
977
|
+
the provider state before retrying. All requests and parts still use the Apple proxy.
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.63
|
|
@@ -62,7 +62,7 @@ class BuildUploadTests(unittest.TestCase):
|
|
|
62
62
|
api.assert_not_called()
|
|
63
63
|
transfer.assert_not_called()
|
|
64
64
|
|
|
65
|
-
def
|
|
65
|
+
def test_parts_are_joined_before_committing_the_uploaded_file(self):
|
|
66
66
|
file = {"id": "file", "attributes": {"uploadOperations": [{"offset": 0, "length": 2},
|
|
67
67
|
{"offset": 2, "length": 3}]}}
|
|
68
68
|
done = []
|
|
@@ -71,8 +71,7 @@ class BuildUploadTests(unittest.TestCase):
|
|
|
71
71
|
upload_build.transfer_all(Path("unused"), file, {"fileSize": 5, "sha256": "digest"})
|
|
72
72
|
self.assertEqual(sorted(done), [0, 2])
|
|
73
73
|
body = api.call_args.args[2]["data"]["attributes"]
|
|
74
|
-
self.assertEqual(body, {"uploaded": True
|
|
75
|
-
"file": {"hash": "digest", "algorithm": "SHA_256"}}})
|
|
74
|
+
self.assertEqual(body, {"uploaded": True})
|
|
76
75
|
|
|
77
76
|
def test_failed_part_never_commits_the_file(self):
|
|
78
77
|
file = {"id": "file", "attributes": {"uploadOperations": [{"offset": 0, "length": 5}]}}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Completed-file recovery keeps byte identity without inventing optional provider attributes."""
|
|
2
|
+
import base64
|
|
3
|
+
from contextlib import redirect_stdout
|
|
4
|
+
import hashlib
|
|
5
|
+
import io
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import unittest
|
|
10
|
+
from unittest import mock
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
13
|
+
import upload_build
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CompletedUploadTests(unittest.TestCase):
|
|
17
|
+
def test_provider_exit_emits_typed_failure_without_exception_content(self):
|
|
18
|
+
output = io.StringIO()
|
|
19
|
+
with tempfile.TemporaryDirectory() as temporary, redirect_stdout(output), \
|
|
20
|
+
mock.patch.object(sys, "argv", ["upload_build.py", "app.ipa", "--report", temporary + "/receipt.json"]), \
|
|
21
|
+
mock.patch.object(upload_build, "deliver", side_effect=SystemExit("private-provider-detail")):
|
|
22
|
+
with self.assertRaisesRegex(SystemExit, "retain its receipt"):
|
|
23
|
+
upload_build.main()
|
|
24
|
+
self.assertIn("::error title=apple_build_upload_failed::", output.getvalue())
|
|
25
|
+
self.assertNotIn("private-provider-detail", output.getvalue())
|
|
26
|
+
|
|
27
|
+
def test_provider_completion_without_optional_checksum_reuses_exact_file(self):
|
|
28
|
+
info = {"fileName": "source-sha256.ipa", "fileSize": 5}
|
|
29
|
+
file = {"attributes": {**info, "assetDeliveryState": {"state": "COMPLETE"}}}
|
|
30
|
+
with mock.patch.object(upload_build, "api") as api, mock.patch.object(upload_build, "transfer") as transfer:
|
|
31
|
+
upload_build.transfer_all(Path("not-read"), file, info)
|
|
32
|
+
api.assert_not_called()
|
|
33
|
+
transfer.assert_not_called()
|
|
34
|
+
file["attributes"]["fileName"] = "another-source.ipa"
|
|
35
|
+
with self.assertRaisesRegex(ValueError, "identity"):
|
|
36
|
+
upload_build.transfer_all(Path("not-read"), file, info)
|
|
37
|
+
|
|
38
|
+
def test_present_provider_checksums_are_verified_against_actual_bytes(self):
|
|
39
|
+
with tempfile.TemporaryDirectory() as temporary:
|
|
40
|
+
ipa = Path(temporary) / "app.ipa"
|
|
41
|
+
ipa.write_bytes(b"the uploaded file")
|
|
42
|
+
info = {"fileName": "source-sha256.ipa", "fileSize": ipa.stat().st_size}
|
|
43
|
+
for algorithm, digest in [("SHA_256", hashlib.sha256(ipa.read_bytes())),
|
|
44
|
+
("MD5", hashlib.md5(ipa.read_bytes()))]:
|
|
45
|
+
for value in [digest.hexdigest(), base64.b64encode(digest.digest()).decode()]:
|
|
46
|
+
with self.subTest(algorithm=algorithm, encoding=value):
|
|
47
|
+
attributes = {**info, "assetDeliveryState": {"state": "COMPLETE"},
|
|
48
|
+
"sourceFileChecksums": {"file": {"hash": value, "algorithm": algorithm}}}
|
|
49
|
+
upload_build.transfer_all(ipa, {"attributes": attributes}, info)
|
|
50
|
+
attributes["sourceFileChecksums"]["file"]["hash"] = "different-bytes"
|
|
51
|
+
with self.assertRaisesRegex(ValueError, "checksum"):
|
|
52
|
+
upload_build.transfer_all(ipa, {"attributes": attributes}, info)
|
|
53
|
+
|
|
54
|
+
def test_commit_uses_documented_uploaded_attribute_after_all_bytes_finish(self):
|
|
55
|
+
# Apple's WWDC25 BuildUpload sample sends uploaded=true. The observed IPA
|
|
56
|
+
# endpoint rejected the optional SHA_256 sourceFileChecksums object with409.
|
|
57
|
+
parts = [{"offset": 0, "length": 2}, {"offset": 2, "length": 3}]
|
|
58
|
+
done = []
|
|
59
|
+
def provider(method, path, body):
|
|
60
|
+
self.assertEqual(sorted(done), [0, 2])
|
|
61
|
+
attributes = body["data"]["attributes"]
|
|
62
|
+
if "sourceFileChecksums" in attributes:
|
|
63
|
+
raise SystemExit("409 ENTITY_ERROR.ATTRIBUTE.INVALID")
|
|
64
|
+
self.assertTrue(attributes["uploaded"])
|
|
65
|
+
return {"data": {"id": "existing-file"}}
|
|
66
|
+
with mock.patch.object(upload_build, "api", side_effect=provider) as api, \
|
|
67
|
+
mock.patch.object(upload_build, "transfer", side_effect=lambda _path, op: done.append(op["offset"])):
|
|
68
|
+
upload_build.transfer_all(Path("not-read"), {"id": "existing-file", "attributes": {
|
|
69
|
+
"uploadOperations": parts}}, {"fileSize": 5, "sha256": "local-receipt-digest"})
|
|
70
|
+
self.assertEqual(api.call_count, 1)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
unittest.main()
|
|
@@ -15,6 +15,7 @@ import zipfile
|
|
|
15
15
|
|
|
16
16
|
import asc_common
|
|
17
17
|
import apple_store_proxy as store_proxy
|
|
18
|
+
import upload_checksums
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def api(method: str, path: str, body=None, params=None) -> dict:
|
|
@@ -96,9 +97,7 @@ def transfer_all(ipa: Path, file: dict, info: dict) -> None:
|
|
|
96
97
|
attributes = file.get("attributes") or {}
|
|
97
98
|
state = (attributes.get("assetDeliveryState") or {}).get("state")
|
|
98
99
|
if state == "COMPLETE":
|
|
99
|
-
|
|
100
|
-
if (attributes.get("sourceFileChecksums") or {}).get("file") != expected:
|
|
101
|
-
raise ValueError("Apple's completed file checksum does not match this IPA")
|
|
100
|
+
upload_checksums.completed(ipa, attributes, info)
|
|
102
101
|
return
|
|
103
102
|
operations = sorted(attributes.get("uploadOperations") or [], key=lambda row: row["offset"])
|
|
104
103
|
offset = 0
|
|
@@ -113,8 +112,10 @@ def transfer_all(ipa: Path, file: dict, info: dict) -> None:
|
|
|
113
112
|
for result in results:
|
|
114
113
|
result.result()
|
|
115
114
|
api("PATCH", f"/buildUploadFiles/{file['id']}", {"data": {
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
# Apple's build-upload contract commits with uploaded=true. Its generic
|
|
116
|
+
# checksum enum includes SHA_256, but IPA finalization rejected that
|
|
117
|
+
# optional attribute with ENTITY_ERROR.ATTRIBUTE.INVALID in production.
|
|
118
|
+
"type": "buildUploadFiles", "id": file["id"], "attributes": {"uploaded": True},
|
|
118
119
|
}})
|
|
119
120
|
|
|
120
121
|
|
|
@@ -154,7 +155,11 @@ def main() -> None:
|
|
|
154
155
|
args = parser.parse_args()
|
|
155
156
|
try:
|
|
156
157
|
receipt = deliver(args.ipa, args.report, args.timeout)
|
|
157
|
-
except (OSError, ValueError, KeyError, RuntimeError):
|
|
158
|
+
except (OSError, ValueError, KeyError, RuntimeError, SystemExit):
|
|
159
|
+
print('::error title=apple_build_upload_failed::' + json.dumps({
|
|
160
|
+
"schema": "gowalk-cicd/apple-build-upload-failed.v1",
|
|
161
|
+
"receipt_available": args.report.is_file(),
|
|
162
|
+
"action": "retain the IPA and receipt; read provider upload state before retrying"}))
|
|
158
163
|
message = "Apple REST upload did not complete; retain its receipt and resolve the provider state"
|
|
159
164
|
raise SystemExit(message) from None
|
|
160
165
|
print(json.dumps(receipt))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Check completed upload identity without requiring Apple's optional checksum attribute."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import hashlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def completed(ipa: Path, attributes: dict, info: dict) -> None:
|
|
10
|
+
if any(attributes.get(key) != info[key] for key in ("fileName", "fileSize")):
|
|
11
|
+
raise ValueError("Apple's completed file identity does not match this IPA")
|
|
12
|
+
checksum = (attributes.get("sourceFileChecksums") or {}).get("file")
|
|
13
|
+
if checksum is None:
|
|
14
|
+
# The name is the local SHA-256 plus .ipa; upload_file verified this same
|
|
15
|
+
# provider-owned file identity before any transfer or completed-file reuse.
|
|
16
|
+
return
|
|
17
|
+
algorithm = {"SHA_256": "sha256", "MD5": "md5"}.get(checksum.get("algorithm"))
|
|
18
|
+
if algorithm is None:
|
|
19
|
+
raise ValueError("Apple returned an unsupported completed-file checksum algorithm")
|
|
20
|
+
with ipa.open("rb") as stream:
|
|
21
|
+
digest = hashlib.file_digest(stream, algorithm)
|
|
22
|
+
expected = {digest.hexdigest(), digest.hexdigest().upper(), base64.b64encode(digest.digest()).decode()}
|
|
23
|
+
if checksum.get("hash") not in expected:
|
|
24
|
+
raise ValueError("Apple's completed file checksum does not match this IPA")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
48
|
-
self.assertEqual(child["HTTP_PROXY"],
|
|
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.
|
|
1
|
+
1.0.63
|