gowalk-cicd 1.0.133 → 1.0.135
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 +3 -0
- package/README.md +4 -0
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/android-action/play-upload/run.cjs +32 -1
- package/android-action/scripts/jvm_proxy_relay.py +3 -2
- package/android-action/scripts/test_jvm_proxy_loopback.py +91 -0
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -142,6 +142,9 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
142
142
|
and HTTPS_PROXY variables also name the credential-free loopback relay because
|
|
143
143
|
Crashlytics Buildtools reads them first and rejects authenticated proxy URLs. HTTPS is tunneled
|
|
144
144
|
without TLS interception; no upstream failure may fall back to the destination.
|
|
145
|
+
Java bypasses the relay only for exact `localhost`, `127.0.0.1` and `[::1]`
|
|
146
|
+
targets so owned HTTP test fixtures remain local. Never widen that host list;
|
|
147
|
+
external requests and environment-first clients retain the assigned relay.
|
|
145
148
|
The wrapper disables Gradle daemons and closes its relay when the command ends.
|
|
146
149
|
The relay's idle deadline covers the whole tunnel: traffic in either direction
|
|
147
150
|
keeps downloads/uploads alive. An idle direction alone must never end a transfer.
|
package/README.md
CHANGED
|
@@ -517,6 +517,10 @@ The relay authenticates to that exit without putting credentials in JVM options;
|
|
|
517
517
|
HTTPS remains an end-to-end TLS tunnel. An unavailable proxy fails the request,
|
|
518
518
|
and the relay shuts down with its child command. Gradle daemons are disabled for
|
|
519
519
|
these commands so they cannot retain an expired relay port.
|
|
520
|
+
Java requests to exact `localhost`, `127.0.0.1` and `[::1]` targets stay local for
|
|
521
|
+
HTTP test fixtures. All other Java hosts, including lookalike names such as
|
|
522
|
+
`localhost.example.com`, retain the assigned proxy; environment-first clients
|
|
523
|
+
continue to use the relay.
|
|
520
524
|
|
|
521
525
|
### Private git dependencies (Flutter)
|
|
522
526
|
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.135
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.135
|
|
@@ -32,6 +32,21 @@ async function upload(program, env, directory, execute = processRunner.execute,
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
//: A code reaches the annotation only as a bare token: it is interpolated into
|
|
36
|
+
//: workflow-command syntax, so anything outside this shape is reported as such
|
|
37
|
+
//: rather than trusted.
|
|
38
|
+
function state(code) {
|
|
39
|
+
return typeof code === 'string' && /^[a-z0-9_]{1,64}$/.test(code) ? code : 'unknown';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stateAnnotation(receipt) {
|
|
43
|
+
return `::${receipt.ok === true ? 'notice' : 'error'} title=play_upload_state::`
|
|
44
|
+
+ `gowalk-cicd/play-upload-state.v1 ok=${receipt.ok === true}`
|
|
45
|
+
+ ` code=${state(receipt.code)} review_pending=${receipt.review_pending === true}`
|
|
46
|
+
+ ` attempts=${Number.isSafeInteger(receipt.attempts) ? receipt.attempts : 'unknown'}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
35
50
|
async function main() {
|
|
36
51
|
let directory, receipt;
|
|
37
52
|
try {
|
|
@@ -66,10 +81,26 @@ async function main() {
|
|
|
66
81
|
appendFileSync(process.env.GITHUB_OUTPUT, `review-pending=${receipt.review_pending}\nreceipt=${encoded}\n`);
|
|
67
82
|
}
|
|
68
83
|
console.log(`::${receipt.ok ? 'notice' : 'error'} title=play_upload_result::${encoded}`);
|
|
84
|
+
// The JSON receipt above does not survive as a READABLE annotation on this job.
|
|
85
|
+
// `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON` is a multi-line JSON secret, so Actions
|
|
86
|
+
// registers each of its lines as maskable — and its first and last lines are a
|
|
87
|
+
// bare `{` and `}`. Every brace in this job's output is therefore replaced by
|
|
88
|
+
// `***`, leaving the payload legible to a person but not parseable by the
|
|
89
|
+
// panel, which reads typed check-run annotations and is forbidden the log text.
|
|
90
|
+
// The iOS job emits the same JSON shape unmasked, which is what isolates the
|
|
91
|
+
// cause to this job's secret rather than to redaction policy in general.
|
|
92
|
+
//
|
|
93
|
+
// So the decision-relevant fields also go out brace-free, in the same
|
|
94
|
+
// key=value shape as `play_upload_diagnostic` — which is emitted from this
|
|
95
|
+
// very action and arrives unmasked. `review_pending` is the field that
|
|
96
|
+
// distinguishes an upload from a submission, and losing it is how a green
|
|
97
|
+
// Android job reads as "Play is done" when the AAB sits unreviewed on a
|
|
98
|
+
// test track.
|
|
99
|
+
console.log(stateAnnotation(receipt));
|
|
69
100
|
if (!receipt.ok) process.exitCode = 1;
|
|
70
101
|
}
|
|
71
102
|
|
|
72
|
-
module.exports = {upload, main};
|
|
103
|
+
module.exports = {upload, main, state, stateAnnotation};
|
|
73
104
|
if (require.main === module) void main().catch(() => {
|
|
74
105
|
process.stderr.write('play_upload_receipt_failed\n'); process.exitCode = 1;
|
|
75
106
|
});
|
|
@@ -149,7 +149,7 @@ def running(proxy: str):
|
|
|
149
149
|
|
|
150
150
|
def environment(env: dict, port: int) -> dict:
|
|
151
151
|
# Crashlytics Buildtools prefers HTTP_PROXY to JVM options and rejects URLs
|
|
152
|
-
# with userinfo.
|
|
152
|
+
# with userinfo. External child requests use the same owned relay; authentication
|
|
153
153
|
# is added only on its connection to the configured account exit.
|
|
154
154
|
local = f"http://127.0.0.1:{port}"
|
|
155
155
|
for name in ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"):
|
|
@@ -157,7 +157,8 @@ def environment(env: dict, port: int) -> dict:
|
|
|
157
157
|
env.update(NO_PROXY="", no_proxy="")
|
|
158
158
|
options = (f"-Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort={port} "
|
|
159
159
|
f"-Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort={port} "
|
|
160
|
-
"-Dhttp.nonProxyHosts=
|
|
160
|
+
"-Dhttp.nonProxyHosts=localhost|127.0.0.1|[::1] "
|
|
161
|
+
"-Dhttps.nonProxyHosts=localhost|127.0.0.1|[::1] -Djava.net.useSystemProxies=false")
|
|
161
162
|
# Java prints these options at startup; they contain no proxy credentials.
|
|
162
163
|
for name in ("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS", "GRADLE_OPTS"):
|
|
163
164
|
env[name] = (env.get(name, "") + " " + options).strip()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Local HTTP fixtures stay local while every external Java request keeps the assigned relay."""
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
import socket
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
import threading
|
|
11
|
+
import unittest
|
|
12
|
+
|
|
13
|
+
import jvm_proxy_relay as relay
|
|
14
|
+
from test_jvm_proxy_relay import upstream
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@contextmanager
|
|
18
|
+
def fixture(ipv6=False):
|
|
19
|
+
seen = []
|
|
20
|
+
|
|
21
|
+
class Handler(BaseHTTPRequestHandler):
|
|
22
|
+
def do_GET(self):
|
|
23
|
+
seen.append(self.path)
|
|
24
|
+
self.send_response(200)
|
|
25
|
+
self.end_headers()
|
|
26
|
+
self.wfile.write(b"owned local fixture")
|
|
27
|
+
|
|
28
|
+
def log_message(self, *_):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
class Server(ThreadingHTTPServer):
|
|
32
|
+
address_family = socket.AF_INET6 if ipv6 else socket.AF_INET
|
|
33
|
+
|
|
34
|
+
server = Server(("::1" if ipv6 else "127.0.0.1", 0), Handler)
|
|
35
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
36
|
+
thread.start()
|
|
37
|
+
try:
|
|
38
|
+
yield server.server_port, seen
|
|
39
|
+
finally:
|
|
40
|
+
server.shutdown()
|
|
41
|
+
server.server_close()
|
|
42
|
+
thread.join(timeout=5)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def java_probe(port, urls):
|
|
46
|
+
source = '''import java.net.*;
|
|
47
|
+
public class Probe {
|
|
48
|
+
public static void main(String[] args) throws Exception {
|
|
49
|
+
for (var url : args) {
|
|
50
|
+
var connection = URI.create(url).toURL().openConnection();
|
|
51
|
+
connection.setConnectTimeout(3000); connection.setReadTimeout(3000);
|
|
52
|
+
System.out.println(new String(connection.getInputStream().readAllBytes()));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}'''
|
|
56
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
57
|
+
path = Path(directory) / "Probe.java"
|
|
58
|
+
path.write_text(source)
|
|
59
|
+
env = relay.environment({**os.environ, "JAVA_TOOL_OPTIONS": "-Dhttp.nonProxyHosts=*"}, port)
|
|
60
|
+
return subprocess.run(["java", str(path), *urls], env=env, capture_output=True, text=True, timeout=20)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@unittest.skipUnless(shutil.which("java"), "Java runtime unavailable")
|
|
64
|
+
class LoopbackTests(unittest.TestCase):
|
|
65
|
+
def test_ipv4_and_localhost_fixtures_bypass_only_the_owned_relay(self):
|
|
66
|
+
with fixture() as (local, requests), upstream() as (proxy, seen), relay.running(proxy) as port:
|
|
67
|
+
result = java_probe(port, [f"http://127.0.0.1:{local}/literal", f"http://localhost:{local}/hostname",
|
|
68
|
+
"http://unresolvable.invalid/provider", "http://localhost.invalid/provider"])
|
|
69
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
70
|
+
self.assertEqual(result.stdout.splitlines(), ["owned local fixture", "owned local fixture",
|
|
71
|
+
"through pinned proxy", "through pinned proxy"])
|
|
72
|
+
self.assertEqual(requests, ["/literal", "/hostname"])
|
|
73
|
+
self.assertEqual([row[1] for row in seen], ["http://unresolvable.invalid/provider",
|
|
74
|
+
"http://localhost.invalid/provider"])
|
|
75
|
+
self.assertTrue(all(row[2].startswith("Basic ") for row in seen))
|
|
76
|
+
self.assertNotIn("fixture-pass", result.stderr)
|
|
77
|
+
|
|
78
|
+
def test_ipv6_loopback_fixture_remains_local(self):
|
|
79
|
+
try:
|
|
80
|
+
with fixture(ipv6=True) as (local, requests), upstream() as (proxy, seen), relay.running(proxy) as port:
|
|
81
|
+
result = java_probe(port, [f"http://[::1]:{local}/ipv6", "http://unresolvable.invalid/provider"])
|
|
82
|
+
except OSError as exc:
|
|
83
|
+
self.skipTest(f"IPv6 fixture unavailable: {exc.errno}")
|
|
84
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
85
|
+
self.assertEqual(result.stdout.splitlines(), ["owned local fixture", "through pinned proxy"])
|
|
86
|
+
self.assertEqual(requests, ["/ipv6"])
|
|
87
|
+
self.assertEqual([row[1] for row in seen], ["http://unresolvable.invalid/provider"])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
unittest.main()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.135
|