ota-manager 1.0.7 ā 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 +31 -4
- package/lib/ota-rollback.js +130 -0
- package/lib/ota-version.js +34 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Modern mobile hybrid apps (built with Astro, Vite, or Capacitor) require a robus
|
|
|
17
17
|
* š **Flexible CLI Shorthands**: Supports both `npx ota-manager` and `npx ota-updates`. Features lightning-fast shorthands like `npx ota-manager -d training` and `-d live`.
|
|
18
18
|
* š **Multi-Provider & Multi-Channel Routing**: Built-in support for GitHub and GitLab strategies. Configure independent target repositories and branches for `training` vs `live` environments.
|
|
19
19
|
* š”ļø **Size Guardian Protocol**: Pre-flight audit of your `dist/` directory and generated ZIP archive to prevent Zip Bombs (>50MB threshold protection).
|
|
20
|
-
*
|
|
20
|
+
* āļø **Absolute Path Normalization (`flatten-dist.cjs`)**: Post-build normalization of absolute asset paths (`/assets/`) to relative paths (`./assets/`) to guarantee flawless Capacitor WebView navigation.
|
|
21
21
|
* š **API Route Protection**: Automatically isolates and hides `/src/pages/api` during static export/build to prevent build failures, then restores them seamlessly.
|
|
22
22
|
* š **Security Auditor (`ota-security.js`)**: Automated inspection of Personal Access Tokens (PAT) to prevent token leaks or overly broad repository access.
|
|
23
23
|
* š” **E2E Connection Simulation**: Built-in `test` command to simulate push and read capabilities against your Git provider before executing actual deployments.
|
|
@@ -124,7 +124,7 @@ PUBLIC_OTA_UPDATE_URL=https://raw.githubusercontent.com/your-org/your-ota-repo/m
|
|
|
124
124
|
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
125
125
|
ā¼
|
|
126
126
|
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
127
|
-
ā 3. Build &
|
|
127
|
+
ā 3. Build & Path Normalization: Normalizes /assets/ paths ā
|
|
128
128
|
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
129
129
|
ā¼
|
|
130
130
|
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
package/lib/flatten-dist.cjs
CHANGED
|
@@ -2,9 +2,12 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* ABSOLUTE PATH NORMALIZATION UTILITY:
|
|
6
6
|
* Menghancurkan SEMUA variasi "/assets/" dan ".//assets/" di seluruh file
|
|
7
7
|
* tanpa peduli ada tanda kutip atau tidak.
|
|
8
|
+
|
|
9
|
+
Catatan: Mengubah jalur absolut menjadi relatif agar aplikasi Capacitor
|
|
10
|
+
dapat merender aset dengan benar di WebView lokal.
|
|
8
11
|
*/
|
|
9
12
|
function fixPathsInFile(filePath) {
|
|
10
13
|
let content = fs.readFileSync(filePath, 'utf8');
|
|
@@ -32,7 +35,7 @@ function fixPathsInFile(filePath) {
|
|
|
32
35
|
|
|
33
36
|
if (content !== fixedContent) {
|
|
34
37
|
fs.writeFileSync(filePath, fixedContent);
|
|
35
|
-
console.log(`
|
|
38
|
+
console.log(` āļø PATH NORMALIZATION in: ${path.basename(filePath)}`);
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -70,7 +73,7 @@ function flatten(dir, rootDir = dir) {
|
|
|
70
73
|
|
|
71
74
|
const distPath = path.join(process.cwd(), 'dist');
|
|
72
75
|
if (fs.existsSync(distPath)) {
|
|
73
|
-
console.log('š
|
|
76
|
+
console.log('š PATH NORMALIZATION: Running absolute path normalization...');
|
|
74
77
|
flatten(distPath);
|
|
75
78
|
console.log('ā
ALL PATHS ARE NOW 100% PURE RELATIVE!');
|
|
76
79
|
} else {
|
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();
|
package/lib/ota-deploy.js
CHANGED
|
@@ -160,7 +160,7 @@ async function deployOTA() {
|
|
|
160
160
|
execSync('npm run build', { stdio: 'inherit', cwd: rootDir });
|
|
161
161
|
|
|
162
162
|
if (fs.existsSync(FLATTEN_SCRIPT)) {
|
|
163
|
-
console.log('š Running
|
|
163
|
+
console.log('š Running Absolute Path Normalization...');
|
|
164
164
|
execSync(`node "${FLATTEN_SCRIPT}"`, { stdio: 'inherit', cwd: rootDir });
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -192,7 +192,9 @@ async function deployOTA() {
|
|
|
192
192
|
console.log('š Changes detected! Auto-incrementing from remote manifest...');
|
|
193
193
|
const parts = previousVersion.split('.');
|
|
194
194
|
const lastIdx = parts.length - 1;
|
|
195
|
-
|
|
195
|
+
const oldPart = parts[lastIdx];
|
|
196
|
+
const oldLen = oldPart.length;
|
|
197
|
+
parts[lastIdx] = (parseInt(oldPart) + 1).toString().padStart(oldLen, '0');
|
|
196
198
|
nextVersion = parts.join('.');
|
|
197
199
|
console.log(`š Auto-increment: ${previousVersion} -> ${nextVersion}`);
|
|
198
200
|
}
|
|
@@ -221,12 +223,90 @@ async function deployOTA() {
|
|
|
221
223
|
|
|
222
224
|
console.log(`š Updating .env to version ${nextVersion} (Android & iOS) and setting OTA target...`);
|
|
223
225
|
fs.writeFileSync(ENV_PATH, updatedEnv);
|
|
226
|
+
|
|
227
|
+
const envLivePath = path.join(rootDir, '.env-live');
|
|
228
|
+
const envTrainingPath = path.join(rootDir, '.env-training');
|
|
229
|
+
if (argChannel === 'live' && fs.existsSync(envLivePath)) {
|
|
230
|
+
let liveEnv = fs.readFileSync(envLivePath, 'utf-8');
|
|
231
|
+
liveEnv = liveEnv.replace(/PUBLIC_APP_VERSION=.*/, `PUBLIC_APP_VERSION=${nextVersion}`);
|
|
232
|
+
if (liveEnv.includes('PUBLIC_OTA_UPDATE_URL=')) {
|
|
233
|
+
liveEnv = liveEnv.replace(/PUBLIC_OTA_UPDATE_URL=.*/, `PUBLIC_OTA_UPDATE_URL=${activeOtaUrl}`);
|
|
234
|
+
} else {
|
|
235
|
+
liveEnv += `\nPUBLIC_OTA_UPDATE_URL=${activeOtaUrl}`;
|
|
236
|
+
}
|
|
237
|
+
fs.writeFileSync(envLivePath, liveEnv);
|
|
238
|
+
console.log(`š Updated .env-live to version ${nextVersion}`);
|
|
239
|
+
}
|
|
240
|
+
if (argChannel === 'training' && fs.existsSync(envTrainingPath)) {
|
|
241
|
+
let trainingEnv = fs.readFileSync(envTrainingPath, 'utf-8');
|
|
242
|
+
trainingEnv = trainingEnv.replace(/PUBLIC_APP_VERSION=.*/, `PUBLIC_APP_VERSION=${nextVersion}`);
|
|
243
|
+
if (trainingEnv.includes('PUBLIC_OTA_UPDATE_URL=')) {
|
|
244
|
+
trainingEnv = trainingEnv.replace(/PUBLIC_OTA_UPDATE_URL=.*/, `PUBLIC_OTA_UPDATE_URL=${activeOtaUrl}`);
|
|
245
|
+
} else {
|
|
246
|
+
trainingEnv += `\nPUBLIC_OTA_UPDATE_URL=${activeOtaUrl}`;
|
|
247
|
+
}
|
|
248
|
+
fs.writeFileSync(envTrainingPath, trainingEnv);
|
|
249
|
+
console.log(`š Updated .env-training to version ${nextVersion}`);
|
|
250
|
+
}
|
|
224
251
|
|
|
252
|
+
// Sync Native Android & iOS Versions
|
|
253
|
+
try {
|
|
254
|
+
const partsStr = nextVersion.split('.');
|
|
255
|
+
let majorStr = partsStr[0] || '0';
|
|
256
|
+
let minorStr = partsStr[1] || '0';
|
|
257
|
+
let patchStr = partsStr[2] || '00';
|
|
258
|
+
if (patchStr.length === 1) patchStr = '0' + patchStr;
|
|
259
|
+
const versionCodeNum = parseInt(`${majorStr}${minorStr}${patchStr}`, 10) || 1;
|
|
260
|
+
|
|
261
|
+
const buildGradlePath = path.join(rootDir, 'android', 'app', 'build.gradle');
|
|
262
|
+
if (fs.existsSync(buildGradlePath)) {
|
|
263
|
+
let bgContent = fs.readFileSync(buildGradlePath, 'utf8');
|
|
264
|
+
bgContent = bgContent.replace(/versionCode\s+\d+/, `versionCode ${versionCodeNum}`);
|
|
265
|
+
bgContent = bgContent.replace(/versionName\s+".*?"/, `versionName "${nextVersion}"`);
|
|
266
|
+
fs.writeFileSync(buildGradlePath, bgContent);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const pbxprojPath = path.join(rootDir, 'ios', 'App', 'App.xcodeproj', 'project.pbxproj');
|
|
270
|
+
if (fs.existsSync(pbxprojPath)) {
|
|
271
|
+
let pbxContent = fs.readFileSync(pbxprojPath, 'utf8');
|
|
272
|
+
pbxContent = pbxContent.replace(/CURRENT_PROJECT_VERSION = \d+;/g, `CURRENT_PROJECT_VERSION = ${versionCodeNum};`);
|
|
273
|
+
pbxContent = pbxContent.replace(/MARKETING_VERSION = .*?;/g, `MARKETING_VERSION = ${nextVersion};`);
|
|
274
|
+
fs.writeFileSync(pbxprojPath, pbxContent);
|
|
275
|
+
}
|
|
276
|
+
console.log(`ā
Native Android & iOS versions synchronized (versionCode: ${versionCodeNum}).`);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
console.error('ā ļø Warning: Failed to sync native versions:', err.message);
|
|
279
|
+
}
|
|
280
|
+
|
|
225
281
|
if (fs.existsSync(MAIN_MANIFEST_PATH)) {
|
|
226
282
|
const manifest = JSON.parse(fs.readFileSync(MAIN_MANIFEST_PATH, 'utf8'));
|
|
227
283
|
manifest.version = nextVersion;
|
|
228
284
|
fs.writeFileSync(MAIN_MANIFEST_PATH, JSON.stringify(manifest, null, 2));
|
|
229
285
|
}
|
|
286
|
+
const pkgJsonPath = path.join(rootDir, 'package.json');
|
|
287
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
288
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
289
|
+
pkgJson.version = nextVersion;
|
|
290
|
+
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.log('šļø Rebuilding Astro to bake new version string into JS bundle...');
|
|
294
|
+
hideApi();
|
|
295
|
+
try {
|
|
296
|
+
execSync('npm run build', { stdio: 'inherit', cwd: rootDir });
|
|
297
|
+
|
|
298
|
+
if (fs.existsSync(FLATTEN_SCRIPT)) {
|
|
299
|
+
console.log('š Running Absolute Path Normalization...');
|
|
300
|
+
execSync(`node "${FLATTEN_SCRIPT}"`, { stdio: 'inherit', cwd: rootDir });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (fs.existsSync(VERIFY_SCRIPT)) {
|
|
304
|
+
console.log('š Running Pre-Flight Verification...');
|
|
305
|
+
execSync(`node "${VERIFY_SCRIPT}"`, { stdio: 'inherit', cwd: rootDir });
|
|
306
|
+
}
|
|
307
|
+
} finally {
|
|
308
|
+
showApi();
|
|
309
|
+
}
|
|
230
310
|
|
|
231
311
|
console.log('š”ļø Size Guardian: Checking dist/ folder size...');
|
|
232
312
|
const distSizeBytes = getDirSize(DIST_DIR);
|
|
@@ -237,7 +317,7 @@ async function deployOTA() {
|
|
|
237
317
|
throw new Error(`CRITICAL SIZE VIOLATION: Folder dist/ has bloated to ${distSizeMB.toFixed(2)} MB! Limit is ${MAX_OTA_SIZE_MB} MB. Packaging aborted to prevent ZIP BOMB!`);
|
|
238
318
|
}
|
|
239
319
|
|
|
240
|
-
const otaName =
|
|
320
|
+
const otaName = `${nextVersion.replace(/\./g, '_')}.zip`;
|
|
241
321
|
const otaPath = path.join(rootDir, otaName);
|
|
242
322
|
|
|
243
323
|
const isWindows = process.platform === 'win32';
|
|
@@ -306,7 +386,7 @@ async function deployToRemote(otaPath, otaName, version, channel, branch, buildH
|
|
|
306
386
|
fs.writeFileSync(flatManifestPath, JSON.stringify(manifest[channel], null, 2));
|
|
307
387
|
|
|
308
388
|
execSync(`git add .`, { cwd: OTA_RELEASES_DIR });
|
|
309
|
-
execSync(`git commit -m "release:
|
|
389
|
+
execSync(`git commit -m "release: ${version} for ${channel}"`, { cwd: OTA_RELEASES_DIR });
|
|
310
390
|
execSync(`git push origin ${branch}`, { cwd: OTA_RELEASES_DIR });
|
|
311
391
|
|
|
312
392
|
if (fs.existsSync(otaPath)) fs.unlinkSync(otaPath);
|
package/lib/ota-main.js
CHANGED
|
@@ -23,6 +23,7 @@ const scripts = {
|
|
|
23
23
|
verify: path.join(__dirname, 'verify-dist.cjs'),
|
|
24
24
|
security: path.join(__dirname, 'ota-security.js'),
|
|
25
25
|
build: path.join(__dirname, 'ota-build.cjs'),
|
|
26
|
+
rollback: path.join(__dirname, 'ota-rollback.js'),
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
function showHelp() {
|
|
@@ -43,9 +44,18 @@ Management Commands:
|
|
|
43
44
|
test : Run E2E simulation (Push & Read).
|
|
44
45
|
|
|
45
46
|
Operational Commands:
|
|
46
|
-
status
|
|
47
|
-
deploy training
|
|
48
|
-
deploy live
|
|
47
|
+
status : Check local vs remote version.
|
|
48
|
+
deploy training : Deploy update to TRAINING channel.
|
|
49
|
+
deploy live : Deploy update to LIVE channel.
|
|
50
|
+
rollback <v> [ch] : Rollback / Downgrade ke versi lama (e.g., rollback 2.0.00 training).
|
|
51
|
+
|
|
52
|
+
Native Build Commands:
|
|
53
|
+
build android : Build Android APK Debug (default).
|
|
54
|
+
build android release : Build Android APK Release (assembleRelease).
|
|
55
|
+
build android bundle : Build Android App Bundle (.aab) siap rilis Play Store.
|
|
56
|
+
build ios : Trigger iOS GitHub Actions build & sync branding (Debug/Sideloading).
|
|
57
|
+
build ios release : Trigger iOS App Store Release signed build & sync branding.
|
|
58
|
+
* Gunakan flag 'no', 'norev', 'nover', '-no', atau '-nv' di akhir perintah untuk mencegah kenaikan versi.
|
|
49
59
|
|
|
50
60
|
Active Infrastructure:
|
|
51
61
|
Strategy : ${OTA_CONFIG.strategy.toUpperCase()}
|
|
@@ -125,7 +135,8 @@ async function run() {
|
|
|
125
135
|
break;
|
|
126
136
|
|
|
127
137
|
case 'build':
|
|
128
|
-
|
|
138
|
+
const allArgs = process.argv.slice(3).join(' ');
|
|
139
|
+
execSync(`node "${scripts.build}" ${allArgs}`, { stdio: 'inherit' });
|
|
129
140
|
process.exit(0);
|
|
130
141
|
break;
|
|
131
142
|
|
|
@@ -136,6 +147,22 @@ async function run() {
|
|
|
136
147
|
process.exit(0);
|
|
137
148
|
break;
|
|
138
149
|
|
|
150
|
+
case 'rollback':
|
|
151
|
+
case 'downgrade':
|
|
152
|
+
case 'set-version':
|
|
153
|
+
if (!subArg) {
|
|
154
|
+
console.log('ā Error: Mohon tentukan versi tujuan (e.g., npx ota-manager rollback 0.2.0.11 training).');
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
const rbChannel = versionArg || 'training';
|
|
158
|
+
if (await confirm(`ARE YOU SURE you want to rollback ${rbChannel.toUpperCase()} to version ${subArg}?`)) {
|
|
159
|
+
execSync(`node "${scripts.rollback}" ${subArg} ${rbChannel}`, { stdio: 'inherit' });
|
|
160
|
+
} else {
|
|
161
|
+
console.log('ā Rollback cancelled.');
|
|
162
|
+
}
|
|
163
|
+
process.exit(0);
|
|
164
|
+
break;
|
|
165
|
+
|
|
139
166
|
case 'training':
|
|
140
167
|
execSync(`node "${scripts.version}"`, { stdio: 'inherit' });
|
|
141
168
|
execSync(`node "${scripts.verify}"`, { stdio: 'inherit' });
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import OTA_CONFIG from './ota-config.js';
|
|
5
|
+
|
|
6
|
+
const rootDir = process.cwd();
|
|
7
|
+
const ENV_PATH = path.join(rootDir, '.env');
|
|
8
|
+
const MAIN_MANIFEST_PATH = path.join(rootDir, 'src', 'data', 'update-data.json');
|
|
9
|
+
const OTA_RELEASES_DIR = path.join(rootDir, 'ota-releases');
|
|
10
|
+
|
|
11
|
+
let githubPat = '';
|
|
12
|
+
let gitlabPat = '';
|
|
13
|
+
if (fs.existsSync(ENV_PATH)) {
|
|
14
|
+
const envContent = fs.readFileSync(ENV_PATH, 'utf-8');
|
|
15
|
+
envContent.split('\n').forEach(line => {
|
|
16
|
+
const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/);
|
|
17
|
+
if (match) {
|
|
18
|
+
if (match[1] === 'GITHUB_DEV_PAT') githubPat = match[2].replace(/['"]/g, '').trim();
|
|
19
|
+
if (match[1] === 'GITLAB_DEV_PAT') gitlabPat = match[2].replace(/['"]/g, '').trim();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getRawBaseUrl(repoUrl, strategy, branch = 'main') {
|
|
25
|
+
let base = repoUrl.replace(/\/$/, '');
|
|
26
|
+
if (strategy === 'github') {
|
|
27
|
+
return base.replace('github.com', 'raw.githubusercontent.com') + `/${branch}`;
|
|
28
|
+
} else if (strategy === 'gitlab') {
|
|
29
|
+
return base + `/-/raw/${branch}`;
|
|
30
|
+
}
|
|
31
|
+
return base;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function rollbackOTA() {
|
|
35
|
+
const targetVersion = process.argv[2];
|
|
36
|
+
const argChannel = process.argv[3] || 'training';
|
|
37
|
+
|
|
38
|
+
if (!targetVersion) {
|
|
39
|
+
console.log('ā Error: Mohon tentukan versi tujuan rollback (e.g., npx ota-manager rollback 0.2.0.11 training)');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`\nš --- MEMULAI PROSES ROLLBACK OTA (${argChannel.toUpperCase()}) ---`);
|
|
44
|
+
console.log(`šÆ Target Versi : v${targetVersion}`);
|
|
45
|
+
console.log(`š¹ Strategy : ${OTA_CONFIG.strategy.toUpperCase()}`);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const config = OTA_CONFIG[OTA_CONFIG.strategy];
|
|
49
|
+
if (!config || !config.repo) {
|
|
50
|
+
throw new Error(`Repository not configured for strategy "${OTA_CONFIG.strategy}".`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const channelConfig = config.channels?.[argChannel];
|
|
54
|
+
const activeBranch = channelConfig?.branch || config.branch || 'main';
|
|
55
|
+
|
|
56
|
+
console.log(`š Menyiapkan repositori OTA jarak jauh (Branch: ${activeBranch})...`);
|
|
57
|
+
if (fs.existsSync(OTA_RELEASES_DIR)) {
|
|
58
|
+
fs.rmSync(OTA_RELEASES_DIR, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const pat = OTA_CONFIG.strategy === 'gitlab' ? gitlabPat : githubPat;
|
|
62
|
+
const cloneRepo = config.repo.endsWith('.git') ? config.repo : config.repo + '.git';
|
|
63
|
+
const authRepo = cloneRepo.replace('https://', `https://${pat}@`);
|
|
64
|
+
|
|
65
|
+
execSync(`git clone --branch ${activeBranch} ${authRepo} "${OTA_RELEASES_DIR}"`, { stdio: 'inherit' });
|
|
66
|
+
|
|
67
|
+
const zipFileName = `v${targetVersion.replace(/\./g, '_')}.zip`;
|
|
68
|
+
const zipFilePath = path.join(OTA_RELEASES_DIR, zipFileName);
|
|
69
|
+
|
|
70
|
+
console.log(`š Memeriksa ketersediaan bungkusan fisik: ${zipFileName} di server...`);
|
|
71
|
+
if (!fs.existsSync(zipFilePath)) {
|
|
72
|
+
console.log(`\nā ERROR FATAL: File bungkusan ${zipFileName} TIDAK DITEMUKAN di repositori jarak jauh!`);
|
|
73
|
+
console.log(`š” Rollback dibatalkan karena versi tujuan tidak memiliki bungkusan fisik yang valid.`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
console.log(`ā
File bungkusan fisik ${zipFileName} tersedia dan valid!`);
|
|
77
|
+
|
|
78
|
+
const manifestFileName = argChannel === 'training' ? 'manifest-training.json' : 'manifest.json';
|
|
79
|
+
const manifestPath = path.join(OTA_RELEASES_DIR, manifestFileName);
|
|
80
|
+
|
|
81
|
+
const rawBaseUrl = getRawBaseUrl(config.repo, OTA_CONFIG.strategy, activeBranch);
|
|
82
|
+
const activeOtaUrl = `${rawBaseUrl}/${manifestFileName}`;
|
|
83
|
+
const activeZipUrl = `${rawBaseUrl}/${zipFileName}`;
|
|
84
|
+
|
|
85
|
+
console.log(`š Memperbarui ${manifestFileName} jarak jauh ke versi ${targetVersion}...`);
|
|
86
|
+
const manifest = {
|
|
87
|
+
version: targetVersion,
|
|
88
|
+
url: activeZipUrl
|
|
89
|
+
};
|
|
90
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
91
|
+
|
|
92
|
+
console.log(`š Memperbarui .env lokal ke versi ${targetVersion}...`);
|
|
93
|
+
if (fs.existsSync(ENV_PATH)) {
|
|
94
|
+
let envContent = fs.readFileSync(ENV_PATH, 'utf-8');
|
|
95
|
+
envContent = envContent.replace(/PUBLIC_APP_VERSION_ANDROID=.*/, `PUBLIC_APP_VERSION_ANDROID=${targetVersion}`);
|
|
96
|
+
envContent = envContent.replace(/PUBLIC_APP_VERSION_IOS=.*/, `PUBLIC_APP_VERSION_IOS=${targetVersion}`);
|
|
97
|
+
if (envContent.includes('PUBLIC_APP_VERSION=')) {
|
|
98
|
+
envContent = envContent.replace(/PUBLIC_APP_VERSION=.*/, `PUBLIC_APP_VERSION=${targetVersion}`);
|
|
99
|
+
}
|
|
100
|
+
if (envContent.includes('PUBLIC_OTA_UPDATE_URL=')) {
|
|
101
|
+
envContent = envContent.replace(/PUBLIC_OTA_UPDATE_URL=.*/, `PUBLIC_OTA_UPDATE_URL=${activeOtaUrl}`);
|
|
102
|
+
} else {
|
|
103
|
+
envContent += `\nPUBLIC_OTA_UPDATE_URL=${activeOtaUrl}`;
|
|
104
|
+
}
|
|
105
|
+
fs.writeFileSync(ENV_PATH, envContent);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (fs.existsSync(MAIN_MANIFEST_PATH)) {
|
|
109
|
+
const updateManifest = JSON.parse(fs.readFileSync(MAIN_MANIFEST_PATH, 'utf8'));
|
|
110
|
+
updateManifest.version = targetVersion;
|
|
111
|
+
fs.writeFileSync(MAIN_MANIFEST_PATH, JSON.stringify(updateManifest, null, 2));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(`š¤ Mengirim manifest rollback ke repositori OTA jarak jauh...`);
|
|
115
|
+
execSync('git add .', { cwd: OTA_RELEASES_DIR, stdio: 'ignore' });
|
|
116
|
+
execSync(`git commit -m "chore: rollback OTA manifest to v${targetVersion} (${argChannel})"`, { cwd: OTA_RELEASES_DIR, stdio: 'ignore' });
|
|
117
|
+
execSync(`git push origin ${activeBranch}`, { cwd: OTA_RELEASES_DIR, stdio: 'inherit' });
|
|
118
|
+
|
|
119
|
+
console.log(`\nš ROLLBACK SUKSES BERHASIL DIEKSEKUSI!`);
|
|
120
|
+
console.log(`š Versi Aktif Sekarang : v${targetVersion}`);
|
|
121
|
+
console.log(`š Channel : ${argChannel}`);
|
|
122
|
+
console.log(`š” Capgo di HP pengguna akan langsung mendeteksi manifest baru dan melakukan downgrade otomatis ke v${targetVersion}.\n`);
|
|
123
|
+
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.error(`\nā Rollback Gagal: ${e.message}\n`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
rollbackOTA();
|
package/lib/ota-version.js
CHANGED
|
@@ -25,10 +25,32 @@ async function checkStatus() {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// 1. Get Local Version
|
|
28
|
+
// 1. Get Local Version & Native Status
|
|
29
29
|
const envContent = fs.readFileSync(ENV_PATH, 'utf-8');
|
|
30
|
-
const localVersion = envContent.match(/PUBLIC_APP_VERSION_ANDROID=(.*)/)?.[1]?.trim().replace(/^"|"$/g, '');
|
|
31
|
-
|
|
30
|
+
const localVersion = envContent.match(/PUBLIC_APP_VERSION_ANDROID=(.*)/)?.[1]?.trim().replace(/^"|"$/g, '') || envContent.match(/PUBLIC_APP_VERSION=(.*)/)?.[1]?.trim().replace(/^"|"$/g, '');
|
|
31
|
+
const appId = envContent.match(/PUBLIC_APP_ID=(.*)/)?.[1]?.trim().replace(/^"|"$/g, '') || 'com.cyclopedia.one_eighty_run';
|
|
32
|
+
console.log(`š» Local Version (.env) : ${localVersion || 'unknown'}`);
|
|
33
|
+
console.log(`š¦ App ID / Package Name : ${appId}`);
|
|
34
|
+
|
|
35
|
+
// Cek Android versionCode
|
|
36
|
+
const buildGradlePath = path.join(rootDir, 'android', 'app', 'build.gradle');
|
|
37
|
+
let androidVersionCode = 'unknown';
|
|
38
|
+
if (fs.existsSync(buildGradlePath)) {
|
|
39
|
+
const bgContent = fs.readFileSync(buildGradlePath, 'utf8');
|
|
40
|
+
const match = bgContent.match(/versionCode\s+(\d+)/);
|
|
41
|
+
if (match) androidVersionCode = match[1];
|
|
42
|
+
}
|
|
43
|
+
console.log(`š¤ Android versionCode : ${androidVersionCode}`);
|
|
44
|
+
|
|
45
|
+
// Cek iOS CURRENT_PROJECT_VERSION
|
|
46
|
+
const pbxprojPath = path.join(rootDir, 'ios', 'App', 'App.xcodeproj', 'project.pbxproj');
|
|
47
|
+
let iosProjectVersion = 'unknown';
|
|
48
|
+
if (fs.existsSync(pbxprojPath)) {
|
|
49
|
+
const pbxContent = fs.readFileSync(pbxprojPath, 'utf8');
|
|
50
|
+
const match = pbxContent.match(/CURRENT_PROJECT_VERSION = (\d+);/);
|
|
51
|
+
if (match) iosProjectVersion = match[1];
|
|
52
|
+
}
|
|
53
|
+
console.log(`š iOS Project Version : ${iosProjectVersion}`);
|
|
32
54
|
|
|
33
55
|
// 2. Get Remote Version based on Config
|
|
34
56
|
const strategy = OTA_CONFIG.strategy;
|
|
@@ -83,18 +105,21 @@ async function checkStatus() {
|
|
|
83
105
|
}
|
|
84
106
|
|
|
85
107
|
const remoteData = JSON.parse(result);
|
|
86
|
-
const remoteVersion = remoteData.version;
|
|
108
|
+
const remoteVersion = remoteData.version || remoteData[channel]?.version;
|
|
87
109
|
|
|
88
|
-
if (remoteVersion
|
|
89
|
-
console.log(
|
|
110
|
+
if (!remoteVersion) {
|
|
111
|
+
console.log(`⨠Status: CHANNEL "${channel.toUpperCase()}" KOSONG (Belum ada rilis)`);
|
|
112
|
+
console.log(`š” Jalankan 'npm run ota-manager deploy ${channel}' untuk merilis versi perdana ke channel ini!`);
|
|
113
|
+
} else if (remoteVersion === localVersion) {
|
|
114
|
+
console.log(`ā
Status: UP TO DATE (${remoteVersion})`);
|
|
90
115
|
} else {
|
|
91
116
|
console.log(`ā ļø Status: OUTDATED!`);
|
|
92
|
-
console.log(`š” Remote Version :
|
|
93
|
-
console.log(`š” Run '
|
|
117
|
+
console.log(`š” Remote Version : ${remoteVersion}`);
|
|
118
|
+
console.log(`š” Run 'npm run ota-updates ${channel}' to update.`);
|
|
94
119
|
}
|
|
95
120
|
} catch (e) {
|
|
96
121
|
if (e.message === 'File not found') {
|
|
97
|
-
console.log(`⨠Status: NEW INFRASTRUCTURE (
|
|
122
|
+
console.log(`⨠Status: NEW INFRASTRUCTURE (0.0.0)`);
|
|
98
123
|
console.log(`š” No releases found on server. Ready for initial deployment!`);
|
|
99
124
|
} else {
|
|
100
125
|
console.log(`ā Could not fetch remote manifest.`);
|