kasy-cli 1.31.5 → 1.31.7
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/bin/kasy.js +20 -3
- package/docs/cli-reference.md +8 -6
- package/lib/commands/codemagic.js +110 -45
- package/lib/commands/new.js +5 -0
- package/lib/commands/reset.js +6 -6
- package/lib/commands/run.js +11 -11
- package/lib/scaffold/engine.js +5 -3
- package/lib/scaffold/generate.js +7 -0
- package/lib/utils/codemagic-release.js +122 -13
- package/lib/utils/env-tools.js +66 -0
- package/lib/utils/flutter-install.js +8 -0
- package/lib/utils/flutter-run.js +5 -25
- package/lib/utils/i18n/messages-en.js +18 -7
- package/lib/utils/i18n/messages-es.js +18 -7
- package/lib/utils/i18n/messages-pt.js +18 -7
- package/package.json +1 -1
- package/templates/firebase/codemagic.yaml +213 -0
- package/templates/firebase/docs/codemagic-release.en.md +45 -18
- package/templates/firebase/docs/codemagic-release.es.md +40 -14
- package/templates/firebase/docs/codemagic-release.pt.md +42 -15
package/bin/kasy.js
CHANGED
|
@@ -602,7 +602,22 @@ function buildProgram(language) {
|
|
|
602
602
|
],
|
|
603
603
|
});
|
|
604
604
|
if (sub === 'configure') await runCodemagicConfigure('.', { language });
|
|
605
|
-
else if (sub === 'release')
|
|
605
|
+
else if (sub === 'release') {
|
|
606
|
+
const platform = await ui.select({
|
|
607
|
+
message: t('cli.command.codemagic.release.platformPick'),
|
|
608
|
+
options: [
|
|
609
|
+
{ value: 'both', label: t('cli.command.codemagic.release.platform.both') },
|
|
610
|
+
{ value: 'ios', label: t('cli.command.codemagic.release.platform.ios') },
|
|
611
|
+
{ value: 'android', label: t('cli.command.codemagic.release.platform.android') },
|
|
612
|
+
],
|
|
613
|
+
});
|
|
614
|
+
if (typeof platform === 'string') {
|
|
615
|
+
const relOpts = { language };
|
|
616
|
+
if (platform === 'ios') relOpts.ios = true;
|
|
617
|
+
else if (platform === 'android') relOpts.android = true;
|
|
618
|
+
await runCodemagicRelease('.', relOpts);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
606
621
|
else if (sub === 'status') {
|
|
607
622
|
const buildId = await ui.text({
|
|
608
623
|
message: 'Build ID',
|
|
@@ -625,9 +640,11 @@ function buildProgram(language) {
|
|
|
625
640
|
codemagicCmd
|
|
626
641
|
.command('release')
|
|
627
642
|
.argument('[directory]', 'Project folder (default: current directory)', '.')
|
|
643
|
+
.option('--ios', t('cli.command.codemagic.release.iosOption'))
|
|
644
|
+
.option('--android', t('cli.command.codemagic.release.androidOption'))
|
|
628
645
|
.description(t('cli.command.codemagic.release.description'))
|
|
629
|
-
.action(async (directory) => {
|
|
630
|
-
await runCodemagicRelease(directory, { language });
|
|
646
|
+
.action(async (directory, opts) => {
|
|
647
|
+
await runCodemagicRelease(directory, { language, ios: opts.ios, android: opts.android });
|
|
631
648
|
}),
|
|
632
649
|
t
|
|
633
650
|
);
|
package/docs/cli-reference.md
CHANGED
|
@@ -121,15 +121,17 @@ Atalho: `make release-ios`
|
|
|
121
121
|
|
|
122
122
|
---
|
|
123
123
|
|
|
124
|
-
### `kasy codemagic` — Release iOS (nuvem)
|
|
124
|
+
### `kasy codemagic` — Release iOS/Android (nuvem)
|
|
125
125
|
|
|
126
|
-
Dispara build iOS no Codemagic (ideal sem Mac).
|
|
126
|
+
Dispara build **iOS e Android** no Codemagic (ideal sem Mac), levando as chaves do `.env` no disparo.
|
|
127
127
|
|
|
128
128
|
```bash
|
|
129
|
-
kasy add ci
|
|
130
|
-
kasy codemagic configure
|
|
131
|
-
kasy codemagic release
|
|
132
|
-
kasy codemagic
|
|
129
|
+
kasy add ci # entrega o codemagic.yaml (se necessário)
|
|
130
|
+
kasy codemagic configure # token API → valida e lista seus apps → branch
|
|
131
|
+
kasy codemagic release # iOS + Android na nuvem
|
|
132
|
+
kasy codemagic release --ios # só iOS
|
|
133
|
+
kasy codemagic release --android # só Android
|
|
134
|
+
kasy codemagic status <id> # status do build
|
|
133
135
|
```
|
|
134
136
|
|
|
135
137
|
Documentação: `docs/codemagic-release.md` (idioma da CLI)
|
|
@@ -11,18 +11,39 @@ const {
|
|
|
11
11
|
codemagicReleaseDocPath,
|
|
12
12
|
writeCodemagicEnv,
|
|
13
13
|
validateCodemagicSetup,
|
|
14
|
+
loadProjectEnv,
|
|
15
|
+
listApps,
|
|
16
|
+
workflowIdFor,
|
|
17
|
+
resolveReleaseVars,
|
|
14
18
|
triggerBuild,
|
|
15
19
|
getBuildStatus,
|
|
16
20
|
URL_CODEMAGIC_APPS,
|
|
17
21
|
URL_CODEMAGIC_API,
|
|
18
22
|
} = require('../utils/codemagic-release');
|
|
19
23
|
|
|
24
|
+
// Human label per platform (proper nouns — same across locales).
|
|
25
|
+
const PLATFORM_LABEL = { ios: 'iOS', android: 'Android' };
|
|
26
|
+
|
|
20
27
|
async function assertProject(projectDir, t) {
|
|
21
28
|
if (!(await isKasyFlutterProject(projectDir))) {
|
|
22
29
|
throw new Error(t('codemagic.error.notProject'));
|
|
23
30
|
}
|
|
24
31
|
}
|
|
25
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Decide which platforms to release. Explicit flags win; with no flag we build
|
|
35
|
+
* both. Returns an ordered list like ['ios', 'android'].
|
|
36
|
+
*/
|
|
37
|
+
function platformsFromOptions(options) {
|
|
38
|
+
const wantIos = !!options.ios;
|
|
39
|
+
const wantAndroid = !!options.android;
|
|
40
|
+
if (!wantIos && !wantAndroid) return ['ios', 'android'];
|
|
41
|
+
const platforms = [];
|
|
42
|
+
if (wantIos) platforms.push('ios');
|
|
43
|
+
if (wantAndroid) platforms.push('android');
|
|
44
|
+
return platforms;
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
async function runConfigure(directory, options = {}) {
|
|
27
48
|
const lang = options.language || detectDefaultLanguage();
|
|
28
49
|
const t = createTranslator(lang);
|
|
@@ -35,39 +56,62 @@ async function runConfigure(directory, options = {}) {
|
|
|
35
56
|
ui.log.message(kleur.dim(`${t('codemagic.configure.doc')}: ${codemagicReleaseDocPath(lang)}`));
|
|
36
57
|
ui.log.warn(t('codemagic.configure.checklist'));
|
|
37
58
|
|
|
38
|
-
ui.log.info(t('codemagic.configure.openingLinks'));
|
|
39
|
-
openUrl(URL_CODEMAGIC_API);
|
|
40
|
-
openUrl(URL_CODEMAGIC_APPS);
|
|
41
|
-
|
|
42
59
|
const cancel = () => { ui.cancel(t('codemagic.configure.cancelled')); process.exit(0); };
|
|
43
60
|
const required = (v) => (v && String(v).trim().length > 0 ? undefined : t('codemagic.configure.q.required'));
|
|
44
61
|
|
|
45
|
-
|
|
62
|
+
// 1. API token — open the settings page so the user can copy it.
|
|
63
|
+
ui.log.info(t('codemagic.configure.openingToken'));
|
|
64
|
+
openUrl(URL_CODEMAGIC_API);
|
|
65
|
+
const tokenRaw = await ui.password({
|
|
46
66
|
message: t('codemagic.configure.q.token'),
|
|
47
67
|
validate: required,
|
|
48
68
|
onCancel: cancel,
|
|
49
69
|
});
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
})
|
|
60
|
-
|
|
70
|
+
const token = String(tokenRaw).trim();
|
|
71
|
+
|
|
72
|
+
// 2. Validate the token by listing the user's apps.
|
|
73
|
+
const spinner = ui.spinner();
|
|
74
|
+
spinner.start(t('codemagic.configure.validating'));
|
|
75
|
+
let apps;
|
|
76
|
+
try {
|
|
77
|
+
apps = await listApps(token);
|
|
78
|
+
spinner.stop(t('codemagic.configure.validated'));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
spinner.stop(`${t('codemagic.configure.tokenInvalid')} (${err.message})`, 2);
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 3. Pick the app (or guide the user to connect a repo first).
|
|
86
|
+
let appId;
|
|
87
|
+
if (!apps.length) {
|
|
88
|
+
ui.log.warn(t('codemagic.configure.noApps'));
|
|
89
|
+
openUrl(URL_CODEMAGIC_APPS);
|
|
90
|
+
const manual = await ui.text({
|
|
91
|
+
message: t('codemagic.configure.q.appId'),
|
|
92
|
+
validate: required,
|
|
93
|
+
onCancel: cancel,
|
|
94
|
+
});
|
|
95
|
+
appId = String(manual).trim();
|
|
96
|
+
} else {
|
|
97
|
+
appId = await ui.select({
|
|
98
|
+
message: t('codemagic.configure.pickApp'),
|
|
99
|
+
options: apps.map((a) => ({ value: a.id, label: a.name, hint: a.id })),
|
|
100
|
+
});
|
|
101
|
+
if (typeof appId === 'symbol') cancel();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. Branch that triggers the build.
|
|
105
|
+
const branchRaw = await ui.text({
|
|
61
106
|
message: t('codemagic.configure.q.branch'),
|
|
62
107
|
initialValue: 'main',
|
|
63
108
|
onCancel: cancel,
|
|
64
109
|
});
|
|
65
110
|
|
|
66
111
|
await writeCodemagicEnv(projectDir, {
|
|
67
|
-
token
|
|
68
|
-
appId
|
|
69
|
-
|
|
70
|
-
branch: String(branch).trim() || 'main',
|
|
112
|
+
token,
|
|
113
|
+
appId,
|
|
114
|
+
branch: String(branchRaw).trim() || 'main',
|
|
71
115
|
});
|
|
72
116
|
|
|
73
117
|
ui.log.success(t('codemagic.configure.success'));
|
|
@@ -87,44 +131,64 @@ async function runRelease(directory, options = {}) {
|
|
|
87
131
|
if (validation.issues.includes('missing_yaml')) {
|
|
88
132
|
ui.log.message(kleur.dim(t('codemagic.error.addCi')));
|
|
89
133
|
}
|
|
90
|
-
if (validation.issues.includes('missing_env')) {
|
|
134
|
+
if (validation.issues.includes('missing_env') || validation.issues.includes('missing_token') || validation.issues.includes('missing_app_id')) {
|
|
91
135
|
ui.log.message(kleur.dim(t('codemagic.error.runConfigure')));
|
|
92
136
|
}
|
|
93
137
|
process.exitCode = 1;
|
|
94
138
|
return;
|
|
95
139
|
}
|
|
96
140
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
141
|
+
const platforms = platformsFromOptions(options);
|
|
142
|
+
|
|
143
|
+
// iOS builds need the Google Sign-In URL scheme to be in sync.
|
|
144
|
+
if (platforms.includes('ios')) {
|
|
145
|
+
const googleScheme = await validateGoogleIosUrlScheme(projectDir);
|
|
146
|
+
if (!googleScheme.ok) {
|
|
147
|
+
printCompactHeader(t);
|
|
148
|
+
ui.log.error(t('codemagic.error.googleUrlScheme'));
|
|
149
|
+
ui.log.message(kleur.dim(googleScheme.error));
|
|
150
|
+
ui.log.info(t('ios.error.googleUrlSchemeHint'));
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
105
154
|
}
|
|
106
155
|
|
|
107
156
|
printCompactHeader(t);
|
|
108
157
|
ui.intro(t('codemagic.release.title'));
|
|
109
158
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
159
|
+
// Read the project's keys and carry them with the build.
|
|
160
|
+
const projectEnv = await loadProjectEnv(projectDir);
|
|
161
|
+
const variables = resolveReleaseVars(projectEnv);
|
|
162
|
+
|
|
163
|
+
if (platforms.includes('ios') && !variables.RC_IOS_PROD_KEY) {
|
|
164
|
+
ui.log.warn(t('codemagic.release.noIosKey'));
|
|
165
|
+
}
|
|
166
|
+
if (platforms.includes('android') && !variables.RC_ANDROID_PROD_KEY) {
|
|
167
|
+
ui.log.warn(t('codemagic.release.noAndroidKey'));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let anyFailed = false;
|
|
171
|
+
for (const platform of platforms) {
|
|
172
|
+
const label = PLATFORM_LABEL[platform];
|
|
173
|
+
const workflowId = workflowIdFor(validation.env, platform);
|
|
174
|
+
const spinner = ui.spinner();
|
|
175
|
+
spinner.start(t('codemagic.release.spinPlatform', { platform: label }));
|
|
176
|
+
try {
|
|
177
|
+
const result = await triggerBuild(validation.env, { workflowId, variables });
|
|
178
|
+
const buildId = result.buildId || result._id;
|
|
179
|
+
spinner.stop(t('codemagic.release.triggeredPlatform', { platform: label }));
|
|
180
|
+
if (buildId) {
|
|
181
|
+
ui.log.info(`Build ID: ${kleur.cyan(buildId)}`);
|
|
182
|
+
ui.log.message(kleur.dim(`https://codemagic.io/app/${validation.env.CODEMAGIC_APP_ID}/build/${buildId}`));
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
anyFailed = true;
|
|
186
|
+
spinner.stop(`${label}: ${err.message}`, 2);
|
|
123
187
|
}
|
|
124
|
-
} catch (err) {
|
|
125
|
-
spinner.stop(err.message, 2);
|
|
126
|
-
process.exitCode = 1;
|
|
127
188
|
}
|
|
189
|
+
|
|
190
|
+
if (anyFailed) process.exitCode = 1;
|
|
191
|
+
ui.outro('');
|
|
128
192
|
}
|
|
129
193
|
|
|
130
194
|
async function runStatus(buildId, directory, options = {}) {
|
|
@@ -168,4 +232,5 @@ module.exports = {
|
|
|
168
232
|
runConfigure,
|
|
169
233
|
runRelease,
|
|
170
234
|
runStatus,
|
|
235
|
+
platformsFromOptions,
|
|
171
236
|
};
|
package/lib/commands/new.js
CHANGED
|
@@ -1733,6 +1733,11 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1733
1733
|
// generation step once choices are made. The success card at the end shows
|
|
1734
1734
|
// the full summary. Cancel any time with Ctrl+C.
|
|
1735
1735
|
|
|
1736
|
+
// Windows: disable native assets before the generator runs pub get / builds,
|
|
1737
|
+
// so a username with a space (C:\Users\John Silva) doesn't break the
|
|
1738
|
+
// objective_c hook compile. No-op elsewhere, idempotent, best-effort.
|
|
1739
|
+
require('../utils/env-tools').disableNativeAssetsWindows();
|
|
1740
|
+
|
|
1736
1741
|
// ── Generate ────────────────────────────────────────────────────────────
|
|
1737
1742
|
// Quick: single rolling line that mutates message. Advanced: stack each step.
|
|
1738
1743
|
const stepper = ui.makeQuickStepper({ color: paintLime });
|
package/lib/commands/reset.js
CHANGED
|
@@ -7,9 +7,7 @@ const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
|
7
7
|
const { printCompactHeader } = require('../utils/brand');
|
|
8
8
|
const { readBundleId, readPackageName } = require('../utils/mobile-identity');
|
|
9
9
|
const { spawnFlutterWithSpinner } = require('../utils/flutter-run');
|
|
10
|
-
const {
|
|
11
|
-
|
|
12
|
-
const isWindows = process.platform === 'win32';
|
|
10
|
+
const { spawnSyncFlutter, disableNativeAssetsWindows } = require('../utils/env-tools');
|
|
13
11
|
|
|
14
12
|
function runCmd(cmd, args) {
|
|
15
13
|
const res = spawnSync(cmd, args, { encoding: 'utf8' });
|
|
@@ -21,11 +19,9 @@ function runCmd(cmd, args) {
|
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
async function listFlutterDevices(projectDir) {
|
|
24
|
-
const res =
|
|
22
|
+
const res = spawnSyncFlutter(['devices', '--machine'], {
|
|
25
23
|
cwd: projectDir,
|
|
26
24
|
encoding: 'utf8',
|
|
27
|
-
env: augmentedEnv(),
|
|
28
|
-
shell: isWindows,
|
|
29
25
|
});
|
|
30
26
|
if (res.status !== 0) return [];
|
|
31
27
|
try {
|
|
@@ -268,6 +264,10 @@ async function runReset(directory, options = {}) {
|
|
|
268
264
|
throw new Error(t('reset.error.notFlutterProject'));
|
|
269
265
|
}
|
|
270
266
|
|
|
267
|
+
// Windows: disable native assets before the reinstall rebuild so a username
|
|
268
|
+
// with a space doesn't break the objective_c hook compile. No-op elsewhere.
|
|
269
|
+
disableNativeAssetsWindows();
|
|
270
|
+
|
|
271
271
|
printCompactHeader(t);
|
|
272
272
|
ui.intro(t('reset.title'));
|
|
273
273
|
|
package/lib/commands/run.js
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
const path = require('node:path');
|
|
2
|
-
const { spawnSync } = require('node:child_process');
|
|
3
2
|
const fs = require('fs-extra');
|
|
4
3
|
const kleur = require('kleur');
|
|
5
4
|
const ui = require('../utils/ui');
|
|
6
5
|
const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
7
6
|
const { printCompactHeader, paintLime } = require('../utils/brand');
|
|
8
7
|
const { spawnFlutterWithSpinner } = require('../utils/flutter-run');
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
const isWindows = process.platform === 'win32';
|
|
8
|
+
const { spawnSyncFlutter, disableNativeAssetsWindows } = require('../utils/env-tools');
|
|
12
9
|
|
|
13
10
|
function listFlutterDevices(projectDir) {
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// `kasy new` reports "Flutter not found" here
|
|
18
|
-
const res =
|
|
11
|
+
// Shared flutter spawner: exposes a freshly-installed SDK on PATH (the terminal
|
|
12
|
+
// may not have it yet right after `kasy new`) and handles the Windows flutter.bat
|
|
13
|
+
// without tripping Node's shell+args deprecation warning. Without it, a machine
|
|
14
|
+
// that just ran `kasy new` reports "Flutter not found" here.
|
|
15
|
+
const res = spawnSyncFlutter(['devices', '--machine'], {
|
|
19
16
|
cwd: projectDir,
|
|
20
17
|
encoding: 'utf8',
|
|
21
|
-
env: augmentedEnv(),
|
|
22
|
-
shell: isWindows,
|
|
23
18
|
});
|
|
24
19
|
if (res.status !== 0) return [];
|
|
25
20
|
try {
|
|
@@ -254,6 +249,11 @@ async function runRun(directory, options = {}) {
|
|
|
254
249
|
throw new Error(t('run.error.notFlutterProject'));
|
|
255
250
|
}
|
|
256
251
|
|
|
252
|
+
// Windows: turn off native assets before building so a username with a space
|
|
253
|
+
// (C:\Users\John Silva) doesn't break the objective_c hook compile. No-op on
|
|
254
|
+
// macOS/Linux, idempotent, and best-effort — see disableNativeAssetsWindows.
|
|
255
|
+
disableNativeAssetsWindows();
|
|
256
|
+
|
|
257
257
|
// Resolve device flag. If none of the platform shortcuts or -d is set,
|
|
258
258
|
// ask the user when more than one device is available — `flutter run`
|
|
259
259
|
// bails out with "More than one device connected" otherwise.
|
package/lib/scaffold/engine.js
CHANGED
|
@@ -27,12 +27,14 @@ const ALWAYS_EXCLUDE = new Set([
|
|
|
27
27
|
'.cursor',
|
|
28
28
|
'.idea',
|
|
29
29
|
'.claude', // AI assistant instructions — not for the client
|
|
30
|
-
//
|
|
31
|
-
// (these files live in features/ci/ and are applied as a feature patch)
|
|
30
|
+
// GitHub/GitLab CI pipelines are not shipped to generated projects yet.
|
|
32
31
|
'.github',
|
|
33
32
|
'.gitlab',
|
|
34
33
|
'.gitlab-ci.yml',
|
|
35
|
-
|
|
34
|
+
// NOTE: codemagic.yaml is intentionally NOT excluded here. It ships in the
|
|
35
|
+
// base template and generate.js removes it unless the 'ci' module is
|
|
36
|
+
// selected (see the "Phase B" gating). This is what lets `kasy codemagic`
|
|
37
|
+
// work in a generated project.
|
|
36
38
|
]);
|
|
37
39
|
|
|
38
40
|
// File basenames that should NEVER be copied, regardless of directory depth
|
package/lib/scaffold/generate.js
CHANGED
|
@@ -236,6 +236,13 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
|
|
|
236
236
|
// Remove directories of modules that were NOT selected
|
|
237
237
|
await removeModuleDirs(targetDir, modules);
|
|
238
238
|
|
|
239
|
+
// codemagic.yaml ships in the base template (so `kasy codemagic` works out
|
|
240
|
+
// of the box). It belongs to the 'ci' module — strip it when ci was not
|
|
241
|
+
// selected, mirroring how the other optional modules are gated above.
|
|
242
|
+
if (!modules.includes('ci')) {
|
|
243
|
+
await fs.remove(path.join(targetDir, 'codemagic.yaml'));
|
|
244
|
+
}
|
|
245
|
+
|
|
239
246
|
// Write no-op analytics_api.dart if analytics not selected
|
|
240
247
|
if (!modules.includes('analytics')) {
|
|
241
248
|
await writeNoOpAnalyticsApi(targetDir, packageName);
|
|
@@ -8,18 +8,22 @@ const CODEMAGIC_ENV_REL = path.join('.kasy', 'codemagic.env');
|
|
|
8
8
|
const CODEMAGIC_API = 'https://api.codemagic.io';
|
|
9
9
|
|
|
10
10
|
const URL_CODEMAGIC_APPS = 'https://codemagic.io/apps';
|
|
11
|
-
|
|
11
|
+
// Codemagic API token lives in the account settings → Integrations page.
|
|
12
|
+
const URL_CODEMAGIC_API = 'https://codemagic.io/settings';
|
|
13
|
+
|
|
14
|
+
// Default workflow ids defined in the codemagic.yaml shipped by the `ci` feature.
|
|
15
|
+
const DEFAULT_IOS_WORKFLOW = 'ios-workflow';
|
|
16
|
+
const DEFAULT_ANDROID_WORKFLOW = 'android-workflow';
|
|
12
17
|
|
|
13
18
|
function codemagicReleaseDocPath() {
|
|
14
19
|
return 'docs/codemagic-release.md';
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const content = await fs.readFile(envPath, 'utf8');
|
|
22
|
+
/**
|
|
23
|
+
* Parse a simple KEY=VALUE env file into an object. Ignores blanks/comments.
|
|
24
|
+
* Values keep everything after the first '=' (so URLs with ':' survive).
|
|
25
|
+
*/
|
|
26
|
+
function parseEnvContent(content) {
|
|
23
27
|
const env = {};
|
|
24
28
|
for (const line of content.split('\n')) {
|
|
25
29
|
const trimmed = line.trim();
|
|
@@ -31,12 +35,32 @@ async function loadCodemagicEnv(projectDir) {
|
|
|
31
35
|
return env;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
async function
|
|
38
|
+
async function loadCodemagicEnv(projectDir) {
|
|
39
|
+
const envPath = path.join(projectDir, CODEMAGIC_ENV_REL);
|
|
40
|
+
if (!(await fs.pathExists(envPath))) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return parseEnvContent(await fs.readFile(envPath, 'utf8'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read the project's runtime `.env` (the app's config). Returns {} when absent.
|
|
48
|
+
*/
|
|
49
|
+
async function loadProjectEnv(projectDir) {
|
|
50
|
+
const envPath = path.join(projectDir, '.env');
|
|
51
|
+
if (!(await fs.pathExists(envPath))) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
return parseEnvContent(await fs.readFile(envPath, 'utf8'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function writeCodemagicEnv(projectDir, { token, appId, iosWorkflowId, androidWorkflowId, branch }) {
|
|
35
58
|
const lines = [
|
|
36
59
|
'# Generated by kasy codemagic configure — do not commit',
|
|
37
60
|
`CODEMAGIC_API_TOKEN=${token}`,
|
|
38
61
|
`CODEMAGIC_APP_ID=${appId}`,
|
|
39
|
-
`
|
|
62
|
+
`CODEMAGIC_IOS_WORKFLOW_ID=${iosWorkflowId || DEFAULT_IOS_WORKFLOW}`,
|
|
63
|
+
`CODEMAGIC_ANDROID_WORKFLOW_ID=${androidWorkflowId || DEFAULT_ANDROID_WORKFLOW}`,
|
|
40
64
|
`CODEMAGIC_BRANCH=${branch || 'main'}`,
|
|
41
65
|
'',
|
|
42
66
|
];
|
|
@@ -83,12 +107,92 @@ function httpsJson(method, urlPath, token, body) {
|
|
|
83
107
|
});
|
|
84
108
|
}
|
|
85
109
|
|
|
86
|
-
|
|
110
|
+
/**
|
|
111
|
+
* List the Codemagic applications the token can access.
|
|
112
|
+
* Returns [{ id, name }] so the configure wizard can let the user pick one
|
|
113
|
+
* instead of pasting a raw Mongo id.
|
|
114
|
+
*/
|
|
115
|
+
async function listApps(token) {
|
|
116
|
+
const result = await httpsJson('GET', '/apps', token);
|
|
117
|
+
const apps = Array.isArray(result) ? result : (result.applications || []);
|
|
118
|
+
return apps
|
|
119
|
+
.map((a) => ({ id: a._id || a.id, name: a.appName || a.name || a.repository?.name || '(unnamed app)' }))
|
|
120
|
+
.filter((a) => a.id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolve which workflow id to trigger for a platform, honoring custom ids
|
|
125
|
+
* saved at configure time and falling back to the shipped defaults. The legacy
|
|
126
|
+
* single CODEMAGIC_WORKFLOW_ID (older configs) is treated as the iOS workflow.
|
|
127
|
+
*/
|
|
128
|
+
function workflowIdFor(env, platform) {
|
|
129
|
+
if (platform === 'android') {
|
|
130
|
+
return env.CODEMAGIC_ANDROID_WORKFLOW_ID || DEFAULT_ANDROID_WORKFLOW;
|
|
131
|
+
}
|
|
132
|
+
return env.CODEMAGIC_IOS_WORKFLOW_ID || env.CODEMAGIC_WORKFLOW_ID || DEFAULT_IOS_WORKFLOW;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Build the environment variables to inject at trigger time, read from the
|
|
137
|
+
* project's `.env`. These travel with the build so the cloud has the SAME
|
|
138
|
+
* production keys you use locally — the user does not have to fill the
|
|
139
|
+
* Codemagic variable groups by hand.
|
|
140
|
+
*
|
|
141
|
+
* RevenueCat SDK keys (appl_/goog_/test_) are public keys, safe to send this
|
|
142
|
+
* way. Real secrets (keystore, App Store key, service account) stay in the
|
|
143
|
+
* dashboard and are NOT sent here.
|
|
144
|
+
*/
|
|
145
|
+
function resolveReleaseVars(projectEnv) {
|
|
146
|
+
const vars = { ENV: 'prod' };
|
|
147
|
+
|
|
148
|
+
const passthrough = [
|
|
149
|
+
'BACKEND_URL',
|
|
150
|
+
'RC_TEST_KEY',
|
|
151
|
+
'RC_IOS_PROD_KEY',
|
|
152
|
+
'RC_ANDROID_PROD_KEY',
|
|
153
|
+
'MIXPANEL_TOKEN',
|
|
154
|
+
'SENTRY_DSN',
|
|
155
|
+
];
|
|
156
|
+
for (const key of passthrough) {
|
|
157
|
+
const value = projectEnv[key];
|
|
158
|
+
if (value && value.trim()) vars[key] = value.trim();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// LLM chat endpoint may be named either way across template versions; the app
|
|
162
|
+
// reads AI_CHAT_ENDPOINT.
|
|
163
|
+
const endpoint = projectEnv.AI_CHAT_ENDPOINT || projectEnv.LLM_CHAT_ENDPOINT;
|
|
164
|
+
if (endpoint && endpoint.trim()) vars.AI_CHAT_ENDPOINT = endpoint.trim();
|
|
165
|
+
|
|
166
|
+
// Legacy single-key projects (pre test/prod split): promote a non-test key
|
|
167
|
+
// into the prod slot so release builds still get a real key.
|
|
168
|
+
if (!vars.RC_IOS_PROD_KEY && projectEnv.RC_IOS_API_KEY && !projectEnv.RC_IOS_API_KEY.startsWith('test_')) {
|
|
169
|
+
vars.RC_IOS_PROD_KEY = projectEnv.RC_IOS_API_KEY.trim();
|
|
170
|
+
}
|
|
171
|
+
if (!vars.RC_ANDROID_PROD_KEY && projectEnv.RC_ANDROID_API_KEY && !projectEnv.RC_ANDROID_API_KEY.startsWith('test_')) {
|
|
172
|
+
vars.RC_ANDROID_PROD_KEY = projectEnv.RC_ANDROID_API_KEY.trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Numeric App Store id → APP_ID, used by the iOS workflow for build numbering.
|
|
176
|
+
if (projectEnv.APP_STORE_ID && projectEnv.APP_STORE_ID.trim()) {
|
|
177
|
+
vars.APP_ID = projectEnv.APP_STORE_ID.trim();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return vars;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Trigger one build. `variables` is injected via the build's environment so the
|
|
185
|
+
* cloud build carries the project's production keys.
|
|
186
|
+
*/
|
|
187
|
+
async function triggerBuild(env, { workflowId, branch, variables }) {
|
|
87
188
|
const body = {
|
|
88
189
|
appId: env.CODEMAGIC_APP_ID,
|
|
89
|
-
workflowId
|
|
90
|
-
branch: env.CODEMAGIC_BRANCH || 'main',
|
|
190
|
+
workflowId,
|
|
191
|
+
branch: branch || env.CODEMAGIC_BRANCH || 'main',
|
|
91
192
|
};
|
|
193
|
+
if (variables && Object.keys(variables).length > 0) {
|
|
194
|
+
body.environment = { variables };
|
|
195
|
+
}
|
|
92
196
|
return httpsJson('POST', '/builds', env.CODEMAGIC_API_TOKEN, body);
|
|
93
197
|
}
|
|
94
198
|
|
|
@@ -109,7 +213,6 @@ async function validateCodemagicSetup(projectDir) {
|
|
|
109
213
|
}
|
|
110
214
|
if (!env.CODEMAGIC_API_TOKEN) issues.push('missing_token');
|
|
111
215
|
if (!env.CODEMAGIC_APP_ID) issues.push('missing_app_id');
|
|
112
|
-
if (!env.CODEMAGIC_WORKFLOW_ID) issues.push('missing_workflow');
|
|
113
216
|
return { ok: issues.length === 0, issues, env };
|
|
114
217
|
}
|
|
115
218
|
|
|
@@ -118,9 +221,15 @@ module.exports = {
|
|
|
118
221
|
CODEMAGIC_API,
|
|
119
222
|
URL_CODEMAGIC_APPS,
|
|
120
223
|
URL_CODEMAGIC_API,
|
|
224
|
+
DEFAULT_IOS_WORKFLOW,
|
|
225
|
+
DEFAULT_ANDROID_WORKFLOW,
|
|
121
226
|
codemagicReleaseDocPath,
|
|
122
227
|
loadCodemagicEnv,
|
|
228
|
+
loadProjectEnv,
|
|
123
229
|
writeCodemagicEnv,
|
|
230
|
+
listApps,
|
|
231
|
+
workflowIdFor,
|
|
232
|
+
resolveReleaseVars,
|
|
124
233
|
triggerBuild,
|
|
125
234
|
getBuildStatus,
|
|
126
235
|
validateCodemagicSetup,
|