gowalk-cicd 1.0.99 → 1.0.100
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 +5 -0
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/android-action/flutter-setup/action.yml +37 -0
- package/android-action/scripts/flutter_manifest.py +4 -0
- package/android-action/scripts/flutter_release_cache.py +120 -0
- package/android-action/scripts/test_flutter_release_cache.py +103 -0
- package/android-action/scripts/test_flutter_release_cache_scope.py +49 -0
- package/android-action/scripts/test_google_bootstrap_wiring.py +17 -12
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -183,6 +183,11 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
183
183
|
Save only on a cache miss with caching enabled, a real successful install and verified cache
|
|
184
184
|
scope. Preserve resolved keys/paths and the official pinned restore/save programs. Failed or
|
|
185
185
|
dry-run setup never publishes an SDK cache; pub dependency caching retains its job-end behavior.
|
|
186
|
+
- Exact `flutter-version` inputs may reuse previously resolved public release metadata before SDK
|
|
187
|
+
cache lookup. Metadata is scoped to platform, origin, channel, architecture and full version;
|
|
188
|
+
URL/pin/schema/release fields are checked before reuse. Blank, wildcard and version-file inputs
|
|
189
|
+
resolve online. Missing or invalid metadata falls back to the required proxied GET; missing
|
|
190
|
+
account transport still refuses even with a warm cache. SDK keys and checksum checks stay unchanged.
|
|
186
191
|
- Gradle caches use explicit checksum-pinned restore/save actions, each optional and bounded
|
|
187
192
|
to two minutes. Preserve setup-java's key inputs and absolute cache paths to reuse existing
|
|
188
193
|
caches. Java setup, builds and store uploads remain mandatory; save runs after delivery.
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.100
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.100
|
|
@@ -97,12 +97,36 @@ runs:
|
|
|
97
97
|
continue-on-error: true
|
|
98
98
|
shell: bash
|
|
99
99
|
run: node "$GITHUB_ACTION_PATH/../scripts/cache_scope.cjs" --snapshot
|
|
100
|
+
- name: Identify an exact Flutter release metadata cache
|
|
101
|
+
id: release-metadata
|
|
102
|
+
if: ${{ inputs.cache == 'true' && steps.scope.outcome == 'success' }}
|
|
103
|
+
continue-on-error: true
|
|
104
|
+
shell: bash
|
|
105
|
+
env:
|
|
106
|
+
GOWALK_FLUTTER_VERSION: ${{ inputs.flutter-version }}
|
|
107
|
+
GOWALK_FLUTTER_VERSION_FILE: ${{ inputs.flutter-version-file }}
|
|
108
|
+
GOWALK_FLUTTER_CHANNEL: ${{ inputs.channel }}
|
|
109
|
+
GOWALK_FLUTTER_ARCHITECTURE: ${{ inputs.architecture }}
|
|
110
|
+
run: python3 "$GITHUB_ACTION_PATH/../scripts/flutter_release_cache.py"
|
|
111
|
+
- name: Restore exact Flutter release metadata
|
|
112
|
+
id: cache-release
|
|
113
|
+
if: ${{ steps.release-metadata.outputs.enabled == 'true' }}
|
|
114
|
+
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25
|
|
115
|
+
continue-on-error: true
|
|
116
|
+
env:
|
|
117
|
+
NODE_OPTIONS: --require "${{ steps.scope.outputs.preloader }}"
|
|
118
|
+
SEGMENT_DOWNLOAD_TIMEOUT_MINS: '1'
|
|
119
|
+
with:
|
|
120
|
+
path: ${{ steps.release-metadata.outputs.path }}
|
|
121
|
+
key: ${{ steps.release-metadata.outputs.key }}
|
|
100
122
|
- name: Resolve Flutter release and cache identities
|
|
101
123
|
id: flutter-action
|
|
102
124
|
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2
|
|
103
125
|
env:
|
|
104
126
|
BASH_ENV: ${{ steps.transport.outputs.manifest_env }}
|
|
105
127
|
GOWALK_FLUTTER_MANIFEST_HELPER: ${{ steps.transport.outputs.manifest_helper }}
|
|
128
|
+
GOWALK_FLUTTER_RELEASE_CACHE: ${{ steps.release-metadata.outputs.path }}
|
|
129
|
+
GOWALK_FLUTTER_RELEASE_PIN: ${{ steps.release-metadata.outputs.pin }}
|
|
106
130
|
with:
|
|
107
131
|
channel: ${{ inputs.channel }}
|
|
108
132
|
flutter-version: ${{ inputs.flutter-version }}
|
|
@@ -116,6 +140,17 @@ runs:
|
|
|
116
140
|
pub-cache-path: ${{ inputs.pub-cache-path }}
|
|
117
141
|
dry-run: 'true'
|
|
118
142
|
git-source: ${{ inputs.git-source }}
|
|
143
|
+
- name: Save resolved Flutter release metadata
|
|
144
|
+
if: >-
|
|
145
|
+
${{ steps.release-metadata.outputs.enabled == 'true' && steps.flutter-action.outcome == 'success'
|
|
146
|
+
&& steps.cache-release.outputs.cache-hit != 'true' }}
|
|
147
|
+
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25
|
|
148
|
+
continue-on-error: true
|
|
149
|
+
env:
|
|
150
|
+
NODE_OPTIONS: --require "${{ steps.scope.outputs.preloader }}"
|
|
151
|
+
with:
|
|
152
|
+
path: ${{ steps.release-metadata.outputs.path }}
|
|
153
|
+
key: ${{ steps.release-metadata.outputs.key }}
|
|
119
154
|
- name: Cache Flutter
|
|
120
155
|
id: cache-flutter
|
|
121
156
|
if: ${{ inputs.cache == 'true' }}
|
|
@@ -159,6 +194,8 @@ runs:
|
|
|
159
194
|
GOWALK_FLUTTER_MANIFEST_HELPER: ${{ steps.transport.outputs.manifest_helper }}
|
|
160
195
|
GOWALK_FLUTTER_ARCHIVE_CACHE: >-
|
|
161
196
|
${{ inputs.cache == 'true' && format('{0}/gowalk-flutter-archives', runner.temp) || '' }}
|
|
197
|
+
GOWALK_FLUTTER_RELEASE_CACHE: ${{ steps.release-metadata.outputs.path }}
|
|
198
|
+
GOWALK_FLUTTER_RELEASE_PIN: ${{ steps.release-metadata.outputs.pin }}
|
|
162
199
|
with:
|
|
163
200
|
channel: ${{ steps.flutter-action.outputs.CHANNEL }}
|
|
164
201
|
flutter-version: ${{ steps.flutter-action.outputs.VERSION }}
|
|
@@ -10,6 +10,7 @@ from urllib.parse import urlsplit
|
|
|
10
10
|
|
|
11
11
|
import flutter_download
|
|
12
12
|
import flutter_archive_cache
|
|
13
|
+
import flutter_release_cache
|
|
13
14
|
|
|
14
15
|
RETRY_CODES = {5, 6, 7, 18, 28, 35, 52, 55, 56, 92, 95}
|
|
15
16
|
RETRY_HTTP = {408, 429, 500, 502, 503, 504}
|
|
@@ -26,6 +27,8 @@ def download(arguments: list[str], directory: Path) -> int:
|
|
|
26
27
|
destination = directory / "download"
|
|
27
28
|
destination.touch(mode=0o600, exist_ok=True)
|
|
28
29
|
destination.chmod(0o600)
|
|
30
|
+
if flutter_release_cache.reuse(arguments[-1], destination, kind):
|
|
31
|
+
return 0
|
|
29
32
|
if flutter_archive_cache.reuse_complete(destination, kind):
|
|
30
33
|
return 0
|
|
31
34
|
attempted = 0
|
|
@@ -41,6 +44,7 @@ def download(arguments: list[str], directory: Path) -> int:
|
|
|
41
44
|
curl_exit=code, http_status=http, proxy_status=proxy_http)
|
|
42
45
|
if code == 0 and http in ({200, 206} if kind == "archive" else {200}):
|
|
43
46
|
flutter_download.publish(destination, kind)
|
|
47
|
+
flutter_release_cache.remember(arguments[-1], destination, kind)
|
|
44
48
|
if attempt > 1:
|
|
45
49
|
print(f"flutter_{kind}_recovered attempts={attempt}", file=sys.stderr)
|
|
46
50
|
return 0
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Optional public release metadata for exact Flutter pins; SDK identities remain the official action's."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
|
|
10
|
+
import flutter_download
|
|
11
|
+
|
|
12
|
+
MAX_BYTES = 16 * 1024 * 1024
|
|
13
|
+
EXACT_VERSION = re.compile(r"[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z][0-9A-Za-z.-]*)?")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def valid_pin(value) -> bool:
|
|
17
|
+
return (isinstance(value, dict) and set(value) == {"version", "channel", "architecture"}
|
|
18
|
+
and isinstance(value["version"], str)
|
|
19
|
+
and bool(EXACT_VERSION.fullmatch(value["version"]))
|
|
20
|
+
and value["channel"] in ("stable", "beta", "dev")
|
|
21
|
+
and value["architecture"] in ("x64", "arm64"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def context():
|
|
25
|
+
try:
|
|
26
|
+
pin = json.loads(os.environ.get("GOWALK_FLUTTER_RELEASE_PIN", "null"))
|
|
27
|
+
raw = os.environ.get("GOWALK_FLUTTER_RELEASE_CACHE")
|
|
28
|
+
if not raw or not valid_pin(pin):
|
|
29
|
+
return None
|
|
30
|
+
path = Path(raw)
|
|
31
|
+
parent = Path(os.environ["RUNNER_TEMP"]).resolve(strict=True) / "gowalk-flutter-manifests"
|
|
32
|
+
if (path.is_symlink() or path.parent.is_symlink() or path.parent.resolve() != parent
|
|
33
|
+
or not re.fullmatch(r"[a-f0-9]{64}\.json", path.name)):
|
|
34
|
+
return None
|
|
35
|
+
return path, pin
|
|
36
|
+
except (KeyError, OSError, ValueError, TypeError):
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def matches(manifest, pin) -> bool:
|
|
41
|
+
if not isinstance(manifest, dict) or not isinstance(manifest.get("releases"), list):
|
|
42
|
+
return False
|
|
43
|
+
rows = [row for row in manifest["releases"] if isinstance(row, dict)
|
|
44
|
+
and row.get("version") == pin["version"] and row.get("channel") == pin["channel"]
|
|
45
|
+
and (row.get("dart_sdk_arch") or "x64") == pin["architecture"]]
|
|
46
|
+
if len(rows) != 1:
|
|
47
|
+
return False
|
|
48
|
+
row = rows[0]
|
|
49
|
+
return (isinstance(row.get("archive"), str) and not row["archive"].startswith("/")
|
|
50
|
+
and ".." not in row["archive"] and "\\" not in row["archive"]
|
|
51
|
+
and bool(re.fullmatch(r"[a-fA-F0-9]{40}", str(row.get("hash", ""))))
|
|
52
|
+
and bool(re.fullmatch(r"[a-fA-F0-9]{64}", str(row.get("sha256", "")))))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def reuse(url: str, destination: Path, kind: str) -> bool:
|
|
56
|
+
selected = context() if kind == "manifest" else None
|
|
57
|
+
if not selected:
|
|
58
|
+
return False
|
|
59
|
+
path, pin = selected
|
|
60
|
+
try:
|
|
61
|
+
if not path.is_file() or path.stat().st_size > MAX_BYTES:
|
|
62
|
+
return False
|
|
63
|
+
record = json.loads(path.read_bytes())
|
|
64
|
+
if (not isinstance(record, dict) or record.get("schema") != 1 or record.get("url") != url
|
|
65
|
+
or record.get("pin") != pin or not matches(record.get("manifest"), pin)):
|
|
66
|
+
return False
|
|
67
|
+
destination.write_text(json.dumps(record["manifest"]))
|
|
68
|
+
flutter_download.publish(destination, kind)
|
|
69
|
+
print('::notice title=flutter_manifest_cache::{"schema":"gowalk-cicd/flutter-manifest-cache.v1",'
|
|
70
|
+
'"event":"hit"}', file=sys.stderr)
|
|
71
|
+
return True
|
|
72
|
+
except (OSError, ValueError, TypeError):
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def remember(url: str, destination: Path, kind: str) -> None:
|
|
77
|
+
selected = context() if kind == "manifest" else None
|
|
78
|
+
if not selected or destination.stat().st_size > MAX_BYTES:
|
|
79
|
+
return
|
|
80
|
+
path, pin = selected
|
|
81
|
+
temporary = None
|
|
82
|
+
try:
|
|
83
|
+
manifest = json.loads(destination.read_bytes())
|
|
84
|
+
if not matches(manifest, pin):
|
|
85
|
+
return
|
|
86
|
+
content = json.dumps({"schema": 1, "url": url, "pin": pin, "manifest": manifest}).encode()
|
|
87
|
+
if len(content) > MAX_BYTES:
|
|
88
|
+
return
|
|
89
|
+
path.parent.mkdir(mode=0o700, exist_ok=True)
|
|
90
|
+
with tempfile.NamedTemporaryFile(dir=path.parent, prefix="incoming-", delete=False) as output:
|
|
91
|
+
temporary = Path(output.name)
|
|
92
|
+
output.write(content)
|
|
93
|
+
os.replace(temporary, path)
|
|
94
|
+
except (OSError, ValueError, TypeError):
|
|
95
|
+
pass # This optional cache cannot turn a completed provider read into a failed setup.
|
|
96
|
+
finally:
|
|
97
|
+
if temporary:
|
|
98
|
+
try:
|
|
99
|
+
temporary.unlink(missing_ok=True)
|
|
100
|
+
except OSError:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def prepare() -> None:
|
|
105
|
+
pin = {"version": os.environ.get("GOWALK_FLUTTER_VERSION", ""),
|
|
106
|
+
"channel": os.environ.get("GOWALK_FLUTTER_CHANNEL", ""),
|
|
107
|
+
"architecture": os.environ.get("GOWALK_FLUTTER_ARCHITECTURE", "").lower()}
|
|
108
|
+
with Path(os.environ["GITHUB_OUTPUT"]).open("a") as output:
|
|
109
|
+
if os.environ.get("GOWALK_FLUTTER_VERSION_FILE") or not valid_pin(pin):
|
|
110
|
+
output.write("enabled=false\n")
|
|
111
|
+
return
|
|
112
|
+
namespace = {"pin": pin, "os": os.environ["RUNNER_OS"],
|
|
113
|
+
"origin": os.environ.get("FLUTTER_STORAGE_BASE_URL", "https://storage.googleapis.com")}
|
|
114
|
+
digest = hashlib.sha256(json.dumps(namespace, sort_keys=True).encode()).hexdigest()
|
|
115
|
+
path = Path(os.environ["RUNNER_TEMP"]).resolve(strict=True) / "gowalk-flutter-manifests" / f"{digest}.json"
|
|
116
|
+
output.write(f"enabled=true\nkey=flutter-manifest-v1-{digest}\npath={path}\npin={json.dumps(pin)}\n")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
prepare()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Exact pinned setup survives a manifest outage after a verified resolution; misses stay proxied."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
from flutter_manifest_fixture import HOST, official_setup, tls_proxy
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).parent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ReleaseCacheTests(unittest.TestCase):
|
|
15
|
+
@classmethod
|
|
16
|
+
def setUpClass(cls):
|
|
17
|
+
cls.source = official_setup()
|
|
18
|
+
|
|
19
|
+
def setUp(self):
|
|
20
|
+
scratch = tempfile.TemporaryDirectory()
|
|
21
|
+
self.addCleanup(scratch.cleanup)
|
|
22
|
+
self.root = Path(scratch.name)
|
|
23
|
+
self.setup = self.root / "setup.sh"
|
|
24
|
+
self.setup.write_bytes(self.source)
|
|
25
|
+
self.cache = self.root / "gowalk-flutter-manifests" / ("a" * 64 + ".json")
|
|
26
|
+
|
|
27
|
+
def invoke(self, server, cert, *, version="3.44.0", bound=True):
|
|
28
|
+
pin = {"version": version, "channel": "stable", "architecture": "x64"}
|
|
29
|
+
env = {**os.environ, "RUNNER_TEMP": str(self.root), "RUNNER_OS": "Linux", "RUNNER_ARCH": "X64",
|
|
30
|
+
"RUNNER_TOOL_CACHE": str(self.root / "tools"), "GITHUB_OUTPUT": str(self.root / "output"),
|
|
31
|
+
"PUB_CACHE": str(self.root / "pub"), "FLUTTER_STORAGE_BASE_URL": "https://" + HOST,
|
|
32
|
+
"BASH_ENV": str(ROOT / "flutter_manifest_env.sh"), "CURL_CA_BUNDLE": str(cert),
|
|
33
|
+
"GOWALK_FLUTTER_MANIFEST_HELPER": str(ROOT / "flutter_manifest.py"),
|
|
34
|
+
"GOWALK_FLUTTER_RELEASE_CACHE": str(self.cache), "GOWALK_FLUTTER_RELEASE_PIN": json.dumps(pin),
|
|
35
|
+
"HTTPS_PROXY": f"http://127.0.0.1:{server.server_address[1]}", "NO_PROXY": "", "no_proxy": ""}
|
|
36
|
+
env.pop("https_proxy", None)
|
|
37
|
+
if not bound:
|
|
38
|
+
env.pop("HTTPS_PROXY")
|
|
39
|
+
(self.root / "output").write_text("")
|
|
40
|
+
return subprocess.run(["bash", str(self.setup), "-p", "-n", version, "-a", "x64",
|
|
41
|
+
"-k", "same-:version:-:hash:", "stable"], env=env,
|
|
42
|
+
capture_output=True, text=True, timeout=15)
|
|
43
|
+
|
|
44
|
+
def test_warm_resolution_preserves_sdk_identity_without_another_manifest_connection(self):
|
|
45
|
+
with tls_proxy(self.root, ["ok", "origin_auth"]) as (server, cert):
|
|
46
|
+
cold = self.invoke(server, cert)
|
|
47
|
+
self.assertEqual(cold.returncode, 0, cold.stderr)
|
|
48
|
+
identity = (self.root / "output").read_text()
|
|
49
|
+
warm = self.invoke(server, cert)
|
|
50
|
+
self.assertEqual(warm.returncode, 0, warm.stderr)
|
|
51
|
+
self.assertEqual((server.connects, len(server.requests)), (1, 1))
|
|
52
|
+
self.assertEqual((self.root / "output").read_text(), identity)
|
|
53
|
+
self.assertIn("flutter_manifest_cache", warm.stderr)
|
|
54
|
+
|
|
55
|
+
def test_changed_pin_does_not_reuse_another_versions_manifest(self):
|
|
56
|
+
with tls_proxy(self.root, ["ok", "origin_auth"]) as (server, cert):
|
|
57
|
+
self.assertEqual(self.invoke(server, cert).returncode, 0)
|
|
58
|
+
changed = self.invoke(server, cert, version="3.44.1")
|
|
59
|
+
self.assertNotEqual(changed.returncode, 0)
|
|
60
|
+
self.assertEqual(server.connects, 2)
|
|
61
|
+
self.assertIn("http_status=401", changed.stderr)
|
|
62
|
+
|
|
63
|
+
def test_invalid_cache_is_recovered_through_the_assigned_proxy(self):
|
|
64
|
+
self.cache.parent.mkdir()
|
|
65
|
+
self.cache.write_text("truncated JSON")
|
|
66
|
+
with tls_proxy(self.root, ["ok"]) as (server, cert):
|
|
67
|
+
result = self.invoke(server, cert)
|
|
68
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
69
|
+
self.assertEqual(server.connects, 1)
|
|
70
|
+
self.assertEqual(json.loads(self.cache.read_text())["pin"]["version"], "3.44.0")
|
|
71
|
+
|
|
72
|
+
def test_warm_cache_never_bypasses_the_required_account_pin(self):
|
|
73
|
+
with tls_proxy(self.root, ["ok"]) as (server, cert):
|
|
74
|
+
self.assertEqual(self.invoke(server, cert).returncode, 0)
|
|
75
|
+
result = self.invoke(server, cert, bound=False)
|
|
76
|
+
self.assertNotEqual(result.returncode, 0)
|
|
77
|
+
self.assertEqual(server.connects, 1)
|
|
78
|
+
|
|
79
|
+
def test_wrong_origin_or_invalid_release_metadata_requires_a_fresh_provider_read(self):
|
|
80
|
+
for change in ["url", "sha256"]:
|
|
81
|
+
with self.subTest(change=change), tls_proxy(self.root, ["ok", "ok"]) as (server, cert):
|
|
82
|
+
self.cache.unlink(missing_ok=True)
|
|
83
|
+
self.assertEqual(self.invoke(server, cert).returncode, 0)
|
|
84
|
+
record = json.loads(self.cache.read_text())
|
|
85
|
+
if change == "url":
|
|
86
|
+
record["url"] = "https://another.fixture.invalid/releases_linux.json"
|
|
87
|
+
else:
|
|
88
|
+
record["manifest"]["releases"][0]["sha256"] = "invalid"
|
|
89
|
+
self.cache.write_text(json.dumps(record))
|
|
90
|
+
self.assertEqual(self.invoke(server, cert).returncode, 0)
|
|
91
|
+
self.assertEqual(server.connects, 2)
|
|
92
|
+
|
|
93
|
+
def test_wildcard_selection_still_resolves_a_fresh_manifest(self):
|
|
94
|
+
with tls_proxy(self.root, ["ok", "origin_auth"]) as (server, cert):
|
|
95
|
+
self.assertEqual(self.invoke(server, cert, version="3.44.x").returncode, 0)
|
|
96
|
+
result = self.invoke(server, cert, version="3.44.x")
|
|
97
|
+
self.assertNotEqual(result.returncode, 0)
|
|
98
|
+
self.assertEqual(server.connects, 2)
|
|
99
|
+
self.assertFalse(self.cache.exists())
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
unittest.main()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Metadata cache selectors cannot freeze moving requests or cross platforms/origins."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
SCRIPT = Path(__file__).with_name("flutter_release_cache.py")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CacheSelectionTests(unittest.TestCase):
|
|
14
|
+
def prepare(self, root, **changes):
|
|
15
|
+
output = root / "outputs"
|
|
16
|
+
output.write_text("")
|
|
17
|
+
env = {**os.environ, "GITHUB_OUTPUT": str(output), "RUNNER_TEMP": str(root), "RUNNER_OS": "Linux",
|
|
18
|
+
"GOWALK_FLUTTER_VERSION": "3.44.0", "GOWALK_FLUTTER_VERSION_FILE": "",
|
|
19
|
+
"GOWALK_FLUTTER_CHANNEL": "stable", "GOWALK_FLUTTER_ARCHITECTURE": "X64",
|
|
20
|
+
"FLUTTER_STORAGE_BASE_URL": "https://storage.googleapis.com", **changes}
|
|
21
|
+
result = subprocess.run([sys.executable, str(SCRIPT)], env=env, capture_output=True, timeout=10)
|
|
22
|
+
self.assertEqual((result.returncode, result.stdout, result.stderr), (0, b"", b""))
|
|
23
|
+
return dict(line.split("=", 1) for line in output.read_text().splitlines())
|
|
24
|
+
|
|
25
|
+
def test_exact_identity_is_stable_and_other_selections_have_separate_keys(self):
|
|
26
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
27
|
+
root = Path(directory)
|
|
28
|
+
first = self.prepare(root)
|
|
29
|
+
self.assertEqual(first, self.prepare(root))
|
|
30
|
+
self.assertEqual(first["enabled"], "true")
|
|
31
|
+
self.assertEqual(json.loads(first["pin"])["architecture"], "x64")
|
|
32
|
+
for change in [{"RUNNER_OS": "macOS"}, {"GOWALK_FLUTTER_ARCHITECTURE": "ARM64"},
|
|
33
|
+
{"GOWALK_FLUTTER_CHANNEL": "beta"}, {"GOWALK_FLUTTER_VERSION": "3.44.1"},
|
|
34
|
+
{"FLUTTER_STORAGE_BASE_URL": "https://another.fixture.invalid"}]:
|
|
35
|
+
with self.subTest(change=change):
|
|
36
|
+
self.assertNotEqual(first["key"], self.prepare(root, **change)["key"])
|
|
37
|
+
|
|
38
|
+
def test_moving_or_file_resolved_versions_do_not_enable_metadata_reuse(self):
|
|
39
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
40
|
+
root = Path(directory)
|
|
41
|
+
for change in [{"GOWALK_FLUTTER_VERSION": ""}, {"GOWALK_FLUTTER_VERSION": "3.x"},
|
|
42
|
+
{"GOWALK_FLUTTER_CHANNEL": "master"},
|
|
43
|
+
{"GOWALK_FLUTTER_VERSION_FILE": "pubspec.yaml"}]:
|
|
44
|
+
with self.subTest(change=change):
|
|
45
|
+
self.assertEqual(self.prepare(root, **change), {"enabled": "false"})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
unittest.main()
|
|
@@ -9,6 +9,22 @@ ROOT = Path(__file__).resolve().parents[2]
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class BootstrapWiringTests(unittest.TestCase):
|
|
12
|
+
def assert_protected_setup(self, resolve, install):
|
|
13
|
+
for step in (resolve, install):
|
|
14
|
+
self.assertEqual(step["uses"], "subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2")
|
|
15
|
+
expected_env = {
|
|
16
|
+
"BASH_ENV": "${{ steps.transport.outputs.manifest_env }}",
|
|
17
|
+
"GOWALK_FLUTTER_MANIFEST_HELPER": "${{ steps.transport.outputs.manifest_helper }}",
|
|
18
|
+
"GOWALK_FLUTTER_RELEASE_CACHE": "${{ steps.release-metadata.outputs.path }}",
|
|
19
|
+
"GOWALK_FLUTTER_RELEASE_PIN": "${{ steps.release-metadata.outputs.pin }}"}
|
|
20
|
+
if step is install:
|
|
21
|
+
expected_env["GOWALK_FLUTTER_ARCHIVE_CACHE"] = (
|
|
22
|
+
"${{ inputs.cache == 'true' && format('{0}/gowalk-flutter-archives', runner.temp) || '' }}")
|
|
23
|
+
self.assertEqual(step["env"], expected_env)
|
|
24
|
+
self.assertEqual(step["with"]["cache"], "false")
|
|
25
|
+
self.assertEqual(step["with"]["pub-cache"], "false")
|
|
26
|
+
self.assertNotIn("continue-on-error", step)
|
|
27
|
+
|
|
12
28
|
def test_each_flutter_sdk_setup_has_a_prior_mandatory_account_guard(self):
|
|
13
29
|
checked = 0
|
|
14
30
|
for name in ("deploy.yml", "deploy-web.yml"):
|
|
@@ -34,18 +50,7 @@ class BootstrapWiringTests(unittest.TestCase):
|
|
|
34
50
|
self.assertTrue(scope["continue-on-error"])
|
|
35
51
|
self.assertIn("--snapshot", scope["run"])
|
|
36
52
|
self.assertEqual(resolve["with"]["dry-run"], "true")
|
|
37
|
-
|
|
38
|
-
self.assertEqual(step["uses"], "subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2")
|
|
39
|
-
expected_env = {
|
|
40
|
-
"BASH_ENV": "${{ steps.transport.outputs.manifest_env }}",
|
|
41
|
-
"GOWALK_FLUTTER_MANIFEST_HELPER": "${{ steps.transport.outputs.manifest_helper }}"}
|
|
42
|
-
if step is install:
|
|
43
|
-
expected_env["GOWALK_FLUTTER_ARCHIVE_CACHE"] = (
|
|
44
|
-
"${{ inputs.cache == 'true' && format('{0}/gowalk-flutter-archives', runner.temp) || '' }}")
|
|
45
|
-
self.assertEqual(step["env"], expected_env)
|
|
46
|
-
self.assertEqual(step["with"]["cache"], "false")
|
|
47
|
-
self.assertEqual(step["with"]["pub-cache"], "false")
|
|
48
|
-
self.assertNotIn("continue-on-error", step)
|
|
53
|
+
self.assert_protected_setup(resolve, install)
|
|
49
54
|
self.assertNotIn("BASH_ENV", setup.get("env", {}))
|
|
50
55
|
for step in (guard, scope, sdk_cache, pub_cache, save):
|
|
51
56
|
self.assertNotIn("BASH_ENV", step.get("env", {}))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.100
|