gowalk-cicd 1.0.70 → 1.0.71
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 +10 -0
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/android-action/flutter-setup/action.yml +149 -0
- package/android-action/scripts/cache_env.cjs +10 -0
- package/android-action/scripts/cache_scope.cjs +77 -0
- package/android-action/scripts/test_flutter_cache_setup.py +125 -0
- package/android-action/scripts/test_google_bootstrap_wiring.py +44 -5
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
- package/templates/deploy-web.yml +1 -6
- package/templates/deploy.yml +13 -16
package/CLAUDE.md
CHANGED
|
@@ -135,6 +135,16 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
135
135
|
localization, global activation and builds run through the existing owned relay.
|
|
136
136
|
The helper's bootstrap and command modes require only Python's standard library;
|
|
137
137
|
importing the Google API client must not become an unprotected bootstrap prerequisite.
|
|
138
|
+
- `android-action/flutter-setup/` owns Flutter setup. It forwards the official action's
|
|
139
|
+
inputs/cache identities while isolating restore/save from provider transport. Its step-local
|
|
140
|
+
`NODE_OPTIONS` preloader (`android-action/scripts/cache_*.cjs`) also protects CocoaPods cache
|
|
141
|
+
steps and verifies the exact pinned Actions cache entrypoint bytes and HTTPS
|
|
142
|
+
GitHub cache/Azure blob destinations before removing proxy variables in that cache process.
|
|
143
|
+
Unknown code or origins refuse; SDK setup, non-cache children and later steps retain their
|
|
144
|
+
proxy requirement. Never export this scope through `GITHUB_ENV` or add cache domains to
|
|
145
|
+
`NO_PROXY`. The preloader snapshot under `RUNNER_TEMP` also covers official post/save after
|
|
146
|
+
consumer autoupdate changes the action directory. The installer delivers this development
|
|
147
|
+
tooling for native iOS repositories too; it does not require an Android app.
|
|
138
148
|
- Flutter setup scopes `SEGMENT_DOWNLOAD_TIMEOUT_MINS=1` to the composite action's optional
|
|
139
149
|
SDK/pub cache restores. This bounds each cache segment, not the whole cache or SDK download.
|
|
140
150
|
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.71
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.71
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
name: Set up proxied Flutter
|
|
2
|
+
description: Set up Flutter with protected provider transport and isolated official GitHub caches
|
|
3
|
+
inputs:
|
|
4
|
+
channel:
|
|
5
|
+
description: The Flutter build release channel
|
|
6
|
+
required: false
|
|
7
|
+
default: stable
|
|
8
|
+
flutter-version:
|
|
9
|
+
description: The Flutter version to make available on the path
|
|
10
|
+
required: false
|
|
11
|
+
default: ''
|
|
12
|
+
flutter-version-file:
|
|
13
|
+
description: The pubspec.yaml or FVM config file with exact Flutter version defined
|
|
14
|
+
required: false
|
|
15
|
+
default: ''
|
|
16
|
+
architecture:
|
|
17
|
+
description: The architecture of Flutter SDK executable (x64 or arm64)
|
|
18
|
+
required: false
|
|
19
|
+
default: ${{ runner.arch }}
|
|
20
|
+
cache:
|
|
21
|
+
description: Cache the Flutter SDK
|
|
22
|
+
required: false
|
|
23
|
+
default: 'false'
|
|
24
|
+
cache-key:
|
|
25
|
+
description: Identifier for the Flutter SDK cache
|
|
26
|
+
required: false
|
|
27
|
+
default: ''
|
|
28
|
+
cache-path:
|
|
29
|
+
description: Flutter SDK cache path
|
|
30
|
+
required: false
|
|
31
|
+
default: ''
|
|
32
|
+
pub-cache:
|
|
33
|
+
description: Cache the Dart pub dependencies
|
|
34
|
+
required: false
|
|
35
|
+
default: ''
|
|
36
|
+
pub-cache-key:
|
|
37
|
+
description: Identifier for the Dart .pub-cache cache
|
|
38
|
+
required: false
|
|
39
|
+
default: ''
|
|
40
|
+
pub-cache-path:
|
|
41
|
+
description: Flutter pub cache path
|
|
42
|
+
required: false
|
|
43
|
+
default: default
|
|
44
|
+
dry-run:
|
|
45
|
+
description: If true, get outputs but do not install Flutter
|
|
46
|
+
required: false
|
|
47
|
+
default: 'false'
|
|
48
|
+
git-source:
|
|
49
|
+
description: Git clone source
|
|
50
|
+
required: false
|
|
51
|
+
default: https://github.com/flutter/flutter.git
|
|
52
|
+
outputs:
|
|
53
|
+
CHANNEL:
|
|
54
|
+
value: ${{ steps.flutter-action.outputs.CHANNEL }}
|
|
55
|
+
description: The selected Flutter release channel
|
|
56
|
+
VERSION:
|
|
57
|
+
value: ${{ steps.flutter-action.outputs.VERSION }}
|
|
58
|
+
description: The selected Flutter version
|
|
59
|
+
ARCHITECTURE:
|
|
60
|
+
value: ${{ steps.flutter-action.outputs.ARCHITECTURE }}
|
|
61
|
+
description: The selected Flutter CPU architecture
|
|
62
|
+
CACHE-KEY:
|
|
63
|
+
value: ${{ steps.flutter-action.outputs.CACHE-KEY }}
|
|
64
|
+
description: Key used to cache the Flutter SDK
|
|
65
|
+
CACHE-PATH:
|
|
66
|
+
value: ${{ steps.flutter-action.outputs.CACHE-PATH }}
|
|
67
|
+
description: Path to Flutter SDK
|
|
68
|
+
PUB-CACHE-KEY:
|
|
69
|
+
value: ${{ steps.flutter-action.outputs.PUB-CACHE-KEY }}
|
|
70
|
+
description: Key used to cache the pub dependencies
|
|
71
|
+
PUB-CACHE-PATH:
|
|
72
|
+
value: ${{ steps.flutter-action.outputs.PUB-CACHE-PATH }}
|
|
73
|
+
description: Path to pub cache
|
|
74
|
+
GIT_SOURCE:
|
|
75
|
+
value: ${{ steps.flutter-action.outputs.GIT_SOURCE }}
|
|
76
|
+
description: Git source of Flutter SDK repository to clone
|
|
77
|
+
CACHE-HIT:
|
|
78
|
+
value: ${{ steps.cache-flutter.outputs.cache-hit }}
|
|
79
|
+
description: '`true` if the flutter cache was a hit'
|
|
80
|
+
PUB-CACHE-HIT:
|
|
81
|
+
value: ${{ steps.cache-pub.outputs.cache-hit }}
|
|
82
|
+
description: '`true` if the pub cache was a hit'
|
|
83
|
+
runs:
|
|
84
|
+
using: composite
|
|
85
|
+
steps:
|
|
86
|
+
- name: Require provider transport
|
|
87
|
+
shell: bash
|
|
88
|
+
run: |
|
|
89
|
+
python3 "$GITHUB_ACTION_PATH/../scripts/play_store_proxy.py" --bootstrap-env
|
|
90
|
+
- name: Retain the cache process scope
|
|
91
|
+
id: scope
|
|
92
|
+
if: ${{ inputs.cache == 'true' || inputs.pub-cache == 'true' }}
|
|
93
|
+
continue-on-error: true
|
|
94
|
+
shell: bash
|
|
95
|
+
run: node "$GITHUB_ACTION_PATH/../scripts/cache_scope.cjs" --snapshot
|
|
96
|
+
- name: Resolve Flutter release and cache identities
|
|
97
|
+
id: flutter-action
|
|
98
|
+
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2
|
|
99
|
+
with:
|
|
100
|
+
channel: ${{ inputs.channel }}
|
|
101
|
+
flutter-version: ${{ inputs.flutter-version }}
|
|
102
|
+
flutter-version-file: ${{ inputs.flutter-version-file }}
|
|
103
|
+
architecture: ${{ inputs.architecture }}
|
|
104
|
+
cache: 'false'
|
|
105
|
+
cache-key: ${{ inputs.cache-key }}
|
|
106
|
+
cache-path: ${{ inputs.cache-path }}
|
|
107
|
+
pub-cache: 'false'
|
|
108
|
+
pub-cache-key: ${{ inputs.pub-cache-key }}
|
|
109
|
+
pub-cache-path: ${{ inputs.pub-cache-path }}
|
|
110
|
+
dry-run: 'true'
|
|
111
|
+
git-source: ${{ inputs.git-source }}
|
|
112
|
+
- name: Cache Flutter
|
|
113
|
+
id: cache-flutter
|
|
114
|
+
if: ${{ inputs.cache == 'true' }}
|
|
115
|
+
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25
|
|
116
|
+
continue-on-error: true
|
|
117
|
+
env:
|
|
118
|
+
NODE_OPTIONS: --require "${{ steps.scope.outputs.preloader }}"
|
|
119
|
+
SEGMENT_DOWNLOAD_TIMEOUT_MINS: '1'
|
|
120
|
+
with:
|
|
121
|
+
path: ${{ steps.flutter-action.outputs.CACHE-PATH }}
|
|
122
|
+
key: ${{ steps.flutter-action.outputs.CACHE-KEY }}
|
|
123
|
+
- name: Cache pub dependencies
|
|
124
|
+
id: cache-pub
|
|
125
|
+
if: ${{ (inputs.pub-cache == '' && inputs.cache == 'true') || inputs.pub-cache == 'true' }}
|
|
126
|
+
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25
|
|
127
|
+
continue-on-error: true
|
|
128
|
+
env:
|
|
129
|
+
NODE_OPTIONS: --require "${{ steps.scope.outputs.preloader }}"
|
|
130
|
+
SEGMENT_DOWNLOAD_TIMEOUT_MINS: '1'
|
|
131
|
+
with:
|
|
132
|
+
path: ${{ steps.flutter-action.outputs.PUB-CACHE-PATH }}
|
|
133
|
+
key: ${{ steps.flutter-action.outputs.PUB-CACHE-KEY }}-${{ hashFiles('**/pubspec.lock') }}
|
|
134
|
+
- name: Install or reuse the resolved Flutter SDK
|
|
135
|
+
if: ${{ inputs.dry-run != 'true' }}
|
|
136
|
+
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2
|
|
137
|
+
with:
|
|
138
|
+
channel: ${{ steps.flutter-action.outputs.CHANNEL }}
|
|
139
|
+
flutter-version: ${{ steps.flutter-action.outputs.VERSION }}
|
|
140
|
+
flutter-version-file: ''
|
|
141
|
+
architecture: ${{ steps.flutter-action.outputs.ARCHITECTURE }}
|
|
142
|
+
cache: 'false'
|
|
143
|
+
cache-key: ${{ inputs.cache-key }}
|
|
144
|
+
cache-path: ${{ steps.flutter-action.outputs.CACHE-PATH }}
|
|
145
|
+
pub-cache: 'false'
|
|
146
|
+
pub-cache-key: ${{ inputs.pub-cache-key }}
|
|
147
|
+
pub-cache-path: ${{ steps.flutter-action.outputs.PUB-CACHE-PATH }}
|
|
148
|
+
dry-run: 'false'
|
|
149
|
+
git-source: ${{ inputs.git-source }}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// NODE_OPTIONS applies this only to the cache step, including its official post/save action.
|
|
2
|
+
try {
|
|
3
|
+
const scope = require('./cache_scope.cjs');
|
|
4
|
+
scope.authorize(process.argv[1], process.env);
|
|
5
|
+
scope.guardRequests();
|
|
6
|
+
for (const key of scope.proxyKeys) delete process.env[key];
|
|
7
|
+
} catch {
|
|
8
|
+
process.stderr.write('github_cache_transport_scope_refused\n');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Direct routing belongs only to these pinned first-party cache programs and HTTPS origins.
|
|
2
|
+
const {readFileSync, copyFileSync, mkdtempSync, appendFileSync} = require('node:fs');
|
|
3
|
+
const {createHash} = require('node:crypto');
|
|
4
|
+
const {basename, dirname, join} = require('node:path');
|
|
5
|
+
const hashes = new Map([
|
|
6
|
+
['restore', 'a49301428b4c5477522c00f6464566457ed327d64ae5f80f8c12e762fb2ea9a2'],
|
|
7
|
+
['save', 'b969065795aaf5c6d20ba158e910be5d353eefb02eb2785263eb32e13090c066'],
|
|
8
|
+
]);
|
|
9
|
+
const proxyKeys = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'ALL_PROXY', 'all_proxy'];
|
|
10
|
+
const refusal = () => new Error('github_cache_transport_scope_refused');
|
|
11
|
+
|
|
12
|
+
function allowedHost(host) {
|
|
13
|
+
return host === 'api.github.com'
|
|
14
|
+
|| /^[a-z0-9.-]+\.actions\.githubusercontent\.com$/.test(host)
|
|
15
|
+
|| /^[a-z0-9]{3,24}\.blob\.core\.windows\.net$/.test(host);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function requireOrigin(value, service = false) {
|
|
19
|
+
let url;
|
|
20
|
+
try { url = new URL(value); } catch { throw refusal(); }
|
|
21
|
+
if (url.protocol !== 'https:' || url.port || url.username || url.password || !allowedHost(url.hostname)
|
|
22
|
+
|| (service && !url.hostname.endsWith('.actions.githubusercontent.com'))) throw refusal();
|
|
23
|
+
return url;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function authorize(entrypoint, env) {
|
|
27
|
+
if (!entrypoint || basename(entrypoint) !== 'index.js' || basename(dirname(dirname(entrypoint))) !== 'dist') {
|
|
28
|
+
throw refusal();
|
|
29
|
+
}
|
|
30
|
+
const expected = hashes.get(basename(dirname(entrypoint)));
|
|
31
|
+
if (!expected || createHash('sha256').update(readFileSync(entrypoint)).digest('hex') !== expected) throw refusal();
|
|
32
|
+
const origins = [env.ACTIONS_RESULTS_URL, env.ACTIONS_CACHE_URL].filter(Boolean);
|
|
33
|
+
if (!origins.length) throw refusal();
|
|
34
|
+
for (const origin of origins) requireOrigin(origin, true);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function requireRequest(args, protocol) {
|
|
38
|
+
const [input, extra] = args;
|
|
39
|
+
const options = typeof input === 'string' || input instanceof URL
|
|
40
|
+
? {protocol, ...require('node:url').urlToHttpOptions(requireOrigin(input)),
|
|
41
|
+
...(typeof extra === 'object' ? extra : {})}
|
|
42
|
+
: {...input};
|
|
43
|
+
if (!options || options.socketPath || (options.protocol || protocol) !== 'https:') throw refusal();
|
|
44
|
+
for (const host of [options.hostname, options.host].filter(Boolean)) {
|
|
45
|
+
requireOrigin(`https://${host}${options.port ? ':' + options.port : ''}`);
|
|
46
|
+
}
|
|
47
|
+
if (!options.hostname && !options.host) throw refusal();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function guardRequests() {
|
|
51
|
+
for (const [name, protocol] of [['node:http', 'http:'], ['node:https', 'https:']]) {
|
|
52
|
+
const client = require(name);
|
|
53
|
+
for (const verb of ['request', 'get']) {
|
|
54
|
+
const original = client[verb];
|
|
55
|
+
client[verb] = function (...args) { requireRequest(args, protocol); return original.apply(this, args); };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (globalThis.fetch) {
|
|
59
|
+
const original = globalThis.fetch;
|
|
60
|
+
globalThis.fetch = function (input, init) {
|
|
61
|
+
requireOrigin(typeof input === 'string' || input instanceof URL ? input : input.url);
|
|
62
|
+
return original.call(this, input, {...init, redirect: 'error'});
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function snapshot() {
|
|
68
|
+
const target = mkdtempSync(join(process.env.RUNNER_TEMP, 'gowalk-cache-scope-'));
|
|
69
|
+
for (const file of ['cache_env.cjs', 'cache_scope.cjs']) copyFileSync(join(__dirname, file), join(target, file));
|
|
70
|
+
appendFileSync(process.env.GITHUB_OUTPUT, `preloader=${join(target, 'cache_env.cjs')}\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {authorize, guardRequests, proxyKeys, requireOrigin};
|
|
74
|
+
if (require.main === module) {
|
|
75
|
+
try { if (process.argv[2] !== '--snapshot') throw refusal(); snapshot(); }
|
|
76
|
+
catch { process.stderr.write('github_cache_scope_snapshot_failed\n'); process.exitCode = 1; }
|
|
77
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Real pinned SDK setup reuses restored bytes; cold/missed cache stays on the owned proxy."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tarfile
|
|
10
|
+
import tempfile
|
|
11
|
+
import threading
|
|
12
|
+
import unittest
|
|
13
|
+
import urllib.request
|
|
14
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
15
|
+
|
|
16
|
+
SCRIPT = Path(__file__).with_name("play_store_proxy.py")
|
|
17
|
+
SETUP_SHA = "35f0cd9e9c1d7a643448223573b4a03879e63676fdc27872de3d0104d5bf89f9"
|
|
18
|
+
SETUP_URL = ("https://raw.githubusercontent.com/subosito/flutter-action/"
|
|
19
|
+
"1a449444c387b1966244ae4d4f8c696479add0b2/setup.sh")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SdkSetupTests(unittest.TestCase):
|
|
23
|
+
@classmethod
|
|
24
|
+
def setUpClass(cls):
|
|
25
|
+
fixture = os.environ.get("GOWALK_CACHE_ACTION_FIXTURES")
|
|
26
|
+
if fixture:
|
|
27
|
+
cls.source = (Path(fixture) / "flutter-setup.sh").read_bytes()
|
|
28
|
+
else:
|
|
29
|
+
with urllib.request.urlopen(SETUP_URL, timeout=30) as response:
|
|
30
|
+
cls.source = response.read(128 * 1024)
|
|
31
|
+
assert hashlib.sha256(cls.source).hexdigest() == SETUP_SHA
|
|
32
|
+
|
|
33
|
+
def setUp(self):
|
|
34
|
+
self.directory = tempfile.TemporaryDirectory()
|
|
35
|
+
self.addCleanup(self.directory.cleanup)
|
|
36
|
+
self.path = Path(self.directory.name)
|
|
37
|
+
self.setup = self.path / "setup.sh"
|
|
38
|
+
self.setup.write_bytes(self.source)
|
|
39
|
+
self.sdk = self.path / "sdk"
|
|
40
|
+
self.env = {**os.environ, "RUNNER_TEMP": str(self.path), "RUNNER_OS": "Linux", "RUNNER_ARCH": "X64",
|
|
41
|
+
"RUNNER_TOOL_CACHE": str(self.path / "tools"), "PUB_CACHE": str(self.path / "pub"),
|
|
42
|
+
"FLUTTER_STORAGE_BASE_URL": "http://provider.invalid", "GITHUB_ENV": str(self.path / "env"),
|
|
43
|
+
"GITHUB_OUTPUT": str(self.path / "output"), "GITHUB_PATH": str(self.path / "path")}
|
|
44
|
+
self.calls = []
|
|
45
|
+
body = io.BytesIO()
|
|
46
|
+
with tarfile.open(fileobj=body, mode="w:gz") as archive:
|
|
47
|
+
entry = tarfile.TarInfo("flutter/bin/flutter")
|
|
48
|
+
content = b"#!/bin/sh\nexit 0\n"
|
|
49
|
+
entry.size, entry.mode = len(content), 0o755
|
|
50
|
+
archive.addfile(entry, io.BytesIO(content))
|
|
51
|
+
self.archive = body.getvalue()
|
|
52
|
+
self.server = ThreadingHTTPServer(("127.0.0.1", 0), self.handler())
|
|
53
|
+
threading.Thread(target=self.server.serve_forever, daemon=True).start()
|
|
54
|
+
self.addCleanup(self.server.server_close)
|
|
55
|
+
self.addCleanup(self.server.shutdown)
|
|
56
|
+
self.env["GOOGLE_STORE_PROXY_URL"] = f"http://127.0.0.1:{self.server.server_port}"
|
|
57
|
+
|
|
58
|
+
def handler(self):
|
|
59
|
+
owner = self
|
|
60
|
+
|
|
61
|
+
class Handler(BaseHTTPRequestHandler):
|
|
62
|
+
def do_GET(self):
|
|
63
|
+
owner.calls.append(self.path)
|
|
64
|
+
if self.path.endswith(".json"):
|
|
65
|
+
data = json.dumps({"releases": [{"channel": "stable", "version": "3.44.0",
|
|
66
|
+
"dart_sdk_arch": "x64", "hash": "fixture", "sha256": "fixture",
|
|
67
|
+
"archive": "sdk.tar.gz"}]}).encode()
|
|
68
|
+
else:
|
|
69
|
+
data = owner.archive
|
|
70
|
+
self.send_response(200)
|
|
71
|
+
self.send_header("Content-Length", str(len(data)))
|
|
72
|
+
self.end_headers()
|
|
73
|
+
self.wfile.write(data)
|
|
74
|
+
|
|
75
|
+
def log_message(self, *_args):
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
return Handler
|
|
79
|
+
|
|
80
|
+
def bootstrap(self):
|
|
81
|
+
result = subprocess.run([sys.executable, "-S", str(SCRIPT), "--bootstrap-env"],
|
|
82
|
+
env=self.env, capture_output=True, text=True, timeout=10)
|
|
83
|
+
if result.returncode == 0:
|
|
84
|
+
self.env.update(line.split("=", 1) for line in (self.path / "env").read_text().splitlines())
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
def install(self):
|
|
88
|
+
return subprocess.run(["bash", str(self.setup), "-n", "3.44.0", "-a", "x64", "-c", str(self.sdk),
|
|
89
|
+
"stable"], env=self.env, capture_output=True, text=True, timeout=10)
|
|
90
|
+
|
|
91
|
+
def test_restored_sdk_fetches_only_manifest_through_proxy(self):
|
|
92
|
+
target = self.sdk / "flutter/bin/flutter"
|
|
93
|
+
target.parent.mkdir(parents=True)
|
|
94
|
+
target.write_text("#!/bin/sh\nexit 0\n")
|
|
95
|
+
target.chmod(0o755)
|
|
96
|
+
self.assertEqual(self.bootstrap().returncode, 0)
|
|
97
|
+
result = self.install()
|
|
98
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
99
|
+
self.assertEqual(self.calls, ["http://provider.invalid/flutter_infra_release/releases/releases_linux.json"])
|
|
100
|
+
|
|
101
|
+
def test_cold_cache_downloads_and_installs_sdk_through_proxy(self):
|
|
102
|
+
self.assertEqual(self.bootstrap().returncode, 0)
|
|
103
|
+
result = self.install()
|
|
104
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
105
|
+
self.assertEqual([url.rsplit("/", 1)[-1] for url in self.calls], ["releases_linux.json", "sdk.tar.gz"])
|
|
106
|
+
self.assertTrue(os.access(self.sdk / "flutter/bin/flutter", os.X_OK))
|
|
107
|
+
|
|
108
|
+
def test_provider_failure_after_cache_miss_still_fails_setup(self):
|
|
109
|
+
self.archive = b"not-a-valid-sdk-archive"
|
|
110
|
+
self.assertEqual(self.bootstrap().returncode, 0)
|
|
111
|
+
result = self.install()
|
|
112
|
+
self.assertNotEqual(result.returncode, 0)
|
|
113
|
+
self.assertFalse((self.sdk / "flutter/bin/flutter").exists())
|
|
114
|
+
self.assertEqual(len(self.calls), 2)
|
|
115
|
+
|
|
116
|
+
def test_missing_pin_refuses_before_manifest_even_with_restored_sdk(self):
|
|
117
|
+
self.env.pop("GOOGLE_STORE_PROXY_URL")
|
|
118
|
+
result = self.bootstrap()
|
|
119
|
+
self.assertNotEqual(result.returncode, 0)
|
|
120
|
+
self.assertFalse((self.path / "env").exists())
|
|
121
|
+
self.assertEqual(self.calls, [])
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
unittest.main()
|
|
@@ -16,16 +16,55 @@ class BootstrapWiringTests(unittest.TestCase):
|
|
|
16
16
|
for job in workflow["jobs"].values():
|
|
17
17
|
steps = job.get("steps", [])
|
|
18
18
|
for index, step in enumerate(steps):
|
|
19
|
-
if not step.get("uses", "").
|
|
19
|
+
if not step.get("uses", "").endswith("/android-app/flutter-setup"):
|
|
20
20
|
continue
|
|
21
21
|
checked += 1
|
|
22
|
-
guard = steps[index - 1]
|
|
23
|
-
self.assertEqual(guard.get("if"), step.get("if"))
|
|
24
22
|
self.assertEqual(job["env"]["GOOGLE_STORE_PROXY_URL"],
|
|
25
23
|
"${{ secrets.GOOGLE_STORE_PROXY_URL }}")
|
|
26
|
-
self.
|
|
27
|
-
self.assertNotIn("continue-on-error", guard)
|
|
24
|
+
self.assertNotIn("continue-on-error", step)
|
|
28
25
|
self.assertEqual(checked, 3)
|
|
26
|
+
setup = yaml.safe_load((ROOT / "android-action/flutter-setup/action.yml").read_text())
|
|
27
|
+
guard, scope, resolve, sdk_cache, pub_cache, install = setup["runs"]["steps"]
|
|
28
|
+
self.assertIn('play_store_proxy.py" --bootstrap-env', guard["run"])
|
|
29
|
+
self.assertNotIn("continue-on-error", guard)
|
|
30
|
+
self.assertTrue(scope["continue-on-error"])
|
|
31
|
+
self.assertIn("--snapshot", scope["run"])
|
|
32
|
+
self.assertEqual(resolve["with"]["dry-run"], "true")
|
|
33
|
+
for step in (resolve, install):
|
|
34
|
+
self.assertNotIn("env", step)
|
|
35
|
+
self.assertEqual(step["with"]["cache"], "false")
|
|
36
|
+
self.assertEqual(step["with"]["pub-cache"], "false")
|
|
37
|
+
self.assertNotIn("continue-on-error", step)
|
|
38
|
+
for step in (sdk_cache, pub_cache):
|
|
39
|
+
self.assertTrue(step["continue-on-error"])
|
|
40
|
+
self.assertIn("steps.scope.outputs.preloader", step["env"]["NODE_OPTIONS"])
|
|
41
|
+
self.assertEqual(step["env"]["SEGMENT_DOWNLOAD_TIMEOUT_MINS"], "1")
|
|
42
|
+
|
|
43
|
+
def test_official_flutter_custom_input_and_output_contract_is_preserved(self):
|
|
44
|
+
setup = yaml.safe_load((ROOT / "android-action/flutter-setup/action.yml").read_text())
|
|
45
|
+
inputs = {"channel", "flutter-version", "flutter-version-file", "architecture", "cache", "cache-key",
|
|
46
|
+
"cache-path", "pub-cache", "pub-cache-key", "pub-cache-path", "dry-run", "git-source"}
|
|
47
|
+
self.assertEqual(set(setup["inputs"]), inputs)
|
|
48
|
+
resolve = next(step for step in setup["runs"]["steps"] if step.get("id") == "flutter-action")
|
|
49
|
+
for name in inputs - {"cache", "pub-cache", "dry-run"}:
|
|
50
|
+
self.assertEqual(resolve["with"][name], "${{ inputs." + name + " }}")
|
|
51
|
+
self.assertEqual(setup["outputs"]["CACHE-HIT"]["value"], "${{ steps.cache-flutter.outputs.cache-hit }}")
|
|
52
|
+
self.assertEqual(setup["outputs"]["PUB-CACHE-HIT"]["value"], "${{ steps.cache-pub.outputs.cache-hit }}")
|
|
53
|
+
|
|
54
|
+
def test_pods_cache_keeps_identity_and_uses_the_same_optional_scope(self):
|
|
55
|
+
workflow = yaml.safe_load((ROOT / "templates/deploy.yml").read_text())
|
|
56
|
+
steps = workflow["jobs"]["ios"]["steps"]
|
|
57
|
+
cache = next(step for step in steps if step.get("name") == "Cache CocoaPods")
|
|
58
|
+
scope = next(step for step in steps if step.get("id") == "pods-cache-scope")
|
|
59
|
+
self.assertTrue(scope["continue-on-error"])
|
|
60
|
+
self.assertTrue(cache["continue-on-error"])
|
|
61
|
+
self.assertEqual(scope["if"], cache["if"])
|
|
62
|
+
self.assertIn("scripts/cache_scope.cjs --snapshot", scope["run"])
|
|
63
|
+
self.assertIn("steps.pods-cache-scope.outputs.preloader", cache["env"]["NODE_OPTIONS"])
|
|
64
|
+
self.assertEqual(cache["uses"], "actions/cache@caa296126883cff596d87d8935842f9db880ef25")
|
|
65
|
+
self.assertEqual(cache["with"]["path"], "ios/Pods\n~/Library/Caches/CocoaPods\n")
|
|
66
|
+
self.assertEqual(cache["with"]["key"],
|
|
67
|
+
"pods-${{ runner.os }}-${{ hashFiles('ios/Podfile.lock', 'pubspec.lock') }}")
|
|
29
68
|
|
|
30
69
|
def test_remaining_flutter_commands_run_inside_the_owned_relay(self):
|
|
31
70
|
groups = []
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.71
|
package/package.json
CHANGED
package/templates/deploy-web.yml
CHANGED
|
@@ -30,12 +30,7 @@ jobs:
|
|
|
30
30
|
GOOGLE_STORE_PROXY_URL: ${{ secrets.GOOGLE_STORE_PROXY_URL }}
|
|
31
31
|
steps:
|
|
32
32
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
33
|
-
-
|
|
34
|
-
shell: bash
|
|
35
|
-
run: python3 .github/actions/android-app/scripts/play_store_proxy.py --bootstrap-env
|
|
36
|
-
- uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
|
|
37
|
-
env:
|
|
38
|
-
SEGMENT_DOWNLOAD_TIMEOUT_MINS: "1"
|
|
33
|
+
- uses: ./.github/actions/android-app/flutter-setup
|
|
39
34
|
with:
|
|
40
35
|
channel: stable
|
|
41
36
|
cache: true
|
package/templates/deploy.yml
CHANGED
|
@@ -77,22 +77,25 @@ jobs:
|
|
|
77
77
|
else
|
|
78
78
|
echo "enabled=false" >> "$GITHUB_OUTPUT"
|
|
79
79
|
fi
|
|
80
|
-
- name: Require the Google proxy before Flutter SDK setup
|
|
81
|
-
if: ${{ steps.flutter.outputs.enabled == 'true' }}
|
|
82
|
-
shell: bash
|
|
83
|
-
run: python3 .github/actions/android-app/scripts/play_store_proxy.py --bootstrap-env
|
|
84
80
|
- name: Set up Flutter
|
|
85
81
|
if: ${{ steps.flutter.outputs.enabled == 'true' }}
|
|
86
|
-
uses:
|
|
87
|
-
env:
|
|
88
|
-
# Optional SDK/pub cache segments must finish promptly; normal SDK setup still fails on errors.
|
|
89
|
-
SEGMENT_DOWNLOAD_TIMEOUT_MINS: "1"
|
|
82
|
+
uses: ./.github/actions/android-app/flutter-setup
|
|
90
83
|
with:
|
|
91
84
|
channel: stable
|
|
92
85
|
cache: true
|
|
86
|
+
- name: Retain CocoaPods cache process scope
|
|
87
|
+
id: pods-cache-scope
|
|
88
|
+
if: ${{ steps.flutter.outputs.enabled == 'true' }}
|
|
89
|
+
continue-on-error: true
|
|
90
|
+
shell: bash
|
|
91
|
+
run: node .github/actions/android-app/scripts/cache_scope.cjs --snapshot
|
|
93
92
|
- name: Cache CocoaPods
|
|
94
93
|
if: ${{ steps.flutter.outputs.enabled == 'true' }}
|
|
95
|
-
uses: actions/cache@
|
|
94
|
+
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5, same pinned cache process as Flutter
|
|
95
|
+
continue-on-error: true
|
|
96
|
+
env:
|
|
97
|
+
NODE_OPTIONS: --require "${{ steps.pods-cache-scope.outputs.preloader }}"
|
|
98
|
+
SEGMENT_DOWNLOAD_TIMEOUT_MINS: "1"
|
|
96
99
|
with:
|
|
97
100
|
path: |
|
|
98
101
|
ios/Pods
|
|
@@ -335,15 +338,9 @@ jobs:
|
|
|
335
338
|
distribution: temurin
|
|
336
339
|
java-version: ${{ steps.jdk.outputs.version }}
|
|
337
340
|
cache: gradle
|
|
338
|
-
- name: Require the Google proxy before Flutter SDK setup
|
|
339
|
-
if: ${{ steps.mode.outputs.mode == 'local' && steps.flutter.outputs.enabled == 'true' }}
|
|
340
|
-
shell: bash
|
|
341
|
-
run: python3 .github/actions/android-app/scripts/play_store_proxy.py --bootstrap-env
|
|
342
341
|
- name: Set up Flutter
|
|
343
342
|
if: ${{ steps.mode.outputs.mode == 'local' && steps.flutter.outputs.enabled == 'true' }}
|
|
344
|
-
uses:
|
|
345
|
-
env:
|
|
346
|
-
SEGMENT_DOWNLOAD_TIMEOUT_MINS: "1"
|
|
343
|
+
uses: ./.github/actions/android-app/flutter-setup
|
|
347
344
|
with:
|
|
348
345
|
channel: stable
|
|
349
346
|
cache: true
|