ota-manager 1.0.6 ā 1.0.13
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/README.md +2 -2
- package/lib/flatten-dist.cjs +6 -3
- package/lib/ota-build.cjs +505 -280
- package/lib/ota-deploy.js +84 -4
- package/lib/ota-main.js +243 -214
- package/lib/ota-rollback.js +130 -0
- package/lib/ota-version.js +34 -9
- package/package.json +1 -1
package/lib/ota-build.cjs
CHANGED
|
@@ -1,280 +1,505 @@
|
|
|
1
|
-
const { execSync }
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const https = require('https');
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
console.log('
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
console.log(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
function confirm(message) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const rl = readline.createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout
|
|
13
|
+
});
|
|
14
|
+
rl.question(`\nā ļø ${message} [y/N]: `, (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
resolve(answer.toLowerCase() === 'y');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getDirHash(dirPath) {
|
|
22
|
+
if (!fs.existsSync(dirPath)) return '';
|
|
23
|
+
const hash = crypto.createHash('sha256');
|
|
24
|
+
const files = fs.readdirSync(dirPath, { recursive: true });
|
|
25
|
+
files.sort().forEach(file => {
|
|
26
|
+
const fullPath = path.join(dirPath, file);
|
|
27
|
+
const stats = fs.statSync(fullPath);
|
|
28
|
+
if (stats.isFile()) {
|
|
29
|
+
hash.update(file);
|
|
30
|
+
hash.update(fs.readFileSync(fullPath));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return hash.digest('hex');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const rootDir = process.cwd();
|
|
37
|
+
const envPath = path.join(rootDir, '.env');
|
|
38
|
+
if (fs.existsSync(envPath)) {
|
|
39
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
40
|
+
envContent.split('\n').forEach(line => {
|
|
41
|
+
const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/);
|
|
42
|
+
if (match) {
|
|
43
|
+
const key = match[1];
|
|
44
|
+
let value = match[2] || '';
|
|
45
|
+
if (value.length > 0 && value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
|
|
46
|
+
if (value.length > 0 && value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
47
|
+
process.env[key] = value.trim();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const rawArgs = process.argv.slice(2).map(a => a.toLowerCase());
|
|
53
|
+
const hasNv = rawArgs.includes('-nv') || rawArgs.includes('--nv') || rawArgs.includes('--no-version') || rawArgs.includes('nover') || rawArgs.includes('no-ver') || rawArgs.includes('-no') || rawArgs.includes('--no') || rawArgs.includes('no') || rawArgs.includes('-norev') || rawArgs.includes('--norev') || rawArgs.includes('norev');
|
|
54
|
+
|
|
55
|
+
const cleanArgs = rawArgs.filter(a => !a.startsWith('-') && a !== 'nover' && a !== 'no-ver' && a !== 'no' && a !== 'norev');
|
|
56
|
+
const rawTarget = cleanArgs[0] || '';
|
|
57
|
+
const rawType = cleanArgs[1] || '';
|
|
58
|
+
|
|
59
|
+
let argTarget = rawTarget;
|
|
60
|
+
let buildType = rawType;
|
|
61
|
+
if (rawTarget === 'apk' || rawTarget === 'aab' || rawTarget === 'bundle') {
|
|
62
|
+
argTarget = 'android';
|
|
63
|
+
if (rawTarget === 'aab' || rawTarget === 'bundle') buildType = 'bundle';
|
|
64
|
+
}
|
|
65
|
+
if (rawTarget === 'ipa') argTarget = 'ios';
|
|
66
|
+
const GITHUB_TOKEN = process.env.GITHUB_DEV_PAT;
|
|
67
|
+
const REPO = "firstryan-sbr/180spm";
|
|
68
|
+
const WORKFLOW_ID = "ios-build.yml";
|
|
69
|
+
|
|
70
|
+
async function run() {
|
|
71
|
+
if (!argTarget) {
|
|
72
|
+
console.log('\nā Error: Please specify target (ios or android)');
|
|
73
|
+
console.log('š” Usage: npx ota-manager build ios\n');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
syncBranding();
|
|
78
|
+
|
|
79
|
+
const otaConfigPath = path.join(rootDir, 'ota-config.json');
|
|
80
|
+
let strategy = 'github';
|
|
81
|
+
let repoUrl = '';
|
|
82
|
+
if (fs.existsSync(otaConfigPath)) {
|
|
83
|
+
const otaConfig = JSON.parse(fs.readFileSync(otaConfigPath, 'utf8'));
|
|
84
|
+
strategy = otaConfig.strategy || 'github';
|
|
85
|
+
repoUrl = otaConfig[strategy]?.repo || '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const otaReleasesDir = path.join(rootDir, 'ota-releases');
|
|
89
|
+
if (repoUrl) {
|
|
90
|
+
console.log(`š Preparing remote OTA repository to synchronize version & hash...`);
|
|
91
|
+
const pat = process.env.GITHUB_DEV_PAT || process.env.GITLAB_DEV_PAT || '';
|
|
92
|
+
const cloneRepo = repoUrl.endsWith('.git') ? repoUrl : repoUrl + '.git';
|
|
93
|
+
const authRepo = cloneRepo.replace('https://', `https://${pat}@`);
|
|
94
|
+
if (fs.existsSync(otaReleasesDir)) fs.rmSync(otaReleasesDir, { recursive: true, force: true });
|
|
95
|
+
try {
|
|
96
|
+
execSync(`git clone --depth 1 ${authRepo} "${otaReleasesDir}"`, { stdio: 'ignore' });
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.log('ā ļø Could not clone remote repo, falling back to existing manifest if available.');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const manifestPath = path.join(otaReleasesDir, 'manifest.json');
|
|
103
|
+
let previousVersion = process.env.PUBLIC_APP_VERSION_ANDROID || '0.1.9.0';
|
|
104
|
+
let previousHash = null;
|
|
105
|
+
if (fs.existsSync(manifestPath)) {
|
|
106
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
107
|
+
previousVersion = manifest.version || manifest.live?.version || previousVersion;
|
|
108
|
+
previousHash = manifest.hash || manifest.live?.hash || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log('šļø Building project for Smart Diff Check...');
|
|
112
|
+
const apiDir = path.join(rootDir, 'src', 'pages', 'api');
|
|
113
|
+
const apiBackupDir = path.join(rootDir, 'src', '_api-backup');
|
|
114
|
+
let apiHidden = false;
|
|
115
|
+
if (fs.existsSync(apiDir)) {
|
|
116
|
+
console.log('š Hiding API routes (Using Robust Move)...');
|
|
117
|
+
if (fs.existsSync(apiBackupDir)) fs.rmSync(apiBackupDir, { recursive: true, force: true });
|
|
118
|
+
if (process.platform === 'win32') {
|
|
119
|
+
try { execSync(`robocopy "${apiDir}" "${apiBackupDir}" /E /MOVE /NFL /NDL /NJH /NJS`, { stdio: 'ignore' }); } catch (e) { if (fs.existsSync(apiDir)) throw e; }
|
|
120
|
+
} else {
|
|
121
|
+
fs.renameSync(apiDir, apiBackupDir);
|
|
122
|
+
}
|
|
123
|
+
apiHidden = true;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
execSync('npm run build', { stdio: 'inherit', cwd: rootDir, env: process.env });
|
|
127
|
+
} finally {
|
|
128
|
+
if (apiHidden && fs.existsSync(apiBackupDir)) {
|
|
129
|
+
console.log('šµ Restoring API routes...');
|
|
130
|
+
if (process.platform === 'win32') {
|
|
131
|
+
try { execSync(`robocopy "${apiBackupDir}" "${apiDir}" /E /MOVE /NFL /NDL /NJH /NJS`, { stdio: 'ignore' }); } catch (e) { if (fs.existsSync(apiBackupDir)) throw e; }
|
|
132
|
+
} else {
|
|
133
|
+
fs.renameSync(apiBackupDir, apiDir);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log('š§ Calculating SHA-256 Hash of dist/ artifacts...');
|
|
139
|
+
const distDir = path.join(rootDir, 'dist');
|
|
140
|
+
const currentBuildHash = getDirHash(distDir);
|
|
141
|
+
console.log(`š Current Build Hash : ${currentBuildHash.substring(0, 16)}...`);
|
|
142
|
+
if (previousHash) console.log(`š Previous Release Hash: ${previousHash.substring(0, 16)}...`);
|
|
143
|
+
|
|
144
|
+
let activeVersion = previousVersion;
|
|
145
|
+
let isChanged = (!previousHash || previousHash !== currentBuildHash);
|
|
146
|
+
|
|
147
|
+
if (hasNv) {
|
|
148
|
+
activeVersion = process.env.PUBLIC_APP_VERSION || process.env.PUBLIC_APP_VERSION_ANDROID || previousVersion;
|
|
149
|
+
console.log(`\nš”ļø [FLAG -nv DETECTED] Mempertahankan versi lokal saat ini: ${activeVersion} (No version increment)`);
|
|
150
|
+
} else if (!isChanged) {
|
|
151
|
+
console.log(`\nā¹ļø [SMART DIFF CHECKER] No code changes detected in build artifacts.`);
|
|
152
|
+
console.log(`š Version maintained at: ${previousVersion} (No version increment)`);
|
|
153
|
+
|
|
154
|
+
const ext = (buildType === 'bundle' || buildType === 'aab') ? 'aab' : 'apk';
|
|
155
|
+
const existingPath = argTarget === 'android'
|
|
156
|
+
? path.join(rootDir, 'android', 'release', `180spm-${previousVersion}.${ext}`)
|
|
157
|
+
: path.join(rootDir, 'ios', 'release', 'ios-build.zip');
|
|
158
|
+
|
|
159
|
+
console.log(`š¦ ${argTarget.toUpperCase()} file for this version is already available at:\n š ${existingPath}`);
|
|
160
|
+
|
|
161
|
+
const proceed = await confirm(`Do you want to force rebuild ${argTarget.toUpperCase()} anyway?`);
|
|
162
|
+
if (!proceed) {
|
|
163
|
+
console.log('\nā Build cancelled. Please use the existing file at the specified path.\n');
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
const parts = previousVersion.split('.');
|
|
168
|
+
const lastIdx = parts.length - 1;
|
|
169
|
+
const oldPart = parts[lastIdx];
|
|
170
|
+
const oldLen = oldPart.length;
|
|
171
|
+
parts[lastIdx] = (parseInt(oldPart) + 1).toString().padStart(oldLen, '0');
|
|
172
|
+
const nextVersion = parts.join('.');
|
|
173
|
+
|
|
174
|
+
console.log(`\nš Code changes detected!`);
|
|
175
|
+
console.log(`š Version will increment: ${previousVersion} -> ${nextVersion}`);
|
|
176
|
+
|
|
177
|
+
const proceed = await confirm(`Do you want to proceed building ${argTarget.toUpperCase()} with the new version (${nextVersion})?`);
|
|
178
|
+
if (!proceed) {
|
|
179
|
+
console.log('\nā Build cancelled.\n');
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
activeVersion = nextVersion;
|
|
184
|
+
console.log(`š Updating .env & manifest to version ${activeVersion}...`);
|
|
185
|
+
const envPath = path.join(rootDir, '.env');
|
|
186
|
+
if (fs.existsSync(envPath)) {
|
|
187
|
+
let envContent = fs.readFileSync(envPath, 'utf8');
|
|
188
|
+
envContent = envContent.replace(/PUBLIC_APP_VERSION_ANDROID=.*/, `PUBLIC_APP_VERSION_ANDROID=${activeVersion}`);
|
|
189
|
+
envContent = envContent.replace(/PUBLIC_APP_VERSION_IOS=.*/, `PUBLIC_APP_VERSION_IOS=${activeVersion}`);
|
|
190
|
+
if (envContent.includes('PUBLIC_APP_VERSION=')) {
|
|
191
|
+
envContent = envContent.replace(/PUBLIC_APP_VERSION=.*/, `PUBLIC_APP_VERSION=${activeVersion}`);
|
|
192
|
+
}
|
|
193
|
+
fs.writeFileSync(envPath, envContent);
|
|
194
|
+
process.env.PUBLIC_APP_VERSION = activeVersion;
|
|
195
|
+
}
|
|
196
|
+
const mainManifestPath = path.join(rootDir, 'src', 'data', 'update-data.json');
|
|
197
|
+
if (fs.existsSync(mainManifestPath)) {
|
|
198
|
+
const manifest = JSON.parse(fs.readFileSync(mainManifestPath, 'utf8'));
|
|
199
|
+
manifest.version = activeVersion;
|
|
200
|
+
fs.writeFileSync(mainManifestPath, JSON.stringify(manifest, null, 2));
|
|
201
|
+
}
|
|
202
|
+
const pkgJsonPath = path.join(rootDir, 'package.json');
|
|
203
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
204
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
205
|
+
pkgJson.version = activeVersion;
|
|
206
|
+
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (argTarget === 'ios') {
|
|
211
|
+
await buildIos();
|
|
212
|
+
} else if (argTarget === 'android') {
|
|
213
|
+
await buildAndroid(activeVersion, buildType);
|
|
214
|
+
} else {
|
|
215
|
+
console.log(`ā Error: Unknown target '${argTarget}'`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function buildIos() {
|
|
221
|
+
if (!GITHUB_TOKEN) {
|
|
222
|
+
console.log('ā Error: GITHUB_DEV_PAT missing in .env');
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const activeWorkflowId = buildType === 'release' ? 'ios-release.yml' : 'ios-build.yml';
|
|
227
|
+
const buildModeName = buildType === 'release' ? 'App Store Release' : 'Free Sideloading';
|
|
228
|
+
console.log(`\nš --- STARTING INTELLIGENT iOS BUILD [${buildModeName.toUpperCase()}] ---`);
|
|
229
|
+
|
|
230
|
+
const branch = execSync('git branch --show-current').toString().trim();
|
|
231
|
+
console.log(`šæ Branch: ${branch}`);
|
|
232
|
+
|
|
233
|
+
console.log('š Checking for active builds on GitHub...');
|
|
234
|
+
const activeRuns = await githubApi(`/repos/${REPO}/actions/runs?status=in_progress&branch=${branch}&workflow=${activeWorkflowId}`);
|
|
235
|
+
const queuedRuns = await githubApi(`/repos/${REPO}/actions/runs?status=queued&branch=${branch}&workflow=${activeWorkflowId}`);
|
|
236
|
+
|
|
237
|
+
let runId = null;
|
|
238
|
+
if ((activeRuns.workflow_runs && activeRuns.workflow_runs.length > 0) || (queuedRuns.workflow_runs && queuedRuns.workflow_runs.length > 0)) {
|
|
239
|
+
const existing = (activeRuns.workflow_runs && activeRuns.workflow_runs[0]) || (queuedRuns.workflow_runs && queuedRuns.workflow_runs[0]);
|
|
240
|
+
runId = existing.id;
|
|
241
|
+
console.log(`ā ļø Build already in progress (ID: ${runId}). Joining existing instance...`);
|
|
242
|
+
} else {
|
|
243
|
+
console.log('š¦ Checking for changes to push...');
|
|
244
|
+
try {
|
|
245
|
+
execSync('git add .', { stdio: 'ignore' });
|
|
246
|
+
const status = execSync('git status --porcelain').toString();
|
|
247
|
+
if (status) {
|
|
248
|
+
console.log('š¤ Pushing changes to GitHub...');
|
|
249
|
+
execSync('git commit -m "chore: automated build sync"', { stdio: 'ignore' });
|
|
250
|
+
execSync(`git push origin ${branch}`, { stdio: 'ignore' });
|
|
251
|
+
console.log('ā
Changes pushed!');
|
|
252
|
+
await sleep(2000);
|
|
253
|
+
} else {
|
|
254
|
+
console.log('ā
No local file changes. Ensuring active branch ref is pushed to remote...');
|
|
255
|
+
execSync(`git push origin ${branch}`, { stdio: 'ignore' });
|
|
256
|
+
}
|
|
257
|
+
} catch (e) {
|
|
258
|
+
console.log('ā ļø Git push skipped or branch already up to date on remote.');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log('š Triggering GitHub Action...');
|
|
262
|
+
try {
|
|
263
|
+
const payload = { ref: branch };
|
|
264
|
+
if (activeWorkflowId === 'ios-build.yml') {
|
|
265
|
+
payload.inputs = {
|
|
266
|
+
channel: process.env.PUBLIC_APP_CHANNEL || 'training'
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
await githubApi(`/repos/${REPO}/actions/workflows/${activeWorkflowId}/dispatches`, 'POST', payload);
|
|
270
|
+
console.log('ā
Trigger Success! Waiting for run to start...');
|
|
271
|
+
} catch (e) {
|
|
272
|
+
console.error('ā Failed to trigger build:', e.message);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let attempts = 0;
|
|
278
|
+
while (!runId && attempts < 10) {
|
|
279
|
+
await sleep(3000);
|
|
280
|
+
const runs = await githubApi(`/repos/${REPO}/actions/runs?branch=${branch}&per_page=5`);
|
|
281
|
+
if (runs.workflow_runs && runs.workflow_runs.length > 0) {
|
|
282
|
+
const latest = runs.workflow_runs[0];
|
|
283
|
+
if (latest.status !== 'completed') {
|
|
284
|
+
runId = latest.id;
|
|
285
|
+
console.log(`š Run ID: ${runId}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
attempts++;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!runId) {
|
|
292
|
+
console.log('ā Could not find the started run. Please check GitHub Actions web UI.');
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log('ā³ Monitoring Progress (This may take 5-10 minutes)...');
|
|
297
|
+
let status = 'queued';
|
|
298
|
+
let startTime = Date.now();
|
|
299
|
+
|
|
300
|
+
while (status !== 'completed') {
|
|
301
|
+
await sleep(5000);
|
|
302
|
+
const runData = await githubApi(`/repos/${REPO}/actions/runs/${runId}`);
|
|
303
|
+
status = runData.status;
|
|
304
|
+
const conclusion = runData.conclusion;
|
|
305
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
306
|
+
|
|
307
|
+
process.stdout.write(`\rš Status: [${status.toUpperCase()}] | Time: ${elapsed}s ... `);
|
|
308
|
+
|
|
309
|
+
if (status === 'completed') {
|
|
310
|
+
console.log('\n');
|
|
311
|
+
if (conclusion === 'success') {
|
|
312
|
+
console.log(`⨠iOS BUILD SUCCESS [${buildModeName.toUpperCase()}]!`);
|
|
313
|
+
await downloadArtifact(runId);
|
|
314
|
+
} else {
|
|
315
|
+
console.log(`ā iOS BUILD FAILED (Conclusion: ${conclusion})`);
|
|
316
|
+
console.log(`š Log: https://github.com/${REPO}/actions/runs/${runId}`);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function downloadArtifact(runId) {
|
|
324
|
+
console.log('š„ Finding artifact to download...');
|
|
325
|
+
const artifactsData = await githubApi(`/repos/${REPO}/actions/runs/${runId}/artifacts`);
|
|
326
|
+
|
|
327
|
+
if (!artifactsData.artifacts || artifactsData.artifacts.length === 0) {
|
|
328
|
+
console.log('ā No artifacts found for this run.');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const artifact = artifactsData.artifacts[0];
|
|
333
|
+
const targetDir = path.join(rootDir, 'ios', 'release');
|
|
334
|
+
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
335
|
+
|
|
336
|
+
const zipName = buildType === 'release' ? 'ios-release.zip' : 'ios-build.zip';
|
|
337
|
+
const targetFile = path.join(targetDir, zipName);
|
|
338
|
+
|
|
339
|
+
console.log(`š„ Downloading ${artifact.name} to ${targetFile}...`);
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
execSync(`curl.exe -L -H "Authorization: token ${GITHUB_TOKEN}" -o "${targetFile}" "${artifact.archive_download_url}"`, { stdio: 'inherit' });
|
|
343
|
+
console.log('\nā
Download Complete!');
|
|
344
|
+
console.log(`š¦ Path: ${targetFile}`);
|
|
345
|
+
if (buildType === 'release') {
|
|
346
|
+
console.log('š” Unzip the package to retrieve the signed "180spm-release.ipa".');
|
|
347
|
+
console.log('š” You can now upload this .ipa file directly to Apple TestFlight or App Store Connect.\n');
|
|
348
|
+
} else {
|
|
349
|
+
console.log('š” Unzip and use Sideloadly to install on iPhone.\n');
|
|
350
|
+
}
|
|
351
|
+
} catch (e) {
|
|
352
|
+
console.error('ā Download failed:', e.message);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function buildAndroid(activeVersion, buildType) {
|
|
357
|
+
console.log('\nš¤ --- STARTING LOCAL ANDROID BUILD ---');
|
|
358
|
+
console.log('ā ļø Note: This requires Java JDK and Android Studio/SDK installed.');
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
console.log('š Syncing Capacitor Android...');
|
|
362
|
+
execSync('npx cap sync android', { stdio: 'inherit', cwd: rootDir, env: process.env });
|
|
363
|
+
|
|
364
|
+
const isBundle = buildType === 'bundle' || buildType === 'aab';
|
|
365
|
+
const isRelease = isBundle || buildType === 'release';
|
|
366
|
+
const task = isBundle ? 'bundleRelease' : (isRelease ? 'assembleRelease' : 'assembleDebug');
|
|
367
|
+
const modeName = isBundle ? 'App Bundle (Release)' : (isRelease ? 'APK (Release)' : 'APK (Debug)');
|
|
368
|
+
|
|
369
|
+
console.log(`ā Running Gradle Build (${task})...`);
|
|
370
|
+
const gradleCmd = process.platform === 'win32' ? '.\\gradlew.bat' : './gradlew';
|
|
371
|
+
const androidDir = path.join(rootDir, 'android');
|
|
372
|
+
|
|
373
|
+
execSync(`${gradleCmd} ${task}`, { stdio: 'inherit', cwd: androidDir });
|
|
374
|
+
|
|
375
|
+
const targetDir = path.join(rootDir, 'android', 'release');
|
|
376
|
+
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
377
|
+
|
|
378
|
+
let sourcePath = '';
|
|
379
|
+
const ext = isBundle ? 'aab' : 'apk';
|
|
380
|
+
|
|
381
|
+
if (isBundle) {
|
|
382
|
+
sourcePath = path.join(androidDir, 'app', 'build', 'outputs', 'bundle', 'release', 'app-release.aab');
|
|
383
|
+
} else if (isRelease) {
|
|
384
|
+
sourcePath = path.join(androidDir, 'app', 'build', 'outputs', 'apk', 'release', 'app-release.apk');
|
|
385
|
+
} else {
|
|
386
|
+
sourcePath = path.join(androidDir, 'app', 'build', 'outputs', 'apk', 'debug', 'app-debug.apk');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const finalFile = path.join(targetDir, `180spm-${activeVersion || 'debug'}.${ext}`);
|
|
390
|
+
fs.copyFileSync(sourcePath, finalFile);
|
|
391
|
+
|
|
392
|
+
console.log(`\nā
ANDROID BUILD SUCCESS (${modeName})!`);
|
|
393
|
+
console.log(`š¦ Path: ${finalFile}\n`);
|
|
394
|
+
} catch (e) {
|
|
395
|
+
console.error('\nā Android Build Failed:', e.message);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function githubApi(endpoint, method = 'GET', data = null) {
|
|
401
|
+
return new Promise((resolve, reject) => {
|
|
402
|
+
const options = {
|
|
403
|
+
hostname: 'api.github.com',
|
|
404
|
+
path: endpoint,
|
|
405
|
+
method: method,
|
|
406
|
+
headers: {
|
|
407
|
+
'Authorization': `token ${GITHUB_TOKEN}`,
|
|
408
|
+
'User-Agent': 'OTA-Manager-CLI',
|
|
409
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
410
|
+
'Content-Type': 'application/json'
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const req = https.request(options, (res) => {
|
|
415
|
+
let body = '';
|
|
416
|
+
res.on('data', (chunk) => body += chunk);
|
|
417
|
+
res.on('end', () => {
|
|
418
|
+
if (res.statusCode >= 400) {
|
|
419
|
+
return reject(new Error(`GitHub API Error: ${res.statusCode} - ${body}`));
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
resolve(body ? JSON.parse(body) : {});
|
|
423
|
+
} catch (e) {
|
|
424
|
+
resolve({});
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
req.on('error', (e) => reject(e));
|
|
430
|
+
if (data) req.write(JSON.stringify(data));
|
|
431
|
+
req.end();
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function syncBranding() {
|
|
436
|
+
const appName = process.env.PUBLIC_APP_NAME || '180spm';
|
|
437
|
+
const appId = process.env.PUBLIC_APP_ID || 'com.cyclopedia.one_eighty_run';
|
|
438
|
+
const appVer = process.env.PUBLIC_APP_VERSION || process.env.PUBLIC_APP_VERSION_ANDROID || '2.0.00';
|
|
439
|
+
console.log(`⨠Syncing Branding & Version: [${appName} (${appId}) - ${appVer}]`);
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
const androidStringsPath = path.join(rootDir, 'android', 'app', 'src', 'main', 'res', 'values', 'strings.xml');
|
|
443
|
+
if (fs.existsSync(androidStringsPath)) {
|
|
444
|
+
let content = fs.readFileSync(androidStringsPath, 'utf8');
|
|
445
|
+
content = content.replace(/<string name="app_name">.*?<\/string>/, `<string name="app_name">${appName}<\/string>`);
|
|
446
|
+
fs.writeFileSync(androidStringsPath, content);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const partsStr = appVer.split('.');
|
|
450
|
+
let majorStr = partsStr[0] || '0';
|
|
451
|
+
let minorStr = partsStr[1] || '0';
|
|
452
|
+
let patchStr = partsStr[2] || '00';
|
|
453
|
+
if (patchStr.length === 1) patchStr = '0' + patchStr;
|
|
454
|
+
const versionCodeNum = parseInt(`${majorStr}${minorStr}${patchStr}`, 10) || 1;
|
|
455
|
+
|
|
456
|
+
const buildGradlePath = path.join(rootDir, 'android', 'app', 'build.gradle');
|
|
457
|
+
if (fs.existsSync(buildGradlePath)) {
|
|
458
|
+
let bgContent = fs.readFileSync(buildGradlePath, 'utf8');
|
|
459
|
+
bgContent = bgContent.replace(/applicationId\s+".*?"/, `applicationId "${appId}"`);
|
|
460
|
+
bgContent = bgContent.replace(/versionCode\s+\d+/, `versionCode ${versionCodeNum}`);
|
|
461
|
+
bgContent = bgContent.replace(/versionName\s+".*?"/, `versionName "${appVer}"`);
|
|
462
|
+
fs.writeFileSync(buildGradlePath, bgContent);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const iosPlistPath = path.join(rootDir, 'ios', 'App', 'App', 'Info.plist');
|
|
466
|
+
if (fs.existsSync(iosPlistPath)) {
|
|
467
|
+
let content = fs.readFileSync(iosPlistPath, 'utf8');
|
|
468
|
+
content = content.replace(/<key>CFBundleDisplayName<\/key>\s*<string>.*?<\/string>/, `<key>CFBundleDisplayName<\/key>\n\t<string>${appName}<\/string>`);
|
|
469
|
+
fs.writeFileSync(iosPlistPath, content);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const pbxprojPath = path.join(rootDir, 'ios', 'App', 'App.xcodeproj', 'project.pbxproj');
|
|
473
|
+
if (fs.existsSync(pbxprojPath)) {
|
|
474
|
+
let pbxContent = fs.readFileSync(pbxprojPath, 'utf8');
|
|
475
|
+
pbxContent = pbxContent.replace(/PRODUCT_BUNDLE_IDENTIFIER = .*?;/g, `PRODUCT_BUNDLE_IDENTIFIER = ${appId};`);
|
|
476
|
+
pbxContent = pbxContent.replace(/CURRENT_PROJECT_VERSION = \d+;/g, `CURRENT_PROJECT_VERSION = ${versionCodeNum};`);
|
|
477
|
+
pbxContent = pbxContent.replace(/MARKETING_VERSION = .*?;/g, `MARKETING_VERSION = ${appVer};`);
|
|
478
|
+
fs.writeFileSync(pbxprojPath, pbxContent);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const iosCapConfig = path.join(rootDir, 'ios', 'App', 'App', 'capacitor.config.json');
|
|
482
|
+
if (fs.existsSync(iosCapConfig)) {
|
|
483
|
+
let capContent = fs.readFileSync(iosCapConfig, 'utf8');
|
|
484
|
+
capContent = capContent.replace(/"appId":\s*".*?"/, `"appId": "${appId}"`);
|
|
485
|
+
capContent = capContent.replace(/"appName":\s*".*?"/, `"appName": "${appName}"`);
|
|
486
|
+
fs.writeFileSync(iosCapConfig, capContent);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const androidCapConfig = path.join(rootDir, 'android', 'app', 'src', 'main', 'assets', 'capacitor.config.json');
|
|
490
|
+
if (fs.existsSync(androidCapConfig)) {
|
|
491
|
+
let capContent = fs.readFileSync(androidCapConfig, 'utf8');
|
|
492
|
+
capContent = capContent.replace(/"appId":\s*".*?"/, `"appId": "${appId}"`);
|
|
493
|
+
capContent = capContent.replace(/"appName":\s*".*?"/, `"appName": "${appName}"`);
|
|
494
|
+
fs.writeFileSync(androidCapConfig, capContent);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
console.log(`ā
Branding & native versions synchronized (versionCode: ${versionCodeNum}).`);
|
|
498
|
+
} catch (e) {
|
|
499
|
+
console.error('ā ļø Warning: Failed to sync branding/version:', e.message);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
|
|
504
|
+
|
|
505
|
+
run();
|