gowalk-cicd 1.0.72 → 1.0.73
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 +4 -0
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/android-action/flutter-setup/action.yml +10 -0
- package/android-action/scripts/flutter_manifest.py +76 -0
- package/android-action/scripts/flutter_manifest_env.sh +9 -0
- package/android-action/scripts/flutter_manifest_fixture.py +85 -0
- package/android-action/scripts/test_flutter_manifest.py +119 -0
- package/android-action/scripts/test_flutter_manifest_scope.py +41 -0
- package/android-action/scripts/test_google_bootstrap_wiring.py +9 -1
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -145,6 +145,10 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
145
145
|
`NO_PROXY`. The preloader snapshot under `RUNNER_TEMP` also covers official post/save after
|
|
146
146
|
consumer autoupdate changes the action directory. The installer delivers this development
|
|
147
147
|
tooling for native iOS repositories too; it does not require an Android app.
|
|
148
|
+
- Flutter manifest GET recovery is scoped to the two pinned setup invocations through `BASH_ENV`.
|
|
149
|
+
It retains the account proxy, retries transient TLS/transport errors within 60 seconds, and emits
|
|
150
|
+
only complete JSON. Partial responses and private curl stderr never enter action outputs;
|
|
151
|
+
authentication/certificate failures remain terminal. SDK archive calls retain upstream behavior.
|
|
148
152
|
- Flutter setup scopes `SEGMENT_DOWNLOAD_TIMEOUT_MINS=1` to the composite action's optional
|
|
149
153
|
SDK/pub cache restores. This bounds each cache segment, not the whole cache or SDK download.
|
|
150
154
|
A cache timeout becomes a cache miss; normal proxied SDK setup remains mandatory and its
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.73
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.73
|
|
@@ -84,9 +84,13 @@ runs:
|
|
|
84
84
|
using: composite
|
|
85
85
|
steps:
|
|
86
86
|
- name: Require provider transport
|
|
87
|
+
id: transport
|
|
87
88
|
shell: bash
|
|
88
89
|
run: |
|
|
89
90
|
python3 "$GITHUB_ACTION_PATH/../scripts/play_store_proxy.py" --bootstrap-env
|
|
91
|
+
printf 'manifest_env=%s\nmanifest_helper=%s\n' \
|
|
92
|
+
"$GITHUB_ACTION_PATH/../scripts/flutter_manifest_env.sh" \
|
|
93
|
+
"$GITHUB_ACTION_PATH/../scripts/flutter_manifest.py" >> "$GITHUB_OUTPUT"
|
|
90
94
|
- name: Retain the cache process scope
|
|
91
95
|
id: scope
|
|
92
96
|
if: ${{ inputs.cache == 'true' || inputs.pub-cache == 'true' }}
|
|
@@ -96,6 +100,9 @@ runs:
|
|
|
96
100
|
- name: Resolve Flutter release and cache identities
|
|
97
101
|
id: flutter-action
|
|
98
102
|
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2
|
|
103
|
+
env:
|
|
104
|
+
BASH_ENV: ${{ steps.transport.outputs.manifest_env }}
|
|
105
|
+
GOWALK_FLUTTER_MANIFEST_HELPER: ${{ steps.transport.outputs.manifest_helper }}
|
|
99
106
|
with:
|
|
100
107
|
channel: ${{ inputs.channel }}
|
|
101
108
|
flutter-version: ${{ inputs.flutter-version }}
|
|
@@ -134,6 +141,9 @@ runs:
|
|
|
134
141
|
- name: Install or reuse the resolved Flutter SDK
|
|
135
142
|
if: ${{ inputs.dry-run != 'true' }}
|
|
136
143
|
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2
|
|
144
|
+
env:
|
|
145
|
+
BASH_ENV: ${{ steps.transport.outputs.manifest_env }}
|
|
146
|
+
GOWALK_FLUTTER_MANIFEST_HELPER: ${{ steps.transport.outputs.manifest_helper }}
|
|
137
147
|
with:
|
|
138
148
|
channel: ${{ steps.flutter-action.outputs.CHANNEL }}
|
|
139
149
|
flutter-version: ${{ steps.flutter-action.outputs.VERSION }}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Retry the official Flutter manifest GET without exposing partial JSON or proxy secrets."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import time
|
|
12
|
+
from urllib.parse import urlsplit
|
|
13
|
+
|
|
14
|
+
RETRY_CODES = {5, 6, 7, 18, 28, 35, 52, 55, 56, 92, 95}
|
|
15
|
+
RETRY_HTTP = {408, 429, 500, 502, 503, 504}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def download(arguments: list[str], directory: Path) -> int:
|
|
19
|
+
# This is the exact GET made by the pinned upstream setup.sh. Refuse new semantics.
|
|
20
|
+
if arguments[:-1] != ["--silent", "--connect-timeout", "15", "--retry", "5"]:
|
|
21
|
+
raise ValueError("flutter_manifest_arguments_unrecognized")
|
|
22
|
+
url = urlsplit(arguments[-1])
|
|
23
|
+
if (url.scheme != "https" or not url.hostname or url.username or url.password
|
|
24
|
+
or url.query or url.fragment or not re.fullmatch(
|
|
25
|
+
r"/flutter_infra_release/releases/releases_(linux|macos|windows)\.json", url.path)):
|
|
26
|
+
raise ValueError("flutter_manifest_url_unrecognized")
|
|
27
|
+
if not (os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")):
|
|
28
|
+
raise ValueError("flutter_manifest_proxy_required")
|
|
29
|
+
child_env = {**os.environ, "NO_PROXY": "", "no_proxy": ""}
|
|
30
|
+
deadline, code, http, proxy_http = time.monotonic() + 60, 1, 0, 0
|
|
31
|
+
for attempt in range(1, 5):
|
|
32
|
+
remaining = deadline - time.monotonic()
|
|
33
|
+
if remaining <= 0:
|
|
34
|
+
break
|
|
35
|
+
destination = directory / f"manifest-{attempt}.json"
|
|
36
|
+
destination.touch(mode=0o600, exist_ok=False)
|
|
37
|
+
destination.chmod(0o600)
|
|
38
|
+
try:
|
|
39
|
+
result = subprocess.run([
|
|
40
|
+
"curl", "--disable", "--silent", "--show-error", "--fail", "--retry", "0",
|
|
41
|
+
"--connect-timeout", "15", "--max-time", str(min(20, remaining)),
|
|
42
|
+
"--max-filesize", "16777216", "--output", str(destination),
|
|
43
|
+
"--write-out", "%{http_code} %{http_connect}", arguments[-1],
|
|
44
|
+
], capture_output=True, timeout=remaining, env=child_env)
|
|
45
|
+
code = result.returncode
|
|
46
|
+
status = result.stdout.split()
|
|
47
|
+
http, proxy_http = (map(int, status) if len(status) == 2 and all(part.isdigit() for part in status)
|
|
48
|
+
else (0, 0))
|
|
49
|
+
except subprocess.TimeoutExpired:
|
|
50
|
+
code, http, proxy_http = 28, 0, 0
|
|
51
|
+
if code == 0 and http == 200:
|
|
52
|
+
body = destination.read_bytes()
|
|
53
|
+
json.loads(body) # A successful transport must deliver complete JSON.
|
|
54
|
+
sys.stdout.buffer.write(body)
|
|
55
|
+
if attempt > 1:
|
|
56
|
+
print(f"flutter_manifest_recovered attempts={attempt}", file=sys.stderr)
|
|
57
|
+
return 0
|
|
58
|
+
if attempt == 4 or proxy_http in {401, 403, 407} or (code not in RETRY_CODES and http not in RETRY_HTTP):
|
|
59
|
+
break
|
|
60
|
+
time.sleep(min(2 ** (attempt - 1), max(0, deadline - time.monotonic())))
|
|
61
|
+
print(f"flutter_manifest_transport_failed curl_exit={code} http_status={http} "
|
|
62
|
+
f"proxy_status={proxy_http} attempts={attempt}", file=sys.stderr)
|
|
63
|
+
return code or 1
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main() -> int:
|
|
67
|
+
try:
|
|
68
|
+
with tempfile.TemporaryDirectory(prefix="flutter-manifest-", dir=os.environ["RUNNER_TEMP"]) as scratch:
|
|
69
|
+
return download(sys.argv[1:], Path(scratch))
|
|
70
|
+
except (ValueError, OSError, KeyError):
|
|
71
|
+
print("flutter_manifest_invalid_request_or_response", file=sys.stderr)
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
sys.exit(main())
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""An owned CONNECT proxy terminates only fixture TLS; it never dials a destination."""
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import socket
|
|
8
|
+
import socketserver
|
|
9
|
+
import ssl
|
|
10
|
+
import struct
|
|
11
|
+
import subprocess
|
|
12
|
+
import threading
|
|
13
|
+
import urllib.request
|
|
14
|
+
|
|
15
|
+
from test_flutter_cache_setup import SETUP_SHA, SETUP_URL
|
|
16
|
+
|
|
17
|
+
HOST = "manifest.fixture.invalid"
|
|
18
|
+
MANIFEST = json.dumps({"releases": [{"channel": "stable", "version": "3.44.0", "dart_sdk_arch": "x64",
|
|
19
|
+
"hash": "a" * 40, "sha256": "b" * 64, "archive": "owned-sdk.tar.xz"}]}).encode()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def official_setup() -> bytes:
|
|
23
|
+
fixture = os.environ.get("GOWALK_CACHE_ACTION_FIXTURES")
|
|
24
|
+
if fixture:
|
|
25
|
+
source = (Path(fixture) / "flutter-setup.sh").read_bytes()
|
|
26
|
+
else:
|
|
27
|
+
with urllib.request.urlopen(SETUP_URL, timeout=30) as response:
|
|
28
|
+
source = response.read(128 * 1024)
|
|
29
|
+
assert hashlib.sha256(source).hexdigest() == SETUP_SHA
|
|
30
|
+
return source
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Proxy(socketserver.StreamRequestHandler):
|
|
34
|
+
def handle(self):
|
|
35
|
+
first = self.rfile.readline(4096).split()
|
|
36
|
+
while self.rfile.readline(4096) not in (b"\r\n", b"\n", b""):
|
|
37
|
+
pass
|
|
38
|
+
if first != [b"CONNECT", (HOST + ":443").encode(), b"HTTP/1.1"]:
|
|
39
|
+
return
|
|
40
|
+
with self.server.lock:
|
|
41
|
+
self.server.connects += 1
|
|
42
|
+
mode = self.server.modes.pop(0)
|
|
43
|
+
if mode == "proxy_auth":
|
|
44
|
+
self.wfile.write(b"HTTP/1.1 407 Proxy Authentication Required\r\nContent-Length: 0\r\n\r\n")
|
|
45
|
+
return
|
|
46
|
+
self.wfile.write(b"HTTP/1.1 200 Connection established\r\n\r\n")
|
|
47
|
+
self.wfile.flush()
|
|
48
|
+
if mode == "reset":
|
|
49
|
+
self.connection.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
|
|
50
|
+
return
|
|
51
|
+
try:
|
|
52
|
+
with self.server.context.wrap_socket(self.connection, server_side=True) as stream:
|
|
53
|
+
stream.settimeout(3)
|
|
54
|
+
with stream.makefile("rb") as reader:
|
|
55
|
+
request = reader.readline(4096).split()
|
|
56
|
+
while reader.readline(4096) not in (b"\r\n", b"\n", b""):
|
|
57
|
+
pass
|
|
58
|
+
self.server.requests.append(request[1].decode())
|
|
59
|
+
status = b"401 Unauthorized" if mode == "origin_auth" else b"200 OK"
|
|
60
|
+
body = MANIFEST[:31] if mode == "partial" else MANIFEST
|
|
61
|
+
stream.sendall(b"HTTP/1.1 " + status + b"\r\nContent-Length: "
|
|
62
|
+
+ str(len(MANIFEST)).encode() + b"\r\nConnection: close\r\n\r\n" + body)
|
|
63
|
+
except (OSError, ssl.SSLError):
|
|
64
|
+
pass # An untrusted fixture certificate must be rejected by the real client.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@contextmanager
|
|
68
|
+
def tls_proxy(root: Path, modes: list[str]):
|
|
69
|
+
cert, key = root / "cert.pem", root / "key.pem"
|
|
70
|
+
command = ["openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "1",
|
|
71
|
+
"-keyout", str(key), "-out", str(cert), "-subj", "/CN=" + HOST,
|
|
72
|
+
"-addext", "subjectAltName=DNS:" + HOST]
|
|
73
|
+
subprocess.run(command, check=True, capture_output=True, timeout=10)
|
|
74
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
75
|
+
context.load_cert_chain(cert, key)
|
|
76
|
+
server = socketserver.ThreadingTCPServer(("127.0.0.1", 0), Proxy)
|
|
77
|
+
server.daemon_threads = True
|
|
78
|
+
server.context, server.modes = context, list(modes)
|
|
79
|
+
server.connects, server.requests, server.lock = 0, [], threading.Lock()
|
|
80
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
81
|
+
thread.start()
|
|
82
|
+
try:
|
|
83
|
+
yield server, cert
|
|
84
|
+
finally:
|
|
85
|
+
server.shutdown(); server.server_close(); thread.join(3)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Real curl and jq execute the byte-identical official setup through owned TLS failures."""
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
from flutter_manifest_fixture import HOST, MANIFEST, official_setup, tls_proxy
|
|
9
|
+
|
|
10
|
+
ROOT = Path(__file__).parent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ManifestRecoveryTests(unittest.TestCase):
|
|
14
|
+
@classmethod
|
|
15
|
+
def setUpClass(cls):
|
|
16
|
+
cls.source = official_setup()
|
|
17
|
+
|
|
18
|
+
def setUp(self):
|
|
19
|
+
self.directory = tempfile.TemporaryDirectory()
|
|
20
|
+
self.addCleanup(self.directory.cleanup)
|
|
21
|
+
self.root = Path(self.directory.name)
|
|
22
|
+
self.setup = self.root / "setup.sh"
|
|
23
|
+
self.setup.write_bytes(self.source)
|
|
24
|
+
(self.root / "test").mkdir()
|
|
25
|
+
(self.root / "test/releases_linux.json").write_bytes(MANIFEST)
|
|
26
|
+
# Local curl preferences may never disable the assigned proxy or certificate checks.
|
|
27
|
+
(self.root / ".curlrc").write_text('noproxy = "*"\ninsecure\n')
|
|
28
|
+
|
|
29
|
+
def invoke(self, server, cert, *, hooked=True, trusted=True, test_mode=False, bound=True):
|
|
30
|
+
proxy = f"http://fixture-user:fixture-secret@127.0.0.1:{server.server_address[1]}"
|
|
31
|
+
env = {**os.environ, "HOME": str(self.root), "RUNNER_TEMP": str(self.root), "RUNNER_OS": "Linux",
|
|
32
|
+
"RUNNER_ARCH": "X64", "RUNNER_TOOL_CACHE": str(self.root / "tools"),
|
|
33
|
+
"PUB_CACHE": str(self.root / "pub"), "FLUTTER_STORAGE_BASE_URL": "https://" + HOST,
|
|
34
|
+
"GITHUB_OUTPUT": str(self.root / "output"), "HTTPS_PROXY": proxy, "https_proxy": proxy,
|
|
35
|
+
"NO_PROXY": "*", "no_proxy": "*"}
|
|
36
|
+
for key in ("BASH_ENV", "CURL_CA_BUNDLE", "SSL_CERT_FILE", "SSL_CERT_DIR"):
|
|
37
|
+
env.pop(key, None)
|
|
38
|
+
if not bound:
|
|
39
|
+
env.pop("HTTPS_PROXY"); env.pop("https_proxy")
|
|
40
|
+
if trusted:
|
|
41
|
+
env["CURL_CA_BUNDLE"] = str(cert)
|
|
42
|
+
if hooked:
|
|
43
|
+
env.update(BASH_ENV=str(ROOT / "flutter_manifest_env.sh"),
|
|
44
|
+
GOWALK_FLUTTER_MANIFEST_HELPER=str(ROOT / "flutter_manifest.py"))
|
|
45
|
+
else:
|
|
46
|
+
# The baseline deliberately runs the original curl defaults, without stale local bypass settings.
|
|
47
|
+
env.update(HOME=str(self.root / "absent-home"), NO_PROXY="", no_proxy="")
|
|
48
|
+
args = ["bash", str(self.setup), "-p", "-n", "3.44.0", "-a", "x64", "-k", "custom-:version:-:hash:"]
|
|
49
|
+
result = subprocess.run([*args, *(["-t"] if test_mode else []), "stable"], env=env,
|
|
50
|
+
capture_output=True, text=True, timeout=20)
|
|
51
|
+
self.assertNotIn("fixture-secret", result.stdout + result.stderr)
|
|
52
|
+
self.assertFalse(list(self.root.glob("flutter-manifest-*")))
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
def test_original_tls_exit35_recovers_without_changing_official_cache_identity(self):
|
|
56
|
+
with tls_proxy(self.root, ["reset"]) as (server, cert):
|
|
57
|
+
original = self.invoke(server, cert, hooked=False)
|
|
58
|
+
self.assertEqual(original.returncode, 35)
|
|
59
|
+
self.assertEqual((server.connects, server.requests), (1, []))
|
|
60
|
+
with tls_proxy(self.root, ["reset", "ok"]) as (server, cert):
|
|
61
|
+
result = self.invoke(server, cert)
|
|
62
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
63
|
+
self.assertEqual((server.connects, len(server.requests)), (2, 1))
|
|
64
|
+
self.assertIn("flutter_manifest_recovered attempts=2", result.stderr)
|
|
65
|
+
baseline = self.invoke(server, cert, test_mode=True)
|
|
66
|
+
self.assertEqual((self.root / "output").read_text(), baseline.stdout)
|
|
67
|
+
self.assertEqual(result.stdout, "")
|
|
68
|
+
|
|
69
|
+
def test_partial_body_is_discarded_before_the_next_complete_json(self):
|
|
70
|
+
with tls_proxy(self.root, ["partial", "ok"]) as (server, cert):
|
|
71
|
+
result = self.invoke(server, cert)
|
|
72
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
73
|
+
self.assertEqual((server.connects, len(server.requests)), (2, 2))
|
|
74
|
+
self.assertIn("VERSION=3.44.0", (self.root / "output").read_text())
|
|
75
|
+
self.assertEqual(result.stdout, "")
|
|
76
|
+
|
|
77
|
+
def test_proxy_auth_refuses_once_without_any_destination_request(self):
|
|
78
|
+
with tls_proxy(self.root, ["proxy_auth"]) as (server, cert):
|
|
79
|
+
result = self.invoke(server, cert)
|
|
80
|
+
self.assertNotEqual(result.returncode, 0)
|
|
81
|
+
self.assertEqual((server.connects, server.requests), (1, []))
|
|
82
|
+
self.assertIn("proxy_status=407 attempts=1", result.stderr)
|
|
83
|
+
self.assertEqual(result.stdout, "")
|
|
84
|
+
|
|
85
|
+
def test_untrusted_certificate_never_retries_or_uses_curlrc_insecure(self):
|
|
86
|
+
with tls_proxy(self.root, ["ok"]) as (server, cert):
|
|
87
|
+
result = self.invoke(server, cert, trusted=False)
|
|
88
|
+
self.assertEqual(result.returncode, 60)
|
|
89
|
+
self.assertEqual((server.connects, server.requests), (1, []))
|
|
90
|
+
self.assertIn("attempts=1", result.stderr)
|
|
91
|
+
self.assertEqual(result.stdout, "")
|
|
92
|
+
|
|
93
|
+
def test_origin_auth_is_terminal_without_exposing_its_body(self):
|
|
94
|
+
with tls_proxy(self.root, ["origin_auth"]) as (server, cert):
|
|
95
|
+
result = self.invoke(server, cert)
|
|
96
|
+
self.assertEqual(result.returncode, 22)
|
|
97
|
+
self.assertEqual((server.connects, len(server.requests)), (1, 1))
|
|
98
|
+
self.assertIn("http_status=401", result.stderr)
|
|
99
|
+
self.assertIn("attempts=1", result.stderr)
|
|
100
|
+
self.assertEqual(result.stdout, "")
|
|
101
|
+
|
|
102
|
+
def test_repeated_tls_resets_stop_after_four_private_attempts(self):
|
|
103
|
+
with tls_proxy(self.root, ["reset"] * 4) as (server, cert):
|
|
104
|
+
result = self.invoke(server, cert)
|
|
105
|
+
self.assertEqual(result.returncode, 35)
|
|
106
|
+
self.assertEqual((server.connects, server.requests), (4, []))
|
|
107
|
+
self.assertIn("attempts=4", result.stderr)
|
|
108
|
+
self.assertEqual(result.stdout, "")
|
|
109
|
+
|
|
110
|
+
def test_missing_proxy_refuses_before_any_connection(self):
|
|
111
|
+
with tls_proxy(self.root, []) as (server, cert):
|
|
112
|
+
result = self.invoke(server, cert, bound=False)
|
|
113
|
+
self.assertEqual(result.returncode, 1)
|
|
114
|
+
self.assertEqual((server.connects, server.requests), (0, []))
|
|
115
|
+
self.assertEqual(result.stdout, "")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
unittest.main()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""The parent composite freezes helper paths before a nested action changes its context."""
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ManifestPathScopeTests(unittest.TestCase):
|
|
15
|
+
def test_real_transport_step_captures_parent_paths_without_exporting_a_job_hook(self):
|
|
16
|
+
action = yaml.safe_load((ROOT / "android-action/flutter-setup/action.yml").read_text())
|
|
17
|
+
guard = action["runs"]["steps"][0]
|
|
18
|
+
with tempfile.TemporaryDirectory(prefix="manifest parent ") as directory:
|
|
19
|
+
root = Path(directory)
|
|
20
|
+
parent = root / "parent/flutter-setup"
|
|
21
|
+
scripts = parent.parent / "scripts"
|
|
22
|
+
parent.mkdir(parents=True); scripts.mkdir()
|
|
23
|
+
for name in ("play_store_proxy.py", "flutter_manifest.py", "flutter_manifest_env.sh"):
|
|
24
|
+
shutil.copyfile(ROOT / "android-action/scripts" / name, scripts / name)
|
|
25
|
+
env_file, output = root / "env", root / "output"
|
|
26
|
+
env = {**os.environ, "GITHUB_ACTION_PATH": str(parent), "GITHUB_ENV": str(env_file),
|
|
27
|
+
"GITHUB_OUTPUT": str(output), "RUNNER_TEMP": str(root),
|
|
28
|
+
"GOOGLE_STORE_PROXY_URL": "http://fixture-user:fixture-secret@127.0.0.1:9"}
|
|
29
|
+
result = subprocess.run(["bash", "-e", "-o", "pipefail", "-c", guard["run"]], env=env,
|
|
30
|
+
capture_output=True, text=True, timeout=10)
|
|
31
|
+
self.assertEqual((result.returncode, result.stdout, result.stderr), (0, "", ""))
|
|
32
|
+
paths = dict(line.split("=", 1) for line in output.read_text().splitlines())
|
|
33
|
+
self.assertEqual(Path(paths["manifest_env"]).resolve(), (scripts / "flutter_manifest_env.sh").resolve())
|
|
34
|
+
self.assertEqual(Path(paths["manifest_helper"]).resolve(), (scripts / "flutter_manifest.py").resolve())
|
|
35
|
+
self.assertNotIn("BASH_ENV", env_file.read_text())
|
|
36
|
+
self.assertNotIn("GOWALK_FLUTTER_MANIFEST_HELPER", env_file.read_text())
|
|
37
|
+
self.assertNotIn("fixture-secret", output.read_text())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
unittest.main()
|
|
@@ -31,10 +31,18 @@ class BootstrapWiringTests(unittest.TestCase):
|
|
|
31
31
|
self.assertIn("--snapshot", scope["run"])
|
|
32
32
|
self.assertEqual(resolve["with"]["dry-run"], "true")
|
|
33
33
|
for step in (resolve, install):
|
|
34
|
-
self.
|
|
34
|
+
self.assertEqual(step["uses"], "subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2")
|
|
35
|
+
self.assertEqual(step["env"], {
|
|
36
|
+
"BASH_ENV": "${{ steps.transport.outputs.manifest_env }}",
|
|
37
|
+
"GOWALK_FLUTTER_MANIFEST_HELPER": "${{ steps.transport.outputs.manifest_helper }}"})
|
|
35
38
|
self.assertEqual(step["with"]["cache"], "false")
|
|
36
39
|
self.assertEqual(step["with"]["pub-cache"], "false")
|
|
37
40
|
self.assertNotIn("continue-on-error", step)
|
|
41
|
+
self.assertNotIn("BASH_ENV", setup.get("env", {}))
|
|
42
|
+
for step in (guard, scope, sdk_cache, pub_cache):
|
|
43
|
+
self.assertNotIn("BASH_ENV", step.get("env", {}))
|
|
44
|
+
self.assertEqual(guard["id"], "transport")
|
|
45
|
+
self.assertIn('>> "$GITHUB_OUTPUT"', guard["run"])
|
|
38
46
|
for step in (sdk_cache, pub_cache):
|
|
39
47
|
self.assertTrue(step["continue-on-error"])
|
|
40
48
|
self.assertIn("steps.scope.outputs.preloader", step["env"]["NODE_OPTIONS"])
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.73
|