gowalk-cicd 1.0.70 → 1.0.72
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 +25 -0
- package/README.md +9 -0
- package/action/.daemux-version +1 -1
- package/action/action.yml +18 -63
- package/action/scripts/artifact_env.cjs +10 -0
- package/action/scripts/cache_scope.cjs +81 -0
- package/action/scripts/resolve_info_plist.py +128 -0
- package/action/scripts/test_resolve_info_plist.py +120 -0
- package/android-action/.daemux-version +1 -1
- package/android-action/action.yml +2 -0
- package/android-action/flutter-setup/action.yml +149 -0
- package/android-action/scripts/artifact_env.cjs +10 -0
- package/android-action/scripts/cache_env.cjs +10 -0
- package/android-action/scripts/cache_scope.cjs +81 -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 +3 -6
- package/templates/deploy.yml +17 -16
package/CLAUDE.md
CHANGED
|
@@ -135,12 +135,30 @@ 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
|
|
141
151
|
errors still fail the job. Never use `continue-on-error` or a timeout around all SDK setup
|
|
142
152
|
to hide an optional cache failure. Do not alter cache keys or disable caches as part of
|
|
143
153
|
this timeout handling.
|
|
154
|
+
- GitHub artifact uploads have their own checksum-pinned `artifact_env.cjs` process scope.
|
|
155
|
+
It reuses the cache transport's approved HTTPS request guard, then removes inherited proxy
|
|
156
|
+
variables only inside the official `upload-artifact` process. Unknown programs/origins refuse;
|
|
157
|
+
later Apple/Google clients retain their proxies and artifact failures remain job failures.
|
|
158
|
+
The iOS action reads this helper from its immutable `SWIFT_APP_ACTION` snapshot. Its shared
|
|
159
|
+
`cache_scope.cjs` and `artifact_env.cjs` copies match the Android distribution byte for byte,
|
|
160
|
+
enforced by `test/artifact-wiring.test.mjs`; update both when changing that common guard.
|
|
161
|
+
Update existing consumer workflow artifact steps as well as action directories on adoption.
|
|
144
162
|
- Backend runtime credentials use the single encrypted `BACKEND_RUNTIME_ENV`
|
|
145
163
|
secret. The action writes it to host-side `.runtime.env` with mode 0600 and
|
|
146
164
|
supplies it to Compose after the generated `.env`; never print its content.
|
|
@@ -155,6 +173,13 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
155
173
|
is the only place that decides which; every downstream step branches on the
|
|
156
174
|
`project_kind` output rather than re-sniffing the repo. Flutter wins the tie
|
|
157
175
|
because a Flutter app also carries `android/settings.gradle`.
|
|
176
|
+
- iOS plist stamping reads complete JSON build settings through `resolve_info_plist.py`,
|
|
177
|
+
preserving Xcode's exit status and bounded sanitized stderr. Select exactly the configured
|
|
178
|
+
bundle's application target and resolve paths from that target's source root; never take
|
|
179
|
+
the first dependency plist or stop reading Xcode output early. The encryption step reuses
|
|
180
|
+
the validated source path. Missing/ambiguous source plists fail; positively generated
|
|
181
|
+
plists retain the existing build-setting version path. Queries disable signing and package
|
|
182
|
+
updates, retain inherited proxies, and never enable provisioning network operations.
|
|
158
183
|
- Version stamping is per-build-system: Flutter takes `--build-number`, Gradle
|
|
159
184
|
has no equivalent, so `set_gradle_version()` rewrites the literal in the
|
|
160
185
|
checkout the way the iOS action patches the pbxproj. Never commit that edit.
|
package/README.md
CHANGED
|
@@ -647,6 +647,15 @@ without the repository secret cannot start provider downloads through these work
|
|
|
647
647
|
The Apple uploader retains `apple-upload-<run>-<attempt>` receipts with IPA SHA-256,
|
|
648
648
|
upload ID and processing readback, and resumes matching bytes after interruption.
|
|
649
649
|
|
|
650
|
+
GitHub artifact traffic uses a separate, per-action transport scope. The exact pinned
|
|
651
|
+
`actions/upload-artifact` client can reach approved GitHub Results and artifact-storage
|
|
652
|
+
HTTPS origins without inheriting the store proxy; unknown programs and destinations are
|
|
653
|
+
refused. The scope ends with that Node process. Apple/Google calls retain their assigned
|
|
654
|
+
exit, and a failed artifact creation, transfer or finalization still fails delivery.
|
|
655
|
+
Adoption requires updating workflow upload steps as well as the distributed actions while
|
|
656
|
+
preserving each app's custom checks and artifact retention settings. This fixes the proven
|
|
657
|
+
inherited-proxy path; an isolated `ECONNRESET` does not identify every possible network fault.
|
|
658
|
+
|
|
650
659
|
1. **Auto-detects** your `.xcodeproj` / `.xcworkspace`, scheme, bundle ID,
|
|
651
660
|
and `team_id` (from the ASC API key). No `ci.config.yaml` required —
|
|
652
661
|
override via action inputs only if auto-detection fails.
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.72
|
package/action/action.yml
CHANGED
|
@@ -601,51 +601,14 @@ runs:
|
|
|
601
601
|
WORKSPACE: ${{ inputs.workspace || env.CFG_WORKSPACE }}
|
|
602
602
|
SCHEME: ${{ inputs.scheme || env.CFG_SCHEME }}
|
|
603
603
|
CONFIGURATION: ${{ inputs.configuration || env.CFG_CONFIGURATION }}
|
|
604
|
+
BUNDLE_ID: ${{ inputs.bundle-id || env.CFG_BUNDLE_ID }}
|
|
604
605
|
run: |
|
|
605
606
|
set -o pipefail
|
|
606
|
-
#
|
|
607
|
-
#
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
# `previousBundleVersion`. Stamp the resolved source Info.plist directly
|
|
612
|
-
# so the archive ships with the computed values regardless of whether
|
|
613
|
-
# the plist uses substitutions or hardcoded literals.
|
|
614
|
-
if [ -n "$WORKSPACE" ]; then
|
|
615
|
-
PROJ_ARGS=(-workspace "$WORKSPACE")
|
|
616
|
-
PROJ_BASE=$(dirname "$WORKSPACE")
|
|
617
|
-
else
|
|
618
|
-
PROJ_ARGS=(-project "$PROJECT")
|
|
619
|
-
PROJ_BASE=$(dirname "$PROJECT")
|
|
620
|
-
fi
|
|
621
|
-
# Try agvtool first — it's Apple's official tool and handles projects
|
|
622
|
-
# using the generic versioning system. Harmless no-op on projects
|
|
623
|
-
# without VERSIONING_SYSTEM set. Run from the project directory.
|
|
624
|
-
# Marketing version is intentionally NOT stamped here: the project's
|
|
625
|
-
# MARKETING_VERSION build setting is the single source of truth and
|
|
626
|
-
# was already resolved into env earlier; CI must never mutate it.
|
|
627
|
-
(
|
|
628
|
-
cd "$PROJ_BASE" 2>/dev/null || exit 0
|
|
629
|
-
xcrun agvtool new-version -all "$BUILD_NUMBER" >/dev/null 2>&1 || true
|
|
630
|
-
)
|
|
631
|
-
# Now resolve the actual Info.plist path and stamp literal values.
|
|
632
|
-
# This is idempotent with agvtool and also covers projects where the
|
|
633
|
-
# plist has hardcoded numbers (the sample1 failure mode).
|
|
634
|
-
INFOPLIST=$(xcodebuild "${PROJ_ARGS[@]}" -scheme "$SCHEME" -configuration "${CONFIGURATION:-Release}" \
|
|
635
|
-
-showBuildSettings 2>/dev/null \
|
|
636
|
-
| awk -F' = ' '/^[[:space:]]*INFOPLIST_FILE/ {
|
|
637
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2; exit
|
|
638
|
-
}')
|
|
639
|
-
if [ -z "$INFOPLIST" ]; then
|
|
640
|
-
echo "::warning::INFOPLIST_FILE not set for scheme $SCHEME; skipping version stamping"
|
|
641
|
-
exit 0
|
|
642
|
-
fi
|
|
643
|
-
INFOPLIST_PATH="$PROJ_BASE/$INFOPLIST"
|
|
644
|
-
if [ ! -f "$INFOPLIST_PATH" ]; then
|
|
645
|
-
INFOPLIST_PATH="$INFOPLIST"
|
|
646
|
-
fi
|
|
647
|
-
if [ ! -f "$INFOPLIST_PATH" ]; then
|
|
648
|
-
echo "::warning::Info.plist not found at $INFOPLIST_PATH; skipping version stamping"
|
|
607
|
+
# Read every target's complete settings and preserve Xcode's actual status/error.
|
|
608
|
+
# The selected application's source root can differ from its containing workspace.
|
|
609
|
+
INFOPLIST_PATH=$(python3 "$SWIFT_APP_ACTION/scripts/resolve_info_plist.py")
|
|
610
|
+
if [ -z "$INFOPLIST_PATH" ]; then
|
|
611
|
+
echo "Generated Info.plist: Archive supplies CURRENT_PROJECT_VERSION and MARKETING_VERSION"
|
|
649
612
|
exit 0
|
|
650
613
|
fi
|
|
651
614
|
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" "$INFOPLIST_PATH" 2>/dev/null \
|
|
@@ -665,29 +628,15 @@ runs:
|
|
|
665
628
|
VALUE: ${{ inputs.uses-non-exempt-encryption || env.CFG_USES_NON_EXEMPT }}
|
|
666
629
|
run: |
|
|
667
630
|
set -o pipefail
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
PROJ_ARGS=(-project "$PROJECT")
|
|
673
|
-
PROJ_BASE=$(dirname "$PROJECT")
|
|
674
|
-
fi
|
|
675
|
-
INFOPLIST=$(xcodebuild "${PROJ_ARGS[@]}" -scheme "$SCHEME" -configuration "${CONFIGURATION:-Release}" \
|
|
676
|
-
-showBuildSettings 2>/dev/null \
|
|
677
|
-
| awk -F' = ' '/^[[:space:]]*INFOPLIST_FILE/ {
|
|
678
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2; exit
|
|
679
|
-
}')
|
|
680
|
-
if [ -z "$INFOPLIST" ]; then
|
|
681
|
-
echo "::warning::INFOPLIST_FILE not set for scheme $SCHEME; skipping ITSAppUsesNonExemptEncryption"
|
|
631
|
+
# Reuse the validated application path from version stamping; no second Xcode query.
|
|
632
|
+
INFOPLIST_PATH="${SWIFT_APP_SOURCE_INFOPLIST?Source plist resolution did not complete}"
|
|
633
|
+
if [ -z "$INFOPLIST_PATH" ]; then
|
|
634
|
+
echo "::warning::The application generates Info.plist; no source plist to edit for encryption"
|
|
682
635
|
exit 0
|
|
683
636
|
fi
|
|
684
|
-
INFOPLIST_PATH="$PROJ_BASE/$INFOPLIST"
|
|
685
|
-
if [ ! -f "$INFOPLIST_PATH" ]; then
|
|
686
|
-
INFOPLIST_PATH="$INFOPLIST"
|
|
687
|
-
fi
|
|
688
637
|
if [ ! -f "$INFOPLIST_PATH" ]; then
|
|
689
|
-
echo "::
|
|
690
|
-
exit
|
|
638
|
+
echo "::error::The resolved application Info.plist is no longer present"
|
|
639
|
+
exit 1
|
|
691
640
|
fi
|
|
692
641
|
/usr/libexec/PlistBuddy -c "Set :ITSAppUsesNonExemptEncryption $VALUE" "$INFOPLIST_PATH" 2>/dev/null \
|
|
693
642
|
|| /usr/libexec/PlistBuddy -c "Add :ITSAppUsesNonExemptEncryption bool $VALUE" "$INFOPLIST_PATH"
|
|
@@ -815,6 +764,8 @@ runs:
|
|
|
815
764
|
- name: Preserve the Firebase console upload artifact
|
|
816
765
|
if: ${{ always() && inputs.archive == 'true' }}
|
|
817
766
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
|
767
|
+
env:
|
|
768
|
+
NODE_OPTIONS: --require "${{ env.SWIFT_APP_ACTION }}/scripts/artifact_env.cjs"
|
|
818
769
|
with:
|
|
819
770
|
name: ios-crashlytics-symbols-${{ github.run_id }}-${{ github.run_attempt }}
|
|
820
771
|
path: ${{ runner.temp }}/firebase-symbols/
|
|
@@ -834,6 +785,8 @@ runs:
|
|
|
834
785
|
- name: Retain Apple upload receipt
|
|
835
786
|
if: ${{ always() && inputs.archive == 'true' && inputs.upload == 'true' }}
|
|
836
787
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
|
788
|
+
env:
|
|
789
|
+
NODE_OPTIONS: --require "${{ env.SWIFT_APP_ACTION }}/scripts/artifact_env.cjs"
|
|
837
790
|
with:
|
|
838
791
|
name: apple-upload-${{ github.run_id }}-${{ github.run_attempt }}
|
|
839
792
|
path: ${{ runner.temp }}/apple-build-upload.json
|
|
@@ -845,6 +798,8 @@ runs:
|
|
|
845
798
|
- name: Retain the exported IPA for upload recovery
|
|
846
799
|
if: ${{ always() && inputs.archive == 'true' && inputs.upload == 'true' && steps.upload.outcome != 'success' }}
|
|
847
800
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
|
801
|
+
env:
|
|
802
|
+
NODE_OPTIONS: --require "${{ env.SWIFT_APP_ACTION }}/scripts/artifact_env.cjs"
|
|
848
803
|
with:
|
|
849
804
|
name: apple-ipa-${{ github.run_id }}-${{ github.run_attempt }}
|
|
850
805
|
path: ${{ runner.temp }}/export/*.ipa
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Only the exact pinned upload-artifact process receives this GitHub-specific transport.
|
|
2
|
+
try {
|
|
3
|
+
const scope = require('./cache_scope.cjs');
|
|
4
|
+
scope.authorize(process.argv[1], process.env, 'artifact');
|
|
5
|
+
scope.guardRequests();
|
|
6
|
+
for (const key of scope.proxyKeys) delete process.env[key];
|
|
7
|
+
} catch {
|
|
8
|
+
process.stderr.write('github_artifact_transport_scope_refused\n');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Direct routing belongs only to pinned first-party Actions programs and approved 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 artifactHash = '168c44946cba03564808c19b83a72601e6d1d6f57081dbd2d6e2ea1cedc522f5';
|
|
10
|
+
const proxyKeys = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'ALL_PROXY', 'all_proxy'];
|
|
11
|
+
const refusal = () => new Error('github_cache_transport_scope_refused');
|
|
12
|
+
|
|
13
|
+
function allowedHost(host) {
|
|
14
|
+
return host === 'api.github.com'
|
|
15
|
+
|| /^[a-z0-9.-]+\.actions\.githubusercontent\.com$/.test(host)
|
|
16
|
+
|| /^[a-z0-9]{3,24}\.blob\.core\.windows\.net$/.test(host);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function requireOrigin(value, service = false) {
|
|
20
|
+
let url;
|
|
21
|
+
try { url = new URL(value); } catch { throw refusal(); }
|
|
22
|
+
if (url.protocol !== 'https:' || url.port || url.username || url.password || !allowedHost(url.hostname)
|
|
23
|
+
|| (service && !url.hostname.endsWith('.actions.githubusercontent.com'))) throw refusal();
|
|
24
|
+
return url;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function authorize(entrypoint, env, kind = 'cache') {
|
|
28
|
+
if (!entrypoint || basename(entrypoint) !== 'index.js' || basename(dirname(dirname(entrypoint))) !== 'dist') {
|
|
29
|
+
throw refusal();
|
|
30
|
+
}
|
|
31
|
+
if (!['cache', 'artifact'].includes(kind)) throw refusal();
|
|
32
|
+
const program = basename(dirname(entrypoint));
|
|
33
|
+
const expected = kind === 'artifact' ? (program === 'upload' && artifactHash) : hashes.get(program);
|
|
34
|
+
if (!expected || createHash('sha256').update(readFileSync(entrypoint)).digest('hex') !== expected) throw refusal();
|
|
35
|
+
const origins = (kind === 'artifact' ? [env.ACTIONS_RESULTS_URL]
|
|
36
|
+
: [env.ACTIONS_RESULTS_URL, env.ACTIONS_CACHE_URL]).filter(Boolean);
|
|
37
|
+
if (!origins.length) throw refusal();
|
|
38
|
+
for (const origin of origins) requireOrigin(origin, true);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function requireRequest(args, protocol) {
|
|
42
|
+
const [input, extra] = args;
|
|
43
|
+
const options = typeof input === 'string' || input instanceof URL
|
|
44
|
+
? {protocol, ...require('node:url').urlToHttpOptions(requireOrigin(input)),
|
|
45
|
+
...(typeof extra === 'object' ? extra : {})}
|
|
46
|
+
: {...input};
|
|
47
|
+
if (!options || options.socketPath || (options.protocol || protocol) !== 'https:') throw refusal();
|
|
48
|
+
for (const host of [options.hostname, options.host].filter(Boolean)) {
|
|
49
|
+
requireOrigin(`https://${host}${options.port ? ':' + options.port : ''}`);
|
|
50
|
+
}
|
|
51
|
+
if (!options.hostname && !options.host) throw refusal();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function guardRequests() {
|
|
55
|
+
for (const [name, protocol] of [['node:http', 'http:'], ['node:https', 'https:']]) {
|
|
56
|
+
const client = require(name);
|
|
57
|
+
for (const verb of ['request', 'get']) {
|
|
58
|
+
const original = client[verb];
|
|
59
|
+
client[verb] = function (...args) { requireRequest(args, protocol); return original.apply(this, args); };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (globalThis.fetch) {
|
|
63
|
+
const original = globalThis.fetch;
|
|
64
|
+
globalThis.fetch = function (input, init) {
|
|
65
|
+
requireOrigin(typeof input === 'string' || input instanceof URL ? input : input.url);
|
|
66
|
+
return original.call(this, input, {...init, redirect: 'error'});
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function snapshot() {
|
|
72
|
+
const target = mkdtempSync(join(process.env.RUNNER_TEMP, 'gowalk-cache-scope-'));
|
|
73
|
+
for (const file of ['cache_env.cjs', 'cache_scope.cjs']) copyFileSync(join(__dirname, file), join(target, file));
|
|
74
|
+
appendFileSync(process.env.GITHUB_OUTPUT, `preloader=${join(target, 'cache_env.cjs')}\n`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {authorize, guardRequests, proxyKeys, requireOrigin};
|
|
78
|
+
if (require.main === module) {
|
|
79
|
+
try { if (process.argv[2] !== '--snapshot') throw refusal(); snapshot(); }
|
|
80
|
+
catch { process.stderr.write('github_cache_scope_snapshot_failed\n'); process.exitCode = 1; }
|
|
81
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read complete Xcode settings once and select the requested application's source plist."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
APP_TYPE = "com.apple.product-type.application"
|
|
13
|
+
_VARIABLE = re.compile(r"\$\(([^)]+)\)|\$\{([^}]+)\}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SettingsFailed(RuntimeError):
|
|
17
|
+
def __init__(self, message: str, code: int = 1):
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.code = code
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def diagnostic(text: str) -> str:
|
|
23
|
+
for key, value in os.environ.items():
|
|
24
|
+
if len(value) >= 4 and re.search(r"SECRET|TOKEN|PASSWORD|PROXY|PRIVATE_KEY", key, re.I):
|
|
25
|
+
text = text.replace(value, "[redacted]")
|
|
26
|
+
text = re.sub(r"https?://[^\s]+", "[URL redacted]", text)
|
|
27
|
+
text = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", text)
|
|
28
|
+
return "\n".join("xcodebuild: " + line for line in text[-8000:].splitlines()[-60:])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_settings(project: str, workspace: str, scheme: str, configuration: str, logs: Path) -> list:
|
|
32
|
+
if not scheme or not (workspace or project):
|
|
33
|
+
raise SettingsFailed("Project/workspace and scheme are required for source plist resolution")
|
|
34
|
+
command = ["xcodebuild", "-workspace" if workspace else "-project", workspace or project,
|
|
35
|
+
"-scheme", scheme, "-configuration", configuration or "Release", "-sdk", "iphoneos",
|
|
36
|
+
"-destination", "generic/platform=iOS", "-disableAutomaticPackageResolution", "-skipPackageUpdates",
|
|
37
|
+
"CODE_SIGNING_ALLOWED=NO", "CODE_SIGNING_REQUIRED=NO", "CODE_SIGN_IDENTITY=", "DEVELOPMENT_TEAM=",
|
|
38
|
+
"-showBuildSettings", "-json"]
|
|
39
|
+
logs.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
40
|
+
os.chmod(logs, 0o700)
|
|
41
|
+
output, errors = logs / "build-settings.json", logs / "build-settings.stderr"
|
|
42
|
+
# File sinks consume the entire producer output. An early pipe reader can hide its actual status.
|
|
43
|
+
with output.open("wb") as stdout, errors.open("wb") as stderr:
|
|
44
|
+
os.chmod(output, 0o600)
|
|
45
|
+
os.chmod(errors, 0o600)
|
|
46
|
+
timed_out = False
|
|
47
|
+
try:
|
|
48
|
+
result = subprocess.run(command, stdout=stdout, stderr=stderr, timeout=300, check=False)
|
|
49
|
+
code = result.returncode if result.returncode >= 0 else 128 - result.returncode
|
|
50
|
+
except subprocess.TimeoutExpired:
|
|
51
|
+
timed_out, code = True, 124
|
|
52
|
+
if code:
|
|
53
|
+
detail = diagnostic(errors.read_text(errors="replace"))
|
|
54
|
+
if detail:
|
|
55
|
+
print(detail, file=sys.stderr)
|
|
56
|
+
reason = "timed out after 300 seconds" if timed_out else f"failed with exit {code}"
|
|
57
|
+
raise SettingsFailed(f"xcodebuild -showBuildSettings {reason}", code)
|
|
58
|
+
try:
|
|
59
|
+
settings = json.loads(output.read_text())
|
|
60
|
+
except (ValueError, UnicodeError):
|
|
61
|
+
raise SettingsFailed("xcodebuild returned invalid build-settings JSON") from None
|
|
62
|
+
if not isinstance(settings, list):
|
|
63
|
+
raise SettingsFailed("xcodebuild build-settings response must be a target list")
|
|
64
|
+
return settings
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def application(settings: list, bundle_id: str) -> dict:
|
|
68
|
+
candidates = []
|
|
69
|
+
for row in settings:
|
|
70
|
+
values = row.get("buildSettings") if isinstance(row, dict) else None
|
|
71
|
+
if not isinstance(values, dict) or values.get("PRODUCT_TYPE") != APP_TYPE:
|
|
72
|
+
continue
|
|
73
|
+
if not bundle_id or values.get("PRODUCT_BUNDLE_IDENTIFIER") == bundle_id:
|
|
74
|
+
candidates.append(values)
|
|
75
|
+
if len(candidates) != 1:
|
|
76
|
+
raise SettingsFailed("Expected exactly one application target matching the configured bundle identifier")
|
|
77
|
+
return candidates[0]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def source_plist(settings: dict, checkout: Path) -> Path | None:
|
|
81
|
+
value = str(settings.get("INFOPLIST_FILE") or "").strip()
|
|
82
|
+
if not value:
|
|
83
|
+
if settings.get("GENERATE_INFOPLIST_FILE") == "YES":
|
|
84
|
+
return None
|
|
85
|
+
raise SettingsFailed("The application has no source Info.plist and does not enable generated Info.plist")
|
|
86
|
+
for _ in range(5):
|
|
87
|
+
expanded = _VARIABLE.sub(lambda match: str(settings.get(match[1] or match[2], match[0])), value)
|
|
88
|
+
if expanded == value:
|
|
89
|
+
break
|
|
90
|
+
value = expanded
|
|
91
|
+
if _VARIABLE.search(value) or "\n" in value or "\r" in value:
|
|
92
|
+
raise SettingsFailed("The application's Info.plist path contains an unresolved or invalid setting")
|
|
93
|
+
path = Path(value)
|
|
94
|
+
if not path.is_absolute():
|
|
95
|
+
source = settings.get("SRCROOT") or settings.get("PROJECT_DIR")
|
|
96
|
+
if not source:
|
|
97
|
+
raise SettingsFailed("The application target did not provide its source root")
|
|
98
|
+
path = Path(source) / path
|
|
99
|
+
path = path.resolve()
|
|
100
|
+
if "\n" in str(path) or "\r" in str(path):
|
|
101
|
+
raise SettingsFailed("The resolved source plist cannot be represented in the job environment")
|
|
102
|
+
if not path.is_relative_to(checkout.resolve()) or not path.is_file():
|
|
103
|
+
raise SettingsFailed("The application source Info.plist is missing or outside the checkout")
|
|
104
|
+
return path
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main() -> int:
|
|
108
|
+
try:
|
|
109
|
+
logs = Path(os.environ["RUNNER_TEMP"]) / "swift-app-build-settings"
|
|
110
|
+
rows = read_settings(os.environ.get("PROJECT", ""), os.environ.get("WORKSPACE", ""),
|
|
111
|
+
os.environ.get("SCHEME", ""), os.environ.get("CONFIGURATION", "Release"), logs)
|
|
112
|
+
selected = source_plist(application(rows, os.environ.get("BUNDLE_ID", "")), Path.cwd())
|
|
113
|
+
value = str(selected) if selected else ""
|
|
114
|
+
with Path(os.environ["GITHUB_ENV"]).open("a") as stream:
|
|
115
|
+
stream.write(f"SWIFT_APP_SOURCE_INFOPLIST={value}\n")
|
|
116
|
+
print(value)
|
|
117
|
+
return 0
|
|
118
|
+
except SettingsFailed as exc:
|
|
119
|
+
print(f"::error title=Application build settings::{exc}", file=sys.stderr)
|
|
120
|
+
return exc.code
|
|
121
|
+
except (KeyError, OSError):
|
|
122
|
+
print("::error title=Application build settings::Required settings tooling or private output is unavailable",
|
|
123
|
+
file=sys.stderr)
|
|
124
|
+
return 1
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Consume real producer output/status, then select only the app's own source plist."""
|
|
2
|
+
from contextlib import redirect_stderr
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
import resolve_info_plist as subject
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ResolveInfoPlistTest(unittest.TestCase):
|
|
15
|
+
def setUp(self):
|
|
16
|
+
self.temp = tempfile.TemporaryDirectory()
|
|
17
|
+
self.addCleanup(self.temp.cleanup)
|
|
18
|
+
self.root = Path(self.temp.name)
|
|
19
|
+
self.source = self.root / 'Nested Project' / 'Application Info.plist'
|
|
20
|
+
self.source.parent.mkdir()
|
|
21
|
+
self.source.write_text('<plist/>')
|
|
22
|
+
self.values = {'PRODUCT_TYPE': subject.APP_TYPE, 'PRODUCT_BUNDLE_IDENTIFIER': 'test.local.app',
|
|
23
|
+
'SRCROOT': str(self.source.parent), 'INFOPLIST_FILE': self.source.name}
|
|
24
|
+
|
|
25
|
+
def producer(self, body):
|
|
26
|
+
tool = self.root / 'xcodebuild'
|
|
27
|
+
tool.write_text('#!/usr/bin/env python3\n' + body)
|
|
28
|
+
tool.chmod(0o700)
|
|
29
|
+
return patch.dict(os.environ, {'PATH': str(self.root) + os.pathsep + os.environ['PATH']})
|
|
30
|
+
|
|
31
|
+
def test_drains_large_output_and_preserves_arguments_and_inherited_proxy(self):
|
|
32
|
+
record = self.root / 'invocation.json'
|
|
33
|
+
rows = [{'buildSettings': dict(self.values, LARGE='x' * 500000)}]
|
|
34
|
+
body = ("import json,os,sys\n"
|
|
35
|
+
f"open({str(record)!r},'w').write(json.dumps([sys.argv[1:],os.getenv('HTTPS_PROXY')]))\n"
|
|
36
|
+
f"sys.stdout.write({json.dumps(rows)!r})\n")
|
|
37
|
+
with self.producer(body), patch.dict(os.environ, {'HTTPS_PROXY': 'http://fixture-proxy.invalid:9000'}):
|
|
38
|
+
result = subject.read_settings('App.xcodeproj', '', 'App', 'Release', self.root / 'logs')
|
|
39
|
+
self.assertEqual(result, rows)
|
|
40
|
+
args, proxy = json.loads(record.read_text())
|
|
41
|
+
self.assertEqual(proxy, 'http://fixture-proxy.invalid:9000')
|
|
42
|
+
self.assertIn('-disableAutomaticPackageResolution', args)
|
|
43
|
+
self.assertIn('-skipPackageUpdates', args)
|
|
44
|
+
self.assertIn('CODE_SIGNING_ALLOWED=NO', args)
|
|
45
|
+
self.assertEqual(args[-2:], ['-showBuildSettings', '-json'])
|
|
46
|
+
self.assertNotIn('-allowProvisioningUpdates', args)
|
|
47
|
+
|
|
48
|
+
def test_exit_74_is_not_an_empty_success_and_error_is_bounded_private(self):
|
|
49
|
+
stderr = io.StringIO()
|
|
50
|
+
error = 'x' * 20000 + '\nCould not resolve package: https://private.invalid/token-secret token-secret\n'
|
|
51
|
+
body = f"import sys\nsys.stderr.write({error!r})\nsys.stdout.write('[]')\nsys.exit(74)\n"
|
|
52
|
+
with self.producer(body), patch.dict(os.environ, {'APP_SECRET': 'token-secret'}), redirect_stderr(stderr):
|
|
53
|
+
with self.assertRaises(subject.SettingsFailed) as raised:
|
|
54
|
+
subject.read_settings('App.xcodeproj', '', 'App', 'Release', self.root / 'logs')
|
|
55
|
+
self.assertEqual(raised.exception.code, 74)
|
|
56
|
+
self.assertIn('Could not resolve package', stderr.getvalue())
|
|
57
|
+
self.assertNotIn('token-secret', stderr.getvalue())
|
|
58
|
+
self.assertNotIn('private.invalid', stderr.getvalue())
|
|
59
|
+
self.assertLess(len(stderr.getvalue()), 8200)
|
|
60
|
+
self.assertTrue((self.root / 'logs' / 'build-settings.stderr').is_file())
|
|
61
|
+
for output in ['not-json', '{}']:
|
|
62
|
+
with self.producer(f"print({output!r})\n"):
|
|
63
|
+
with self.assertRaises(subject.SettingsFailed):
|
|
64
|
+
subject.read_settings('App.xcodeproj', '', 'App', 'Release', self.root / 'logs')
|
|
65
|
+
|
|
66
|
+
def test_selects_bundle_matched_application_not_first_dependency_or_other_app(self):
|
|
67
|
+
dependency = dict(self.values, PRODUCT_TYPE='com.apple.product-type.framework', INFOPLIST_FILE='Other.plist')
|
|
68
|
+
other = dict(self.values, PRODUCT_BUNDLE_IDENTIFIER='test.local.other')
|
|
69
|
+
rows = [{'buildSettings': row} for row in [dependency, other, self.values]]
|
|
70
|
+
chosen = subject.application(rows, 'test.local.app')
|
|
71
|
+
self.assertEqual(subject.source_plist(chosen, self.root), self.source.resolve())
|
|
72
|
+
with self.assertRaises(subject.SettingsFailed):
|
|
73
|
+
subject.application(rows, '')
|
|
74
|
+
with self.assertRaises(subject.SettingsFailed):
|
|
75
|
+
subject.application(rows, 'test.local.absent')
|
|
76
|
+
|
|
77
|
+
def test_ambiguous_same_bundle_and_malformed_targets_refuse(self):
|
|
78
|
+
for rows in [[{'buildSettings': self.values}] * 2, [{'buildSettings': []}], [None]]:
|
|
79
|
+
with self.assertRaises(subject.SettingsFailed):
|
|
80
|
+
subject.application(rows, 'test.local.app')
|
|
81
|
+
|
|
82
|
+
def test_relative_and_variable_plist_paths_use_the_target_root(self):
|
|
83
|
+
values = dict(self.values, INFOPLIST_FILE='$(SRCROOT)/Application Info.plist')
|
|
84
|
+
self.assertEqual(subject.source_plist(values, self.root), self.source.resolve())
|
|
85
|
+
values['INFOPLIST_FILE'] = '${UNKNOWN}/Info.plist'
|
|
86
|
+
with self.assertRaises(subject.SettingsFailed):
|
|
87
|
+
subject.source_plist(values, self.root)
|
|
88
|
+
newline_root = self.root / 'project\nname'
|
|
89
|
+
newline_root.mkdir()
|
|
90
|
+
(newline_root / self.source.name).write_text('<plist/>')
|
|
91
|
+
values = dict(self.values, SRCROOT=str(newline_root))
|
|
92
|
+
with self.assertRaises(subject.SettingsFailed):
|
|
93
|
+
subject.source_plist(values, self.root)
|
|
94
|
+
|
|
95
|
+
def test_missing_outside_and_generated_paths_are_distinct(self):
|
|
96
|
+
for values in [dict(self.values, INFOPLIST_FILE='Missing.plist'),
|
|
97
|
+
dict(self.values, INFOPLIST_FILE='/etc/hosts'),
|
|
98
|
+
dict(self.values, INFOPLIST_FILE='')]:
|
|
99
|
+
with self.assertRaises(subject.SettingsFailed):
|
|
100
|
+
subject.source_plist(values, self.root)
|
|
101
|
+
generated = dict(self.values, INFOPLIST_FILE='', GENERATE_INFOPLIST_FILE='YES')
|
|
102
|
+
self.assertIsNone(subject.source_plist(generated, self.root))
|
|
103
|
+
|
|
104
|
+
def test_timeout_keeps_error_evidence_and_does_not_retry(self):
|
|
105
|
+
import subprocess
|
|
106
|
+
|
|
107
|
+
stderr = io.StringIO()
|
|
108
|
+
def timed_out(command, **kwargs):
|
|
109
|
+
kwargs['stderr'].write(b'package graph was unavailable')
|
|
110
|
+
raise subprocess.TimeoutExpired(command, 300)
|
|
111
|
+
with patch.object(subject.subprocess, 'run', side_effect=timed_out) as run, redirect_stderr(stderr):
|
|
112
|
+
with self.assertRaises(subject.SettingsFailed) as raised:
|
|
113
|
+
subject.read_settings('App.xcodeproj', '', 'App', 'Release', self.root / 'logs')
|
|
114
|
+
self.assertEqual(raised.exception.code, 124)
|
|
115
|
+
self.assertEqual(run.call_count, 1)
|
|
116
|
+
self.assertIn('package graph was unavailable', stderr.getvalue())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == '__main__':
|
|
120
|
+
unittest.main()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.72
|
|
@@ -191,6 +191,8 @@ runs:
|
|
|
191
191
|
- name: Retain Dart symbol files
|
|
192
192
|
if: ${{ steps.config.outputs.project_kind == 'flutter' }}
|
|
193
193
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
|
194
|
+
env:
|
|
195
|
+
NODE_OPTIONS: --require "${{ github.action_path }}/scripts/artifact_env.cjs"
|
|
194
196
|
with:
|
|
195
197
|
name: android-symbols-${{ steps.config.outputs.package_name }}-${{ steps.config.outputs.build_number }}
|
|
196
198
|
path: ${{ steps.flutter_build.outputs.symbols_path }}
|
|
@@ -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
|
+
// Only the exact pinned upload-artifact process receives this GitHub-specific transport.
|
|
2
|
+
try {
|
|
3
|
+
const scope = require('./cache_scope.cjs');
|
|
4
|
+
scope.authorize(process.argv[1], process.env, 'artifact');
|
|
5
|
+
scope.guardRequests();
|
|
6
|
+
for (const key of scope.proxyKeys) delete process.env[key];
|
|
7
|
+
} catch {
|
|
8
|
+
process.stderr.write('github_artifact_transport_scope_refused\n');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
@@ -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,81 @@
|
|
|
1
|
+
// Direct routing belongs only to pinned first-party Actions programs and approved 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 artifactHash = '168c44946cba03564808c19b83a72601e6d1d6f57081dbd2d6e2ea1cedc522f5';
|
|
10
|
+
const proxyKeys = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'ALL_PROXY', 'all_proxy'];
|
|
11
|
+
const refusal = () => new Error('github_cache_transport_scope_refused');
|
|
12
|
+
|
|
13
|
+
function allowedHost(host) {
|
|
14
|
+
return host === 'api.github.com'
|
|
15
|
+
|| /^[a-z0-9.-]+\.actions\.githubusercontent\.com$/.test(host)
|
|
16
|
+
|| /^[a-z0-9]{3,24}\.blob\.core\.windows\.net$/.test(host);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function requireOrigin(value, service = false) {
|
|
20
|
+
let url;
|
|
21
|
+
try { url = new URL(value); } catch { throw refusal(); }
|
|
22
|
+
if (url.protocol !== 'https:' || url.port || url.username || url.password || !allowedHost(url.hostname)
|
|
23
|
+
|| (service && !url.hostname.endsWith('.actions.githubusercontent.com'))) throw refusal();
|
|
24
|
+
return url;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function authorize(entrypoint, env, kind = 'cache') {
|
|
28
|
+
if (!entrypoint || basename(entrypoint) !== 'index.js' || basename(dirname(dirname(entrypoint))) !== 'dist') {
|
|
29
|
+
throw refusal();
|
|
30
|
+
}
|
|
31
|
+
if (!['cache', 'artifact'].includes(kind)) throw refusal();
|
|
32
|
+
const program = basename(dirname(entrypoint));
|
|
33
|
+
const expected = kind === 'artifact' ? (program === 'upload' && artifactHash) : hashes.get(program);
|
|
34
|
+
if (!expected || createHash('sha256').update(readFileSync(entrypoint)).digest('hex') !== expected) throw refusal();
|
|
35
|
+
const origins = (kind === 'artifact' ? [env.ACTIONS_RESULTS_URL]
|
|
36
|
+
: [env.ACTIONS_RESULTS_URL, env.ACTIONS_CACHE_URL]).filter(Boolean);
|
|
37
|
+
if (!origins.length) throw refusal();
|
|
38
|
+
for (const origin of origins) requireOrigin(origin, true);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function requireRequest(args, protocol) {
|
|
42
|
+
const [input, extra] = args;
|
|
43
|
+
const options = typeof input === 'string' || input instanceof URL
|
|
44
|
+
? {protocol, ...require('node:url').urlToHttpOptions(requireOrigin(input)),
|
|
45
|
+
...(typeof extra === 'object' ? extra : {})}
|
|
46
|
+
: {...input};
|
|
47
|
+
if (!options || options.socketPath || (options.protocol || protocol) !== 'https:') throw refusal();
|
|
48
|
+
for (const host of [options.hostname, options.host].filter(Boolean)) {
|
|
49
|
+
requireOrigin(`https://${host}${options.port ? ':' + options.port : ''}`);
|
|
50
|
+
}
|
|
51
|
+
if (!options.hostname && !options.host) throw refusal();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function guardRequests() {
|
|
55
|
+
for (const [name, protocol] of [['node:http', 'http:'], ['node:https', 'https:']]) {
|
|
56
|
+
const client = require(name);
|
|
57
|
+
for (const verb of ['request', 'get']) {
|
|
58
|
+
const original = client[verb];
|
|
59
|
+
client[verb] = function (...args) { requireRequest(args, protocol); return original.apply(this, args); };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (globalThis.fetch) {
|
|
63
|
+
const original = globalThis.fetch;
|
|
64
|
+
globalThis.fetch = function (input, init) {
|
|
65
|
+
requireOrigin(typeof input === 'string' || input instanceof URL ? input : input.url);
|
|
66
|
+
return original.call(this, input, {...init, redirect: 'error'});
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function snapshot() {
|
|
72
|
+
const target = mkdtempSync(join(process.env.RUNNER_TEMP, 'gowalk-cache-scope-'));
|
|
73
|
+
for (const file of ['cache_env.cjs', 'cache_scope.cjs']) copyFileSync(join(__dirname, file), join(target, file));
|
|
74
|
+
appendFileSync(process.env.GITHUB_OUTPUT, `preloader=${join(target, 'cache_env.cjs')}\n`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {authorize, guardRequests, proxyKeys, requireOrigin};
|
|
78
|
+
if (require.main === module) {
|
|
79
|
+
try { if (process.argv[2] !== '--snapshot') throw refusal(); snapshot(); }
|
|
80
|
+
catch { process.stderr.write('github_cache_scope_snapshot_failed\n'); process.exitCode = 1; }
|
|
81
|
+
}
|
|
@@ -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.72
|
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
|
|
@@ -61,6 +56,8 @@ jobs:
|
|
|
61
56
|
flutter build web --release --build-number="${GITHUB_RUN_NUMBER}"
|
|
62
57
|
- name: Retain Flutter web release
|
|
63
58
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
|
59
|
+
env:
|
|
60
|
+
NODE_OPTIONS: --require "${{ github.workspace }}/.github/actions/android-app/scripts/artifact_env.cjs"
|
|
64
61
|
with:
|
|
65
62
|
name: web-${{ github.event.repository.name }}-${{ github.run_number }}
|
|
66
63
|
path: build/web
|
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
|
|
@@ -212,6 +215,8 @@ jobs:
|
|
|
212
215
|
- name: Retain Dart symbol files
|
|
213
216
|
if: ${{ !cancelled() && steps.ios_symbols.outputs.present == 'true' }}
|
|
214
217
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
|
218
|
+
env:
|
|
219
|
+
NODE_OPTIONS: --require "${{ env.SWIFT_APP_ACTION }}/scripts/artifact_env.cjs"
|
|
215
220
|
with:
|
|
216
221
|
name: ios-symbols-${{ github.event.repository.name }}-${{ env.BUILD_NUMBER || github.run_number }}
|
|
217
222
|
path: ${{ runner.temp }}/dart-symbols/ios
|
|
@@ -335,15 +340,9 @@ jobs:
|
|
|
335
340
|
distribution: temurin
|
|
336
341
|
java-version: ${{ steps.jdk.outputs.version }}
|
|
337
342
|
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
343
|
- name: Set up Flutter
|
|
343
344
|
if: ${{ steps.mode.outputs.mode == 'local' && steps.flutter.outputs.enabled == 'true' }}
|
|
344
|
-
uses:
|
|
345
|
-
env:
|
|
346
|
-
SEGMENT_DOWNLOAD_TIMEOUT_MINS: "1"
|
|
345
|
+
uses: ./.github/actions/android-app/flutter-setup
|
|
347
346
|
with:
|
|
348
347
|
channel: stable
|
|
349
348
|
cache: true
|
|
@@ -438,6 +437,8 @@ jobs:
|
|
|
438
437
|
- name: Retain signed Android App Bundle
|
|
439
438
|
if: ${{ steps.mode.outputs.mode == 'local' }}
|
|
440
439
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
|
440
|
+
env:
|
|
441
|
+
NODE_OPTIONS: --require "${{ github.workspace }}/.github/actions/android-app/scripts/artifact_env.cjs"
|
|
441
442
|
with:
|
|
442
443
|
name: android-${{ steps.android.outputs.package-name }}-${{ steps.android.outputs.build-number }}
|
|
443
444
|
path: ${{ steps.android.outputs.bundle-path }}
|