ota-manager 1.0.4 ā 1.0.6
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 +143 -56
- package/lib/ota-deploy.js +315 -284
- package/package.json +43 -43
package/README.md
CHANGED
|
@@ -1,56 +1,143 @@
|
|
|
1
|
-
# ota-manager
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
1
|
+
# š¦ ota-manager
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<h3>Enterprise-Grade Over-The-Air (OTA) Update Manager for Astro, Capacitor, and Static Web Apps</h3>
|
|
5
|
+
<p>Seamlessly deploy live updates to mobile WebViews and web apps without going through App Store or Play Store reviews.</p>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## ⨠Why `ota-manager`?
|
|
11
|
+
|
|
12
|
+
Modern mobile hybrid apps (built with Astro, Vite, or Capacitor) require a robust, foolproof update pipeline. `ota-manager` provides an end-to-end automated deployment system with built-in safeguards against common deployment pitfalls like broken asset paths, zip bombs, and token leaks.
|
|
13
|
+
|
|
14
|
+
### š Key Features
|
|
15
|
+
|
|
16
|
+
* š **Universal & Project-Agnostic**: Dynamically detects `process.cwd()` to integrate flawlessly into any Astro, Vite, Next.js, or static web project.
|
|
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
|
+
* š **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
|
+
* š”ļø **Size Guardian Protocol**: Pre-flight audit of your `dist/` directory and generated ZIP archive to prevent Zip Bombs (>50MB threshold protection).
|
|
20
|
+
* š„ **Tsar Bomba Path Cleanse (`flatten-dist.cjs`)**: Post-build normalization of absolute asset paths (`/assets/`) to relative paths (`./assets/`) to guarantee flawless Capacitor WebView navigation.
|
|
21
|
+
* š **API Route Protection**: Automatically isolates and hides `/src/pages/api` during static export/build to prevent build failures, then restores them seamlessly.
|
|
22
|
+
* š **Security Auditor (`ota-security.js`)**: Automated inspection of Personal Access Tokens (PAT) to prevent token leaks or overly broad repository access.
|
|
23
|
+
* š” **E2E Connection Simulation**: Built-in `test` command to simulate push and read capabilities against your Git provider before executing actual deployments.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## š¦ Installation
|
|
28
|
+
|
|
29
|
+
Install `ota-manager` as a development dependency in your project:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install ota-manager --save-dev
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## š Quick Start Guide
|
|
38
|
+
|
|
39
|
+
### 1. Initialize Infrastructure
|
|
40
|
+
Register your Git provider (GitHub or GitLab) to initialize `ota-config.json` and `.env` credentials:
|
|
41
|
+
```bash
|
|
42
|
+
npx ota-manager register github
|
|
43
|
+
# or: npx ota-manager register gitlab
|
|
44
|
+
```
|
|
45
|
+
*Follow the interactive prompts to enter your repository URL and Access Tokens.*
|
|
46
|
+
|
|
47
|
+
### 2. Verify Connection & Security
|
|
48
|
+
Run an end-to-end simulation to ensure your tokens and repository permissions are perfectly configured:
|
|
49
|
+
```bash
|
|
50
|
+
npx ota-manager test
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 3. Check Version Gap
|
|
54
|
+
Compare your local `.env` app version against the remote release manifest:
|
|
55
|
+
```bash
|
|
56
|
+
npx ota-manager status
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 4. Deploy Update
|
|
60
|
+
Deploy your build to the `training` or `live` channel with automated pre-flight checks, path cleansing, and size auditing:
|
|
61
|
+
```bash
|
|
62
|
+
# Deploy to Training Channel
|
|
63
|
+
npx ota-manager deploy training
|
|
64
|
+
# Shorthand alias:
|
|
65
|
+
npx ota-manager -d training
|
|
66
|
+
|
|
67
|
+
# Deploy to Live (Production) Channel
|
|
68
|
+
npx ota-manager deploy live
|
|
69
|
+
# Shorthand alias:
|
|
70
|
+
npx ota-manager -d live
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## āļø Advanced Configuration
|
|
76
|
+
|
|
77
|
+
`ota-manager` stores its active strategy in `ota-config.json` and sensitive tokens in your `.env` file.
|
|
78
|
+
|
|
79
|
+
### Flexible Channel Routing (`ota-config.json`)
|
|
80
|
+
You can configure different branches or even different repositories for your `training` and `live` channels:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"strategy": "github",
|
|
85
|
+
"github": {
|
|
86
|
+
"repo": "https://github.com/your-org/your-ota-repo",
|
|
87
|
+
"branch": "main",
|
|
88
|
+
"channels": {
|
|
89
|
+
"training": {
|
|
90
|
+
"branch": "main"
|
|
91
|
+
},
|
|
92
|
+
"live": {
|
|
93
|
+
"branch": "ota-live"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Environment Variables (`.env`)
|
|
101
|
+
The manager automatically updates your versioning and OTA target URLs during deployment:
|
|
102
|
+
```env
|
|
103
|
+
GITHUB_DEV_PAT="ghp_your_developer_token_here"
|
|
104
|
+
PUBLIC_APP_VERSION_ANDROID=1.0.4
|
|
105
|
+
PUBLIC_APP_VERSION_IOS=1.0.4
|
|
106
|
+
PUBLIC_OTA_UPDATE_URL=https://raw.githubusercontent.com/your-org/your-ota-repo/main/manifest.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## š”ļø Built-in Safeguards Architecture
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
115
|
+
ā npx ota-manager -d live ā
|
|
116
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
117
|
+
ā¼
|
|
118
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
119
|
+
ā 1. Pre-Flight Check: ota-version & verify-dist ā
|
|
120
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
121
|
+
ā¼
|
|
122
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
123
|
+
ā 2. API Route Protection: Hides /src/pages/api ā
|
|
124
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
125
|
+
ā¼
|
|
126
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
127
|
+
ā 3. Build & Tsar Bomba Cleanse: Normalizes /assets/ paths ā
|
|
128
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
129
|
+
ā¼
|
|
130
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
131
|
+
ā 4. Size Guardian Audit: Validates dist/ & ZIP < 50MB ā
|
|
132
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
133
|
+
ā¼
|
|
134
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
135
|
+
ā 5. Remote Push & Manifest Update (GitHub / GitLab) ā
|
|
136
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## š License
|
|
142
|
+
|
|
143
|
+
MIT Ā© [First Ryan](https://github.com/firstryan)
|
package/lib/ota-deploy.js
CHANGED
|
@@ -1,284 +1,315 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { execSync } from 'child_process';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
fs.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (!fs.existsSync(dirPath)) return
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function
|
|
87
|
-
let
|
|
88
|
-
if (
|
|
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
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
const
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import OTA_CONFIG from './ota-config.js';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const rootDir = process.cwd();
|
|
11
|
+
|
|
12
|
+
const OTA_RELEASES_DIR = path.join(rootDir, 'ota-releases');
|
|
13
|
+
const DIST_DIR = path.join(rootDir, 'dist');
|
|
14
|
+
const ENV_PATH = path.join(rootDir, '.env');
|
|
15
|
+
const MAIN_MANIFEST_PATH = path.join(rootDir, 'src', 'data', 'update-data.json');
|
|
16
|
+
const API_DIR = path.join(rootDir, 'src', 'pages', 'api');
|
|
17
|
+
const API_BACKUP_DIR = path.join(rootDir, 'src', '_api-backup');
|
|
18
|
+
|
|
19
|
+
const VERIFY_SCRIPT = path.join(__dirname, 'verify-dist.cjs');
|
|
20
|
+
const FLATTEN_SCRIPT = path.join(__dirname, 'flatten-dist.cjs');
|
|
21
|
+
|
|
22
|
+
function hideApi() {
|
|
23
|
+
if (fs.existsSync(API_DIR)) {
|
|
24
|
+
console.log('š Hiding API routes (Using Robust Move)...');
|
|
25
|
+
try {
|
|
26
|
+
if (fs.existsSync(API_BACKUP_DIR)) fs.rmSync(API_BACKUP_DIR, { recursive: true, force: true });
|
|
27
|
+
|
|
28
|
+
if (process.platform === 'win32') {
|
|
29
|
+
try {
|
|
30
|
+
execSync(`robocopy "${API_DIR}" "${API_BACKUP_DIR}" /E /MOVE /NFL /NDL /NJH /NJS`, { stdio: 'ignore' });
|
|
31
|
+
} catch (e) {
|
|
32
|
+
if (fs.existsSync(API_DIR)) throw e;
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
fs.renameSync(API_DIR, API_BACKUP_DIR);
|
|
36
|
+
}
|
|
37
|
+
console.log('ā
API routes hidden.');
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.warn(`ā ļø Warning: Could not hide API routes (${e.message}).`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function showApi() {
|
|
45
|
+
if (fs.existsSync(API_BACKUP_DIR)) {
|
|
46
|
+
console.log('šµ Restoring API routes...');
|
|
47
|
+
try {
|
|
48
|
+
if (process.platform === 'win32') {
|
|
49
|
+
try {
|
|
50
|
+
execSync(`robocopy "${API_BACKUP_DIR}" "${API_DIR}" /E /MOVE /NFL /NDL /NJH /NJS`, { stdio: 'ignore' });
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (fs.existsSync(API_BACKUP_DIR)) throw e;
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
if (fs.existsSync(API_DIR)) fs.rmSync(API_DIR, { recursive: true, force: true });
|
|
56
|
+
fs.renameSync(API_BACKUP_DIR, API_DIR);
|
|
57
|
+
}
|
|
58
|
+
console.log('ā
API routes restored.');
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error(`ā Error: Failed to restore API routes: ${e.message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const envContent = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : '';
|
|
66
|
+
const githubPat = envContent.match(/GITHUB_DEV_PAT=(.*)/)?.[1]?.trim().replace(/^"|"$/g, '') || '';
|
|
67
|
+
const gitlabPat = envContent.match(/GITLAB_DEV_PAT=(.*)/)?.[1]?.trim().replace(/^"|"$/g, '') || '';
|
|
68
|
+
|
|
69
|
+
const MAX_OTA_SIZE_MB = 50;
|
|
70
|
+
|
|
71
|
+
function getDirHash(dirPath) {
|
|
72
|
+
if (!fs.existsSync(dirPath)) return '';
|
|
73
|
+
const hash = crypto.createHash('sha256');
|
|
74
|
+
const files = fs.readdirSync(dirPath, { recursive: true });
|
|
75
|
+
files.sort().forEach(file => {
|
|
76
|
+
const fullPath = path.join(dirPath, file);
|
|
77
|
+
const stats = fs.statSync(fullPath);
|
|
78
|
+
if (stats.isFile()) {
|
|
79
|
+
hash.update(file);
|
|
80
|
+
hash.update(fs.readFileSync(fullPath));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return hash.digest('hex');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getDirSize(dirPath) {
|
|
87
|
+
let size = 0;
|
|
88
|
+
if (!fs.existsSync(dirPath)) return 0;
|
|
89
|
+
const files = fs.readdirSync(dirPath);
|
|
90
|
+
for (let i = 0; i < files.length; i++) {
|
|
91
|
+
const filePath = path.join(dirPath, files[i]);
|
|
92
|
+
const stats = fs.statSync(filePath);
|
|
93
|
+
if (stats.isFile()) {
|
|
94
|
+
size += stats.size;
|
|
95
|
+
} else if (stats.isDirectory()) {
|
|
96
|
+
size += getDirSize(filePath);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return size;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getRawBaseUrl(repoUrl, strategy, branch = 'main') {
|
|
103
|
+
let base = repoUrl.replace(/\/$/, '');
|
|
104
|
+
if (strategy === 'github') {
|
|
105
|
+
return base.replace('github.com', 'raw.githubusercontent.com') + `/${branch}`;
|
|
106
|
+
} else if (strategy === 'gitlab') {
|
|
107
|
+
return base + `/-/raw/${branch}`;
|
|
108
|
+
}
|
|
109
|
+
return base;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function deployOTA() {
|
|
113
|
+
console.log(`š Starting OTA Deployment (Strategy: ${OTA_CONFIG.strategy})...`);
|
|
114
|
+
|
|
115
|
+
const argChannel = process.argv[2] || 'training';
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
if (!fs.existsSync(OTA_RELEASES_DIR)) fs.mkdirSync(OTA_RELEASES_DIR);
|
|
119
|
+
|
|
120
|
+
const config = OTA_CONFIG[OTA_CONFIG.strategy];
|
|
121
|
+
if (!config || !config.repo) {
|
|
122
|
+
throw new Error(`Repository not configured for strategy "${OTA_CONFIG.strategy}". Run 'npx ota-updates register ${OTA_CONFIG.strategy}' first.`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const channelConfig = config.channels?.[argChannel];
|
|
126
|
+
const activeBranch = channelConfig?.branch || config.branch || 'main';
|
|
127
|
+
|
|
128
|
+
console.log(`š Preparing ${OTA_CONFIG.strategy} OTA Repository (Branch: ${activeBranch})...`);
|
|
129
|
+
if (fs.existsSync(OTA_RELEASES_DIR)) {
|
|
130
|
+
fs.rmSync(OTA_RELEASES_DIR, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pat = OTA_CONFIG.strategy === 'gitlab' ? gitlabPat : githubPat;
|
|
134
|
+
|
|
135
|
+
const cloneRepo = config.repo.endsWith('.git') ? config.repo : config.repo + '.git';
|
|
136
|
+
const authRepo = cloneRepo.replace('https://', `https://${pat}@`);
|
|
137
|
+
|
|
138
|
+
execSync(`git clone --branch ${activeBranch} ${authRepo} "${OTA_RELEASES_DIR}"`, { stdio: 'inherit' });
|
|
139
|
+
|
|
140
|
+
if (!pat) {
|
|
141
|
+
throw new Error(`Developer PAT for ${OTA_CONFIG.strategy.toUpperCase()} is missing in .env! Run 'npx ota-updates register ${OTA_CONFIG.strategy}' to fix it.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const currentEnv = fs.readFileSync(ENV_PATH, 'utf-8');
|
|
145
|
+
const versionMatch = currentEnv.match(/PUBLIC_APP_VERSION_ANDROID=([0-9.]+)/);
|
|
146
|
+
const currentVersion = versionMatch ? versionMatch[1] : '0.1.9.0';
|
|
147
|
+
const otaManifestPath = path.join(OTA_RELEASES_DIR, argChannel === 'training' ? 'manifest-training.json' : 'manifest.json');
|
|
148
|
+
|
|
149
|
+
let previousHash = null;
|
|
150
|
+
let previousVersion = currentVersion;
|
|
151
|
+
if (fs.existsSync(otaManifestPath)) {
|
|
152
|
+
const otaManifest = JSON.parse(fs.readFileSync(otaManifestPath, 'utf8'));
|
|
153
|
+
previousVersion = otaManifest.version || currentVersion;
|
|
154
|
+
previousHash = otaManifest.hash || null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log('šļø Building project for OTA Diff Check...');
|
|
158
|
+
hideApi();
|
|
159
|
+
try {
|
|
160
|
+
execSync('npm run build', { stdio: 'inherit', cwd: rootDir });
|
|
161
|
+
|
|
162
|
+
if (fs.existsSync(FLATTEN_SCRIPT)) {
|
|
163
|
+
console.log('š Running Tsar Bomba Path Cleanse...');
|
|
164
|
+
execSync(`node "${FLATTEN_SCRIPT}"`, { stdio: 'inherit', cwd: rootDir });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (fs.existsSync(VERIFY_SCRIPT)) {
|
|
168
|
+
console.log('š Running Pre-Flight Verification...');
|
|
169
|
+
execSync(`node "${VERIFY_SCRIPT}"`, { stdio: 'inherit', cwd: rootDir });
|
|
170
|
+
}
|
|
171
|
+
} finally {
|
|
172
|
+
showApi();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log('š§ Calculating SHA-256 Hash of dist/ artifacts...');
|
|
176
|
+
const currentBuildHash = getDirHash(DIST_DIR);
|
|
177
|
+
console.log(`š Current Build Hash : ${currentBuildHash.substring(0, 16)}...`);
|
|
178
|
+
if (previousHash) {
|
|
179
|
+
console.log(`š Previous Release Hash: ${previousHash.substring(0, 16)}...`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (previousHash && previousHash === currentBuildHash) {
|
|
183
|
+
console.log(`\nā¹ļø [SMART DIFF CHECKER] No changes detected in build artifacts.`);
|
|
184
|
+
console.log(`ā
Deploy Success (Skipped remote push to save bandwidth & Git storage).`);
|
|
185
|
+
console.log(`š Active Version: ${previousVersion} (Unchanged)`);
|
|
186
|
+
console.log(`š Channel: ${argChannel}\n`);
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let nextVersion = process.argv[3];
|
|
191
|
+
if (!nextVersion) {
|
|
192
|
+
console.log('š Changes detected! Auto-incrementing from remote manifest...');
|
|
193
|
+
const parts = previousVersion.split('.');
|
|
194
|
+
const lastIdx = parts.length - 1;
|
|
195
|
+
parts[lastIdx] = (parseInt(parts[lastIdx]) + 1).toString();
|
|
196
|
+
nextVersion = parts.join('.');
|
|
197
|
+
console.log(`š Auto-increment: ${previousVersion} -> ${nextVersion}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(`š¦ Target Version: ${nextVersion} [${argChannel}]`);
|
|
201
|
+
|
|
202
|
+
const rawBaseUrl = getRawBaseUrl(config.repo, OTA_CONFIG.strategy, activeBranch);
|
|
203
|
+
const manifestFileName = argChannel === 'training' ? 'manifest-training.json' : 'manifest.json';
|
|
204
|
+
const activeOtaUrl = `${rawBaseUrl}/${manifestFileName}`;
|
|
205
|
+
|
|
206
|
+
console.log(`š Auto-Constructing Raw OTA URL: ${activeOtaUrl}`);
|
|
207
|
+
|
|
208
|
+
let updatedEnv = currentEnv;
|
|
209
|
+
updatedEnv = updatedEnv.replace(/PUBLIC_APP_VERSION_ANDROID=.*/, `PUBLIC_APP_VERSION_ANDROID=${nextVersion}`);
|
|
210
|
+
updatedEnv = updatedEnv.replace(/PUBLIC_APP_VERSION_IOS=.*/, `PUBLIC_APP_VERSION_IOS=${nextVersion}`);
|
|
211
|
+
|
|
212
|
+
if (updatedEnv.includes('PUBLIC_APP_VERSION=')) {
|
|
213
|
+
updatedEnv = updatedEnv.replace(/PUBLIC_APP_VERSION=.*/, `PUBLIC_APP_VERSION=${nextVersion}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (updatedEnv.includes('PUBLIC_OTA_UPDATE_URL=')) {
|
|
217
|
+
updatedEnv = updatedEnv.replace(/PUBLIC_OTA_UPDATE_URL=.*/, `PUBLIC_OTA_UPDATE_URL=${activeOtaUrl}`);
|
|
218
|
+
} else {
|
|
219
|
+
updatedEnv += `\nPUBLIC_OTA_UPDATE_URL=${activeOtaUrl}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log(`š Updating .env to version ${nextVersion} (Android & iOS) and setting OTA target...`);
|
|
223
|
+
fs.writeFileSync(ENV_PATH, updatedEnv);
|
|
224
|
+
|
|
225
|
+
if (fs.existsSync(MAIN_MANIFEST_PATH)) {
|
|
226
|
+
const manifest = JSON.parse(fs.readFileSync(MAIN_MANIFEST_PATH, 'utf8'));
|
|
227
|
+
manifest.version = nextVersion;
|
|
228
|
+
fs.writeFileSync(MAIN_MANIFEST_PATH, JSON.stringify(manifest, null, 2));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.log('š”ļø Size Guardian: Checking dist/ folder size...');
|
|
232
|
+
const distSizeBytes = getDirSize(DIST_DIR);
|
|
233
|
+
const distSizeMB = distSizeBytes / (1024 * 1024);
|
|
234
|
+
console.log(`š Estimated Size: ${distSizeMB.toFixed(2)} MB`);
|
|
235
|
+
|
|
236
|
+
if (distSizeMB > MAX_OTA_SIZE_MB) {
|
|
237
|
+
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
|
+
}
|
|
239
|
+
|
|
240
|
+
const otaName = `v${nextVersion.replace(/\./g, '_')}.zip`;
|
|
241
|
+
const otaPath = path.join(rootDir, otaName);
|
|
242
|
+
|
|
243
|
+
const isWindows = process.platform === 'win32';
|
|
244
|
+
const tarCmd = isWindows ? 'tar.exe' : 'tar';
|
|
245
|
+
const zipCmd = `${tarCmd} -a -c -f "${otaPath}" -C "${DIST_DIR}" .`;
|
|
246
|
+
|
|
247
|
+
execSync(zipCmd, { stdio: 'inherit' });
|
|
248
|
+
|
|
249
|
+
console.log('š”ļø Verifying ZIP Integrity...');
|
|
250
|
+
try {
|
|
251
|
+
execSync(`${tarCmd} -t -f "${otaPath}"`, { stdio: 'ignore' });
|
|
252
|
+
console.log('ā
ZIP is valid and readable.');
|
|
253
|
+
} catch (e) {
|
|
254
|
+
throw new Error('CRITICAL: Generated ZIP is corrupt or invalid!');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const zipStats = fs.statSync(otaPath);
|
|
258
|
+
const zipSizeMB = zipStats.size / (1024 * 1024);
|
|
259
|
+
console.log(`š Final ZIP Size: ${zipSizeMB.toFixed(2)} MB`);
|
|
260
|
+
|
|
261
|
+
if (zipSizeMB > MAX_OTA_SIZE_MB) {
|
|
262
|
+
if (fs.existsSync(otaPath)) fs.unlinkSync(otaPath);
|
|
263
|
+
throw new Error(`CRITICAL SIZE VIOLATION: ZIP file bloated to ${zipSizeMB.toFixed(2)} MB! Deployment aborted!`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await deployToRemote(otaPath, otaName, nextVersion, argChannel, activeBranch, currentBuildHash);
|
|
267
|
+
|
|
268
|
+
console.log(`\nā
OTA DEPLOY SUCCESS!`);
|
|
269
|
+
console.log(`š Version: ${nextVersion}`);
|
|
270
|
+
console.log(`š Channel: ${argChannel}\n`);
|
|
271
|
+
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error(`\nā Deployment Failed: ${error.message}\n`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
} finally {
|
|
276
|
+
showApi();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function deployToRemote(otaPath, otaName, version, channel, branch, buildHash) {
|
|
281
|
+
const config = OTA_CONFIG[OTA_CONFIG.strategy];
|
|
282
|
+
const pat = OTA_CONFIG.strategy === 'gitlab' ? gitlabPat : githubPat;
|
|
283
|
+
console.log(`š Pushing to ${OTA_CONFIG.strategy} (Branch: ${branch})...`);
|
|
284
|
+
|
|
285
|
+
const targetPath = path.join(OTA_RELEASES_DIR, otaName);
|
|
286
|
+
fs.copyFileSync(otaPath, targetPath);
|
|
287
|
+
|
|
288
|
+
const manifestPath = path.join(OTA_RELEASES_DIR, 'manifest.json');
|
|
289
|
+
let manifest = { live: {}, training: {} };
|
|
290
|
+
if (fs.existsSync(manifestPath)) {
|
|
291
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const rawBaseUrl = getRawBaseUrl(config.repo, OTA_CONFIG.strategy, branch);
|
|
295
|
+
manifest[channel] = {
|
|
296
|
+
version,
|
|
297
|
+
url: `${rawBaseUrl}/${otaName}`,
|
|
298
|
+
date: new Date().toISOString(),
|
|
299
|
+
hash: buildHash || '',
|
|
300
|
+
note: `Update to ${version} (${channel})`
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
304
|
+
|
|
305
|
+
const flatManifestPath = path.join(OTA_RELEASES_DIR, `manifest-${channel}.json`);
|
|
306
|
+
fs.writeFileSync(flatManifestPath, JSON.stringify(manifest[channel], null, 2));
|
|
307
|
+
|
|
308
|
+
execSync(`git add .`, { cwd: OTA_RELEASES_DIR });
|
|
309
|
+
execSync(`git commit -m "release: v${version} for ${channel}"`, { cwd: OTA_RELEASES_DIR });
|
|
310
|
+
execSync(`git push origin ${branch}`, { cwd: OTA_RELEASES_DIR });
|
|
311
|
+
|
|
312
|
+
if (fs.existsSync(otaPath)) fs.unlinkSync(otaPath);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
deployOTA();
|
package/package.json
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ota-manager",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Multi-provider OTA update manager for Astro and static web projects.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"ota-manager": "bin/cli.js",
|
|
9
|
-
"ota-updates": "bin/cli.js"
|
|
10
|
-
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
-
},
|
|
14
|
-
"keywords": [
|
|
15
|
-
"astro",
|
|
16
|
-
"ota",
|
|
17
|
-
"deployment",
|
|
18
|
-
"github",
|
|
19
|
-
"gitlab",
|
|
20
|
-
"automation"
|
|
21
|
-
],
|
|
22
|
-
"author": {
|
|
23
|
-
"name": "First Ryan",
|
|
24
|
-
"email": "firstryan@gmail.com",
|
|
25
|
-
"url": "https://github.com/firstryan"
|
|
26
|
-
},
|
|
27
|
-
"license": "MIT",
|
|
28
|
-
"repository": {
|
|
29
|
-
"type": "git",
|
|
30
|
-
"url": "git+https://github.com/firstryan/ota-manager.git"
|
|
31
|
-
},
|
|
32
|
-
"bugs": {
|
|
33
|
-
"url": "https://github.com/firstryan/ota-manager/issues"
|
|
34
|
-
},
|
|
35
|
-
"homepage": "https://github.com/firstryan/ota-manager#readme",
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"archiver": "^8.0.0",
|
|
38
|
-
"ota-manager": "^1.0.1"
|
|
39
|
-
},
|
|
40
|
-
"engines": {
|
|
41
|
-
"node": ">=18.0.0"
|
|
42
|
-
}
|
|
43
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "ota-manager",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "Multi-provider OTA update manager for Astro and static web projects.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ota-manager": "bin/cli.js",
|
|
9
|
+
"ota-updates": "bin/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"astro",
|
|
16
|
+
"ota",
|
|
17
|
+
"deployment",
|
|
18
|
+
"github",
|
|
19
|
+
"gitlab",
|
|
20
|
+
"automation"
|
|
21
|
+
],
|
|
22
|
+
"author": {
|
|
23
|
+
"name": "First Ryan",
|
|
24
|
+
"email": "firstryan@gmail.com",
|
|
25
|
+
"url": "https://github.com/firstryan"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/firstryan/ota-manager.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/firstryan/ota-manager/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/firstryan/ota-manager#readme",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"archiver": "^8.0.0",
|
|
38
|
+
"ota-manager": "^1.0.1"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|