sdk-release-kit 0.2.0__py3-none-any.whl
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.
- sdk_release_kit/__init__.py +3 -0
- sdk_release_kit/_data/examples/custom.example.yaml +18 -0
- sdk_release_kit/_data/examples/keys.example.yaml +18 -0
- sdk_release_kit/_data/examples/outputs.example.yaml +19 -0
- sdk_release_kit/_data/examples/repos.example.yaml +33 -0
- sdk_release_kit/_data/recipes/sdks.yaml +222 -0
- sdk_release_kit/_data/web/app.js +305 -0
- sdk_release_kit/_data/web/index.html +142 -0
- sdk_release_kit/_data/web/styles.css +93 -0
- sdk_release_kit/builders/__init__.py +24 -0
- sdk_release_kit/builders/android.py +86 -0
- sdk_release_kit/builders/base.py +217 -0
- sdk_release_kit/builders/flutter.py +16 -0
- sdk_release_kit/builders/generic.py +56 -0
- sdk_release_kit/builders/ios.py +204 -0
- sdk_release_kit/builders/reactnative.py +32 -0
- sdk_release_kit/cli.py +198 -0
- sdk_release_kit/mcp_server.py +240 -0
- sdk_release_kit/models.py +306 -0
- sdk_release_kit/paths.py +85 -0
- sdk_release_kit/pipeline.py +446 -0
- sdk_release_kit/recipes.py +110 -0
- sdk_release_kit/runlog.py +62 -0
- sdk_release_kit/server.py +331 -0
- sdk_release_kit/store/__init__.py +29 -0
- sdk_release_kit/store/_yaml.py +46 -0
- sdk_release_kit/store/custom.py +34 -0
- sdk_release_kit/store/keys.py +111 -0
- sdk_release_kit/store/outputs.py +15 -0
- sdk_release_kit/store/repos.py +15 -0
- sdk_release_kit/uploaders/__init__.py +20 -0
- sdk_release_kit/uploaders/onedrive.py +129 -0
- sdk_release_kit/uploaders/play.py +108 -0
- sdk_release_kit/uploaders/testflight.py +105 -0
- sdk_release_kit-0.2.0.dist-info/METADATA +307 -0
- sdk_release_kit-0.2.0.dist-info/RECORD +39 -0
- sdk_release_kit-0.2.0.dist-info/WHEEL +4 -0
- sdk_release_kit-0.2.0.dist-info/entry_points.txt +2 -0
- sdk_release_kit-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Custom data: free-form YAML. Recipes read it through ${custom.some.key}.
|
|
2
|
+
# Unknown keys are kept as written, so teams and AI agents can add their own.
|
|
3
|
+
|
|
4
|
+
# Apple Developer team used for archive and export (leave empty for automatic).
|
|
5
|
+
ios_team_id: ""
|
|
6
|
+
|
|
7
|
+
# Store identifiers of the demo apps, used for Play and TestFlight uploads.
|
|
8
|
+
app_ids:
|
|
9
|
+
android_native: com.example.paymentsdk.demo
|
|
10
|
+
ios_native: com.example.paymentsdk.demoapp
|
|
11
|
+
flutter_android: com.example.paymentsdk.flutter.demoapp
|
|
12
|
+
flutter_ios: com.example.paymentsdk.flutter.demoapp
|
|
13
|
+
rn_android: com.example.paymentsdk.reactnative.demoapp
|
|
14
|
+
rn_ios: com.example.paymentsdk.reactnative.demoapp
|
|
15
|
+
|
|
16
|
+
# Anything else your team wants to keep with the tool, for example:
|
|
17
|
+
# notify:
|
|
18
|
+
# slack_webhook_env: SLACK_WEBHOOK_URL
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Keys: PATHS to key files and REFERENCES to secrets. Never put a password in this file.
|
|
2
|
+
# A secret reference is {source: env|keychain|none, name: ...}.
|
|
3
|
+
# env: read from the environment variable `name`
|
|
4
|
+
# keychain: read from the OS keychain, service "sdk-release-kit", account `name`
|
|
5
|
+
# (set with `sdk-release-kit keys set <name>` or the Keys page)
|
|
6
|
+
schema_version: 1
|
|
7
|
+
android:
|
|
8
|
+
keystore_path: ~/path/to/release.keystore
|
|
9
|
+
key_alias: release
|
|
10
|
+
store_password: {source: keychain, name: android.store_password}
|
|
11
|
+
key_password: {source: keychain, name: android.key_password}
|
|
12
|
+
inject_signing: true
|
|
13
|
+
play:
|
|
14
|
+
json_key_path: ~/path/to/play-service-account.json
|
|
15
|
+
testflight:
|
|
16
|
+
key_id: {source: env, name: ASC_KEY_ID}
|
|
17
|
+
issuer_id: {source: env, name: ASC_ISSUER_ID}
|
|
18
|
+
key_path: ~/path/to/AuthKey_XXXXXXXXXX.p8
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Outputs: where artifacts go. Not committed anywhere.
|
|
2
|
+
schema_version: 1
|
|
3
|
+
staging: ~/sdk-release-kit/staging
|
|
4
|
+
onedrive:
|
|
5
|
+
# Any locally synced folder works (OneDrive, Google Drive, Dropbox, an SMB mount).
|
|
6
|
+
enabled: false
|
|
7
|
+
root: ~/path/to/OneDrive/Mobile SDK Builds
|
|
8
|
+
demos_android: android
|
|
9
|
+
demos_ios: ios
|
|
10
|
+
packages: packages
|
|
11
|
+
copy_untargeted: false
|
|
12
|
+
play:
|
|
13
|
+
enabled: false
|
|
14
|
+
track: internal
|
|
15
|
+
release_status: completed # use "draft" for an app that has never been published
|
|
16
|
+
method: auto # auto | api | fastlane
|
|
17
|
+
testflight:
|
|
18
|
+
enabled: false
|
|
19
|
+
method: altool # altool | fastlane
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Repositories: recipe id -> checkout on THIS machine. Not committed anywhere.
|
|
2
|
+
# Copied to <settings dir>/repos.yaml by `sdk-release-kit init`.
|
|
3
|
+
schema_version: 1
|
|
4
|
+
repos:
|
|
5
|
+
android-current:
|
|
6
|
+
path: ~/path/to/payment-sdk-android
|
|
7
|
+
enabled: true
|
|
8
|
+
android-legacy:
|
|
9
|
+
path: ~/path/to/payment-sdk-android-legacy
|
|
10
|
+
enabled: false
|
|
11
|
+
ios-current:
|
|
12
|
+
path: ~/path/to/payment-sdk-ios
|
|
13
|
+
enabled: false
|
|
14
|
+
ios-legacy:
|
|
15
|
+
path: ~/path/to/payment-sdk-ios-legacy
|
|
16
|
+
enabled: false
|
|
17
|
+
flutter:
|
|
18
|
+
path: ~/path/to/payment-sdk-flutter
|
|
19
|
+
enabled: false
|
|
20
|
+
reactnative-current:
|
|
21
|
+
path: ~/path/to/payment-sdk-react-native
|
|
22
|
+
enabled: false
|
|
23
|
+
reactnative-legacy:
|
|
24
|
+
path: ~/path/to/payment-sdk-react-native-legacy
|
|
25
|
+
enabled: false
|
|
26
|
+
tooling:
|
|
27
|
+
# Android Studio's bundled JDK on macOS is usually:
|
|
28
|
+
# /Applications/Android Studio.app/Contents/jbr/Contents/Home
|
|
29
|
+
java_home: null
|
|
30
|
+
android_sdk: ~/Library/Android/sdk
|
|
31
|
+
flutter_bin: null # e.g. ~/path/to/flutter/bin
|
|
32
|
+
developer_dir: null # e.g. /Applications/Xcode.app/Contents/Developer
|
|
33
|
+
extra_path: []
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Portable build recipes. Shipped in git.
|
|
2
|
+
#
|
|
3
|
+
# Rules (see AGENTS.md):
|
|
4
|
+
# - Paths here are RELATIVE to the SDK checkout. Absolute paths belong in user settings.
|
|
5
|
+
# - No secrets. Identifiers that differ per team come from Custom data via ${custom.key:-default}.
|
|
6
|
+
# - Adjust workdirs, workspaces and schemes to your repositories, or override them per machine
|
|
7
|
+
# with a file in <settings dir>/recipes.d/ (deep-merged over this file).
|
|
8
|
+
#
|
|
9
|
+
# Step types: run, gradle, flutter, xcode-simulator, xcode-ipa, xcframework, npm-pack, source-zip.
|
|
10
|
+
# Optional on any step: name, workdir, env, when: [onedrive|play|testflight], host: any|macos,
|
|
11
|
+
# collect: [{glob, kind, target, root, required}].
|
|
12
|
+
|
|
13
|
+
schema_version: 1
|
|
14
|
+
|
|
15
|
+
# Shared-folder targets for demo apps: <onedrive root>/<outputs.onedrive[folder]>/<name>
|
|
16
|
+
onedrive_targets:
|
|
17
|
+
android-native-current: {folder: demos_android, name: native-current.apk}
|
|
18
|
+
android-native-legacy: {folder: demos_android, name: native-legacy.apk}
|
|
19
|
+
android-flutter: {folder: demos_android, name: flutter.apk}
|
|
20
|
+
android-rn-current: {folder: demos_android, name: rn-current.apk}
|
|
21
|
+
android-rn-legacy: {folder: demos_android, name: rn-legacy.apk}
|
|
22
|
+
ios-native-current: {folder: demos_ios, name: native-current.app}
|
|
23
|
+
ios-native-legacy: {folder: demos_ios, name: native-legacy.app}
|
|
24
|
+
ios-flutter: {folder: demos_ios, name: flutter.app}
|
|
25
|
+
ios-rn-current: {folder: demos_ios, name: rn-current.app}
|
|
26
|
+
ios-rn-legacy: {folder: demos_ios, name: rn-legacy.app}
|
|
27
|
+
|
|
28
|
+
recipes:
|
|
29
|
+
# ------------------------------------------------------------------------------- Android
|
|
30
|
+
android-current:
|
|
31
|
+
platform: android
|
|
32
|
+
label: Android native SDK (current)
|
|
33
|
+
version: {file: gradle.properties, regex: '^\s*VERSION_NAME\s*=\s*(\S+)'}
|
|
34
|
+
store:
|
|
35
|
+
play: true
|
|
36
|
+
android_package: "${custom.app_ids.android_native:-com.example.paymentsdk.demo}"
|
|
37
|
+
demos:
|
|
38
|
+
- type: gradle
|
|
39
|
+
name: demo APK + AAB
|
|
40
|
+
tasks: [":app:assembleRelease", ":app:bundleRelease"]
|
|
41
|
+
collect:
|
|
42
|
+
- {glob: "app/build/outputs/apk/release/*.apk", kind: apk, target: android-native-current}
|
|
43
|
+
- {glob: "app/build/outputs/bundle/release/*.aab", kind: aab}
|
|
44
|
+
packages:
|
|
45
|
+
- type: gradle
|
|
46
|
+
name: AARs to Maven local
|
|
47
|
+
tasks: [publishToMavenLocal]
|
|
48
|
+
sign: false
|
|
49
|
+
collect:
|
|
50
|
+
- {glob: "*/build/outputs/aar/*-release.aar", kind: aar}
|
|
51
|
+
|
|
52
|
+
android-legacy:
|
|
53
|
+
platform: android
|
|
54
|
+
label: Android native SDK (legacy)
|
|
55
|
+
version: {file: gradle.properties, regex: '^\s*VERSION_NAME\s*=\s*(\S+)'}
|
|
56
|
+
store:
|
|
57
|
+
play: false # same applicationId as android-current: shared folder only
|
|
58
|
+
android_package: "${custom.app_ids.android_native:-com.example.paymentsdk.demo}"
|
|
59
|
+
demos:
|
|
60
|
+
- type: gradle
|
|
61
|
+
name: demo APK + AAB
|
|
62
|
+
tasks: [":app:assembleRelease", ":app:bundleRelease"]
|
|
63
|
+
collect:
|
|
64
|
+
- {glob: "app/build/outputs/apk/release/*.apk", kind: apk, target: android-native-legacy}
|
|
65
|
+
- {glob: "app/build/outputs/bundle/release/*.aab", kind: aab}
|
|
66
|
+
packages:
|
|
67
|
+
- type: gradle
|
|
68
|
+
name: AARs to Maven local
|
|
69
|
+
tasks: [publishToMavenLocal]
|
|
70
|
+
sign: false
|
|
71
|
+
collect:
|
|
72
|
+
- {glob: "*/build/outputs/aar/*-release.aar", kind: aar}
|
|
73
|
+
|
|
74
|
+
# ----------------------------------------------------------------------------------- iOS
|
|
75
|
+
ios-current:
|
|
76
|
+
platform: ios
|
|
77
|
+
label: iOS native SDK (current)
|
|
78
|
+
store:
|
|
79
|
+
testflight: true
|
|
80
|
+
ios_bundle_id: "${custom.app_ids.ios_native:-com.example.paymentsdk.demoapp}"
|
|
81
|
+
demos:
|
|
82
|
+
- type: xcode-simulator
|
|
83
|
+
name: simulator .app
|
|
84
|
+
workspace: DemoApp/DemoApp.xcworkspace
|
|
85
|
+
scheme: DemoApp
|
|
86
|
+
pod_install: false
|
|
87
|
+
target: ios-native-current
|
|
88
|
+
- type: xcode-ipa
|
|
89
|
+
name: archive + IPA for TestFlight
|
|
90
|
+
when: [testflight]
|
|
91
|
+
workspace: DemoApp/DemoApp.xcworkspace
|
|
92
|
+
scheme: DemoApp
|
|
93
|
+
team_id: "${custom.ios_team_id:-}"
|
|
94
|
+
packages:
|
|
95
|
+
- type: xcframework
|
|
96
|
+
workspace: DemoApp/DemoApp.xcworkspace
|
|
97
|
+
scheme: PaymentSDK
|
|
98
|
+
framework_name: PaymentSDK
|
|
99
|
+
|
|
100
|
+
ios-legacy:
|
|
101
|
+
platform: ios
|
|
102
|
+
label: iOS native SDK (legacy)
|
|
103
|
+
store:
|
|
104
|
+
testflight: false # same bundle id as ios-current: shared folder only
|
|
105
|
+
ios_bundle_id: "${custom.app_ids.ios_native:-com.example.paymentsdk.demoapp}"
|
|
106
|
+
demos:
|
|
107
|
+
- type: xcode-simulator
|
|
108
|
+
name: simulator .app
|
|
109
|
+
workspace: DemoApp/DemoApp.xcworkspace
|
|
110
|
+
scheme: DemoApp
|
|
111
|
+
target: ios-native-legacy
|
|
112
|
+
packages:
|
|
113
|
+
- type: xcframework
|
|
114
|
+
workspace: DemoApp/DemoApp.xcworkspace
|
|
115
|
+
scheme: PaymentSDK
|
|
116
|
+
framework_name: PaymentSDK
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------------------- Flutter
|
|
119
|
+
flutter:
|
|
120
|
+
platform: flutter
|
|
121
|
+
label: Flutter plugin
|
|
122
|
+
version: {file: pubspec.yaml, regex: '^version:\s*(\S+)'}
|
|
123
|
+
store:
|
|
124
|
+
play: true
|
|
125
|
+
testflight: true
|
|
126
|
+
android_package: "${custom.app_ids.flutter_android:-com.example.paymentsdk.flutter.demoapp}"
|
|
127
|
+
ios_bundle_id: "${custom.app_ids.flutter_ios:-com.example.paymentsdk.flutter.demoapp}"
|
|
128
|
+
demos:
|
|
129
|
+
- {type: flutter, name: pub get, workdir: example, args: [pub, get]}
|
|
130
|
+
- type: flutter
|
|
131
|
+
name: demo APK
|
|
132
|
+
workdir: example
|
|
133
|
+
args: [build, apk, --release]
|
|
134
|
+
collect:
|
|
135
|
+
- {glob: "build/app/outputs/flutter-apk/app-release.apk", kind: apk, target: android-flutter}
|
|
136
|
+
- type: flutter
|
|
137
|
+
name: demo AAB
|
|
138
|
+
workdir: example
|
|
139
|
+
when: [play]
|
|
140
|
+
args: [build, appbundle, --release]
|
|
141
|
+
collect:
|
|
142
|
+
- {glob: "build/app/outputs/bundle/release/*.aab", kind: aab}
|
|
143
|
+
- type: flutter
|
|
144
|
+
name: simulator .app
|
|
145
|
+
workdir: example
|
|
146
|
+
host: macos
|
|
147
|
+
args: [build, ios, --simulator, --debug]
|
|
148
|
+
collect:
|
|
149
|
+
- {glob: "build/ios/iphonesimulator/Runner.app", kind: app, target: ios-flutter}
|
|
150
|
+
- type: flutter
|
|
151
|
+
name: IPA for TestFlight
|
|
152
|
+
workdir: example
|
|
153
|
+
host: macos
|
|
154
|
+
when: [testflight]
|
|
155
|
+
args: [build, ipa, --release, --export-method, app-store]
|
|
156
|
+
collect:
|
|
157
|
+
- {glob: "build/ios/ipa/*.ipa", kind: ipa}
|
|
158
|
+
packages:
|
|
159
|
+
- {type: source-zip, name: package sources}
|
|
160
|
+
|
|
161
|
+
# -------------------------------------------------------------------------- React Native
|
|
162
|
+
reactnative-current:
|
|
163
|
+
platform: reactnative
|
|
164
|
+
label: React Native SDK (current)
|
|
165
|
+
version: {file: package.json, regex: '"version"\s*:\s*"([^"]+)"'}
|
|
166
|
+
store:
|
|
167
|
+
play: true
|
|
168
|
+
testflight: true
|
|
169
|
+
android_package: "${custom.app_ids.rn_android:-com.example.paymentsdk.reactnative.demoapp}"
|
|
170
|
+
ios_bundle_id: "${custom.app_ids.rn_ios:-com.example.paymentsdk.reactnative.demoapp}"
|
|
171
|
+
demos:
|
|
172
|
+
- {type: run, name: install demo deps, workdir: SimpleIntegration, cmd: [npm, install]}
|
|
173
|
+
- type: gradle
|
|
174
|
+
name: demo APK + AAB
|
|
175
|
+
workdir: SimpleIntegration/android
|
|
176
|
+
tasks: [assembleRelease, bundleRelease]
|
|
177
|
+
collect:
|
|
178
|
+
- {glob: "app/build/outputs/apk/release/*.apk", kind: apk, target: android-rn-current}
|
|
179
|
+
- {glob: "app/build/outputs/bundle/release/*.aab", kind: aab}
|
|
180
|
+
- type: xcode-simulator
|
|
181
|
+
name: simulator .app
|
|
182
|
+
workdir: SimpleIntegration/ios
|
|
183
|
+
workspace: SimpleIntegration.xcworkspace
|
|
184
|
+
scheme: SimpleIntegration
|
|
185
|
+
pod_install: true
|
|
186
|
+
target: ios-rn-current
|
|
187
|
+
- type: xcode-ipa
|
|
188
|
+
name: archive + IPA for TestFlight
|
|
189
|
+
when: [testflight]
|
|
190
|
+
workdir: SimpleIntegration/ios
|
|
191
|
+
workspace: SimpleIntegration.xcworkspace
|
|
192
|
+
scheme: SimpleIntegration
|
|
193
|
+
team_id: "${custom.ios_team_id:-}"
|
|
194
|
+
packages:
|
|
195
|
+
- {type: npm-pack, name: npm pack}
|
|
196
|
+
|
|
197
|
+
reactnative-legacy:
|
|
198
|
+
platform: reactnative
|
|
199
|
+
label: React Native SDK (legacy)
|
|
200
|
+
version: {file: package.json, regex: '"version"\s*:\s*"([^"]+)"'}
|
|
201
|
+
store:
|
|
202
|
+
play: false # duplicates of reactnative-current ids: shared folder only
|
|
203
|
+
testflight: false
|
|
204
|
+
android_package: "${custom.app_ids.rn_android:-com.example.paymentsdk.reactnative.demoapp}"
|
|
205
|
+
ios_bundle_id: "${custom.app_ids.rn_ios:-com.example.paymentsdk.reactnative.demoapp}"
|
|
206
|
+
demos:
|
|
207
|
+
- {type: run, name: install demo deps, workdir: SimpleIntegration, cmd: [npm, install]}
|
|
208
|
+
- type: gradle
|
|
209
|
+
name: demo APK
|
|
210
|
+
workdir: SimpleIntegration/android
|
|
211
|
+
tasks: [assembleRelease]
|
|
212
|
+
collect:
|
|
213
|
+
- {glob: "app/build/outputs/apk/release/*.apk", kind: apk, target: android-rn-legacy}
|
|
214
|
+
- type: xcode-simulator
|
|
215
|
+
name: simulator .app
|
|
216
|
+
workdir: SimpleIntegration/ios
|
|
217
|
+
workspace: SimpleIntegration.xcworkspace
|
|
218
|
+
scheme: SimpleIntegration
|
|
219
|
+
pod_install: true
|
|
220
|
+
target: ios-rn-legacy
|
|
221
|
+
packages:
|
|
222
|
+
- {type: npm-pack, name: npm pack}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// SDK Release Kit Settings UI. Plain JS, no build step.
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const TOKEN = document.querySelector('meta[name="srk-token"]').content;
|
|
5
|
+
const $ = (sel, root = document) => root.querySelector(sel);
|
|
6
|
+
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
|
7
|
+
let STATUS = null;
|
|
8
|
+
let REPOS = null;
|
|
9
|
+
let OUTPUTS = null;
|
|
10
|
+
let KEYS = null;
|
|
11
|
+
|
|
12
|
+
async function api(method, path, body) {
|
|
13
|
+
const res = await fetch(path, {
|
|
14
|
+
method,
|
|
15
|
+
headers: { "Content-Type": "application/json", "X-SRK-Token": TOKEN },
|
|
16
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
let detail = res.statusText;
|
|
20
|
+
try { detail = (await res.json()).detail; } catch (_) { /* not JSON */ }
|
|
21
|
+
throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail, null, 2));
|
|
22
|
+
}
|
|
23
|
+
return res.json();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toast(msg, isError = false) {
|
|
27
|
+
const el = $("#toast");
|
|
28
|
+
el.textContent = msg;
|
|
29
|
+
el.className = "toast" + (isError ? " err" : "");
|
|
30
|
+
el.hidden = false;
|
|
31
|
+
clearTimeout(toast._t);
|
|
32
|
+
toast._t = setTimeout(() => (el.hidden = true), isError ? 8000 : 2500);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function guard(fn) {
|
|
36
|
+
return async (...args) => {
|
|
37
|
+
try { await fn(...args); } catch (e) { toast(e.message, true); }
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const getPath = (obj, dotted) => dotted.split(".").reduce((o, k) => (o == null ? o : o[k]), obj);
|
|
42
|
+
function setPath(obj, dotted, value) {
|
|
43
|
+
const parts = dotted.split(".");
|
|
44
|
+
let o = obj;
|
|
45
|
+
for (const p of parts.slice(0, -1)) o = o[p] ??= {};
|
|
46
|
+
o[parts.at(-1)] = value;
|
|
47
|
+
}
|
|
48
|
+
const nullIfEmpty = (v) => (v === "" ? null : v);
|
|
49
|
+
|
|
50
|
+
// Tabs -------------------------------------------------------------------------------------
|
|
51
|
+
$$("#tabs button").forEach((btn) =>
|
|
52
|
+
btn.addEventListener("click", () => {
|
|
53
|
+
$$("#tabs button").forEach((b) => b.classList.toggle("active", b === btn));
|
|
54
|
+
$$(".tab").forEach((t) => (t.hidden = t.id !== "tab-" + btn.dataset.tab));
|
|
55
|
+
try { localStorage.setItem("srk-tab", btn.dataset.tab); } catch (_) { /* storage blocked */ }
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Repositories -----------------------------------------------------------------------------
|
|
60
|
+
function renderRepos() {
|
|
61
|
+
const tbody = $("#repos-table tbody");
|
|
62
|
+
tbody.innerHTML = "";
|
|
63
|
+
const ids = [...new Set([...Object.keys(STATUS.recipes), ...Object.keys(REPOS.repos)])];
|
|
64
|
+
for (const id of ids) {
|
|
65
|
+
const entry = REPOS.repos[id] || { path: "", enabled: false };
|
|
66
|
+
const recipe = STATUS.recipes[id];
|
|
67
|
+
const tr = document.createElement("tr");
|
|
68
|
+
tr.dataset.id = id;
|
|
69
|
+
tr.innerHTML = `
|
|
70
|
+
<td><input type="checkbox" class="enabled"></td>
|
|
71
|
+
<td><strong></strong><div class="muted small"></div></td>
|
|
72
|
+
<td class="path"><input class="path-input" placeholder="~/path/to/checkout"></td>
|
|
73
|
+
<td class="check"></td>`;
|
|
74
|
+
$(".enabled", tr).checked = !!entry.enabled;
|
|
75
|
+
$("strong", tr).textContent = id;
|
|
76
|
+
$(".small", tr).textContent = recipe ? `${recipe.platform}. ${recipe.label}` : "not in recipes (kept)";
|
|
77
|
+
const input = $(".path-input", tr);
|
|
78
|
+
input.value = entry.path || "";
|
|
79
|
+
const check = guard(async () => {
|
|
80
|
+
const cell = $(".check", tr);
|
|
81
|
+
if (!input.value.trim()) { cell.textContent = ""; return; }
|
|
82
|
+
const r = await api("POST", "/api/validate/path", { path: input.value });
|
|
83
|
+
cell.textContent = r.ok ? "✓ " + r.detail : "✗ " + r.detail;
|
|
84
|
+
cell.className = "check " + (r.ok ? "ok" : "bad");
|
|
85
|
+
});
|
|
86
|
+
input.addEventListener("change", check);
|
|
87
|
+
check();
|
|
88
|
+
tbody.appendChild(tr);
|
|
89
|
+
}
|
|
90
|
+
const t = REPOS.tooling || {};
|
|
91
|
+
for (const input of $$("#tooling-form input")) {
|
|
92
|
+
const v = t[input.name];
|
|
93
|
+
input.value = Array.isArray(v) ? v.join(", ") : v || "";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
$("#save-repos").addEventListener("click", guard(async () => {
|
|
98
|
+
const repos = { ...REPOS.repos };
|
|
99
|
+
for (const tr of $$("#repos-table tbody tr")) {
|
|
100
|
+
repos[tr.dataset.id] = {
|
|
101
|
+
...(repos[tr.dataset.id] || {}),
|
|
102
|
+
path: $(".path-input", tr).value.trim(),
|
|
103
|
+
enabled: $(".enabled", tr).checked,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const tooling = { ...(REPOS.tooling || {}) };
|
|
107
|
+
for (const input of $$("#tooling-form input")) {
|
|
108
|
+
tooling[input.name] = input.name === "extra_path"
|
|
109
|
+
? input.value.split(",").map((s) => s.trim()).filter(Boolean)
|
|
110
|
+
: nullIfEmpty(input.value.trim());
|
|
111
|
+
}
|
|
112
|
+
REPOS = await api("PUT", "/api/settings/repos", { repos, tooling });
|
|
113
|
+
renderRepos();
|
|
114
|
+
renderRunSdks();
|
|
115
|
+
toast("Repositories saved");
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
// Outputs ----------------------------------------------------------------------------------
|
|
119
|
+
function renderOutputs() {
|
|
120
|
+
for (const el of $$("#tab-outputs [data-k]")) {
|
|
121
|
+
const v = getPath(OUTPUTS, el.dataset.k);
|
|
122
|
+
if (el.type === "checkbox") el.checked = !!v;
|
|
123
|
+
else el.value = v ?? "";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
$("#save-outputs").addEventListener("click", guard(async () => {
|
|
128
|
+
const body = structuredClone(OUTPUTS);
|
|
129
|
+
for (const el of $$("#tab-outputs [data-k]")) {
|
|
130
|
+
setPath(body, el.dataset.k, el.type === "checkbox" ? el.checked : nullIfEmpty(el.value.trim()));
|
|
131
|
+
}
|
|
132
|
+
OUTPUTS = await api("PUT", "/api/settings/outputs", body);
|
|
133
|
+
renderOutputs();
|
|
134
|
+
renderRunDestinations();
|
|
135
|
+
toast("Outputs saved");
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
// Keys -------------------------------------------------------------------------------------
|
|
139
|
+
const SECRET_LABELS = {
|
|
140
|
+
"android.store_password": "Keystore password",
|
|
141
|
+
"android.key_password": "Key password (empty uses keystore password)",
|
|
142
|
+
"testflight.key_id": "ASC key id",
|
|
143
|
+
"testflight.issuer_id": "ASC issuer id",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function renderKeys() {
|
|
147
|
+
const { settings, status } = KEYS;
|
|
148
|
+
const chips = $("#keys-status");
|
|
149
|
+
chips.innerHTML = "";
|
|
150
|
+
for (const [name, st] of Object.entries(status)) {
|
|
151
|
+
const c = document.createElement("span");
|
|
152
|
+
c.className = "chip " + (st.configured ? "ok" : "bad");
|
|
153
|
+
c.textContent = `${name}: ${st.configured ? "configured" : "missing"}`;
|
|
154
|
+
c.title = st.detail;
|
|
155
|
+
chips.appendChild(c);
|
|
156
|
+
}
|
|
157
|
+
for (const el of $$("#tab-keys [data-key]")) {
|
|
158
|
+
const v = getPath(settings, el.dataset.key);
|
|
159
|
+
if (el.type === "checkbox") el.checked = !!v;
|
|
160
|
+
else el.value = v ?? "";
|
|
161
|
+
}
|
|
162
|
+
for (const box of $$("#tab-keys [data-secret]")) {
|
|
163
|
+
const field = box.dataset.secret;
|
|
164
|
+
const ref = getPath(settings, field) || { source: "none", name: "" };
|
|
165
|
+
const st = status[field] || {};
|
|
166
|
+
box.innerHTML = `
|
|
167
|
+
<div class="muted">${SECRET_LABELS[field]} <span class="${st.configured ? "ok" : "bad"}">(${st.configured ? "configured" : "missing"})</span></div>
|
|
168
|
+
<div class="row">
|
|
169
|
+
<select class="src"><option value="keychain">OS keychain</option><option value="env">Environment variable</option><option value="none">Not set</option></select>
|
|
170
|
+
<input class="ref-name" placeholder="entry or variable name">
|
|
171
|
+
</div>
|
|
172
|
+
<div class="row keychain-row">
|
|
173
|
+
<input class="value" type="password" autocomplete="new-password" placeholder="new value (stored in keychain, never shown)">
|
|
174
|
+
<button class="store">Store in keychain</button>
|
|
175
|
+
</div>`;
|
|
176
|
+
$(".src", box).value = ref.source;
|
|
177
|
+
$(".ref-name", box).value = ref.name;
|
|
178
|
+
const syncRow = () => ($(".keychain-row", box).hidden = $(".src", box).value !== "keychain");
|
|
179
|
+
$(".src", box).addEventListener("change", syncRow);
|
|
180
|
+
syncRow();
|
|
181
|
+
$(".store", box).addEventListener("click", guard(async () => {
|
|
182
|
+
const value = $(".value", box).value;
|
|
183
|
+
if (!value) { toast("Enter a value first", true); return; }
|
|
184
|
+
KEYS = await api("POST", "/api/keys/secret", { field, value });
|
|
185
|
+
renderKeys();
|
|
186
|
+
toast("Stored in keychain");
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
$$("#tab-keys [data-upload]").forEach((input) =>
|
|
192
|
+
input.addEventListener("change", guard(async () => {
|
|
193
|
+
const file = input.files[0];
|
|
194
|
+
if (!file) return;
|
|
195
|
+
const buf = new Uint8Array(await file.arrayBuffer());
|
|
196
|
+
let bin = "";
|
|
197
|
+
for (let i = 0; i < buf.length; i += 0x8000) bin += String.fromCharCode(...buf.subarray(i, i + 0x8000));
|
|
198
|
+
KEYS = await api("POST", "/api/keys/upload", {
|
|
199
|
+
field: input.dataset.upload, filename: file.name, content_b64: btoa(bin),
|
|
200
|
+
});
|
|
201
|
+
input.value = "";
|
|
202
|
+
renderKeys();
|
|
203
|
+
toast(`${file.name} saved to the private keys folder`);
|
|
204
|
+
}))
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
$("#save-keys").addEventListener("click", guard(async () => {
|
|
208
|
+
const body = structuredClone(KEYS.settings);
|
|
209
|
+
for (const el of $$("#tab-keys [data-key]")) {
|
|
210
|
+
setPath(body, el.dataset.key, el.type === "checkbox" ? el.checked : nullIfEmpty(el.value.trim()));
|
|
211
|
+
}
|
|
212
|
+
for (const box of $$("#tab-keys [data-secret]")) {
|
|
213
|
+
setPath(body, box.dataset.secret, {
|
|
214
|
+
source: $(".src", box).value, name: $(".ref-name", box).value.trim(),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
KEYS = await api("PUT", "/api/settings/keys", body);
|
|
218
|
+
renderKeys();
|
|
219
|
+
toast("Keys saved");
|
|
220
|
+
}));
|
|
221
|
+
|
|
222
|
+
// Custom -----------------------------------------------------------------------------------
|
|
223
|
+
$("#save-custom").addEventListener("click", guard(async () => {
|
|
224
|
+
const r = await api("PUT", "/api/settings/custom", { text: $("#custom-text").value });
|
|
225
|
+
$("#custom-text").value = r.text;
|
|
226
|
+
toast(`Custom data saved (${r.keys.length} top-level keys)`);
|
|
227
|
+
}));
|
|
228
|
+
|
|
229
|
+
// Run --------------------------------------------------------------------------------------
|
|
230
|
+
function renderRunSdks() {
|
|
231
|
+
const box = $("#run-sdks");
|
|
232
|
+
box.innerHTML = "";
|
|
233
|
+
for (const id of Object.keys(STATUS.recipes)) {
|
|
234
|
+
const entry = REPOS.repos[id];
|
|
235
|
+
const ready = entry && entry.enabled && entry.path;
|
|
236
|
+
const label = document.createElement("label");
|
|
237
|
+
label.className = "inline";
|
|
238
|
+
label.innerHTML = `<input type="checkbox" name="sdk"> <span></span>`;
|
|
239
|
+
$("input", label).value = id;
|
|
240
|
+
$("input", label).checked = !!ready;
|
|
241
|
+
$("span", label).textContent = id + (ready ? "" : " (no repo)");
|
|
242
|
+
box.appendChild(label);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function renderRunDestinations() {
|
|
247
|
+
for (const el of $$('input[name="dest"]')) el.checked = !!(OUTPUTS[el.value] || {}).enabled;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function startRun(dryRun) {
|
|
251
|
+
const body = {
|
|
252
|
+
sdks: $$('input[name="sdk"]:checked').map((e) => e.value),
|
|
253
|
+
artifacts: $('input[name="artifacts"]:checked').value,
|
|
254
|
+
destinations: $$('input[name="dest"]:checked').map((e) => e.value),
|
|
255
|
+
dry_run: dryRun,
|
|
256
|
+
skip_upload: $("#skip-upload").checked,
|
|
257
|
+
};
|
|
258
|
+
if (!body.sdks.length) { toast("Select at least one SDK", true); return; }
|
|
259
|
+
const { id } = await api("POST", "/api/runs", body);
|
|
260
|
+
const logEl = $("#run-log");
|
|
261
|
+
logEl.textContent = "";
|
|
262
|
+
$("#run-state").textContent = dryRun ? "planning..." : "running...";
|
|
263
|
+
$$("#dry-run, #start-run").forEach((b) => (b.disabled = true));
|
|
264
|
+
try {
|
|
265
|
+
const res = await fetch(`/api/runs/${id}/log`, { headers: { "X-SRK-Token": TOKEN } });
|
|
266
|
+
const reader = res.body.getReader();
|
|
267
|
+
const dec = new TextDecoder();
|
|
268
|
+
for (;;) {
|
|
269
|
+
const { value, done } = await reader.read();
|
|
270
|
+
if (done) break;
|
|
271
|
+
const atBottom = logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 8;
|
|
272
|
+
logEl.textContent += dec.decode(value, { stream: true });
|
|
273
|
+
if (atBottom) logEl.scrollTop = logEl.scrollHeight;
|
|
274
|
+
}
|
|
275
|
+
const st = await api("GET", `/api/runs/${id}`);
|
|
276
|
+
const ok = st.result && (st.result.dry_run || st.result.ok);
|
|
277
|
+
$("#run-state").textContent = st.error ? "error" : dryRun ? "plan ready" : ok ? "finished" : "finished with problems";
|
|
278
|
+
} finally {
|
|
279
|
+
$$("#dry-run, #start-run").forEach((b) => (b.disabled = false));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
$("#dry-run").addEventListener("click", guard(() => startRun(true)));
|
|
283
|
+
$("#start-run").addEventListener("click", guard(() => startRun(false)));
|
|
284
|
+
|
|
285
|
+
// Boot -------------------------------------------------------------------------------------
|
|
286
|
+
guard(async () => {
|
|
287
|
+
[STATUS, REPOS, OUTPUTS, KEYS] = await Promise.all([
|
|
288
|
+
api("GET", "/api/status"),
|
|
289
|
+
api("GET", "/api/settings/repos"),
|
|
290
|
+
api("GET", "/api/settings/outputs"),
|
|
291
|
+
api("GET", "/api/settings/keys"),
|
|
292
|
+
]);
|
|
293
|
+
$("#version").textContent = "v" + STATUS.version;
|
|
294
|
+
$("#repos-file").textContent = STATUS.files.repos;
|
|
295
|
+
if (STATUS.recipe_error) toast(STATUS.recipe_error, true);
|
|
296
|
+
$("#custom-text").value = (await api("GET", "/api/settings/custom")).text;
|
|
297
|
+
renderRepos();
|
|
298
|
+
renderOutputs();
|
|
299
|
+
renderKeys();
|
|
300
|
+
renderRunSdks();
|
|
301
|
+
renderRunDestinations();
|
|
302
|
+
let tab = null;
|
|
303
|
+
try { tab = localStorage.getItem("srk-tab"); } catch (_) { /* storage blocked */ }
|
|
304
|
+
if (tab) $(`#tabs button[data-tab="${tab}"]`)?.click();
|
|
305
|
+
})();
|