flightdeck 0.0.2 → 0.0.4
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/dist/flightdeck.$configPath.schema.json +27 -15
- package/dist/flightdeck.bin.js +375 -0
- package/dist/flightdeck.main.schema.json +27 -15
- package/dist/klaxon.bin.js +87 -0
- package/dist/lib.d.ts +102 -25
- package/dist/lib.js +215 -76
- package/package.json +10 -8
- package/src/{bin.ts → flightdeck.bin.ts} +24 -28
- package/src/flightdeck.lib.ts +363 -0
- package/src/klaxon.bin.ts +58 -0
- package/src/klaxon.lib.ts +78 -0
- package/src/lib.ts +4 -1
- package/dist/bin.js +0 -102
- package/src/flightdeck.ts +0 -235
|
@@ -4,22 +4,35 @@
|
|
|
4
4
|
"secret": {
|
|
5
5
|
"type": "string"
|
|
6
6
|
},
|
|
7
|
-
"
|
|
7
|
+
"packageName": {
|
|
8
8
|
"type": "string"
|
|
9
9
|
},
|
|
10
|
-
"
|
|
11
|
-
"type": "
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
"services": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"additionalProperties": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"properties": {
|
|
15
|
+
"run": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "string"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"waitFor": {
|
|
22
|
+
"type": "boolean"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"required": [
|
|
26
|
+
"run",
|
|
27
|
+
"waitFor"
|
|
28
|
+
],
|
|
29
|
+
"additionalProperties": false
|
|
17
30
|
}
|
|
18
31
|
},
|
|
19
|
-
"
|
|
32
|
+
"flightdeckRootDir": {
|
|
20
33
|
"type": "string"
|
|
21
34
|
},
|
|
22
|
-
"
|
|
35
|
+
"downloadPackageToUpdatesCmd": {
|
|
23
36
|
"type": "array",
|
|
24
37
|
"items": {
|
|
25
38
|
"type": "string"
|
|
@@ -28,11 +41,10 @@
|
|
|
28
41
|
},
|
|
29
42
|
"required": [
|
|
30
43
|
"secret",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"updateCmd"
|
|
44
|
+
"packageName",
|
|
45
|
+
"services",
|
|
46
|
+
"flightdeckRootDir",
|
|
47
|
+
"downloadPackageToUpdatesCmd"
|
|
36
48
|
],
|
|
37
49
|
"additionalProperties": false,
|
|
38
50
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/flightdeck.bin.ts
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import {cli, optional, parseArrayOption} from "comline";
|
|
6
|
+
import {z} from "zod";
|
|
7
|
+
|
|
8
|
+
// src/flightdeck.lib.ts
|
|
9
|
+
import {execSync, spawn} from "node:child_process";
|
|
10
|
+
import {existsSync, mkdirSync, renameSync, rmSync} from "node:fs";
|
|
11
|
+
import {createServer} from "node:http";
|
|
12
|
+
import {homedir} from "node:os";
|
|
13
|
+
import {resolve} from "node:path";
|
|
14
|
+
import {Future} from "atom.io/internal";
|
|
15
|
+
import {fromEntries, toEntries} from "atom.io/json";
|
|
16
|
+
import {ChildSocket} from "atom.io/realtime-server";
|
|
17
|
+
var PORT = process.env.PORT ?? 8080;
|
|
18
|
+
var ORIGIN = `http://localhost:${PORT}`;
|
|
19
|
+
|
|
20
|
+
class FlightDeck {
|
|
21
|
+
options;
|
|
22
|
+
safety = 0;
|
|
23
|
+
webhookServer;
|
|
24
|
+
services;
|
|
25
|
+
serviceIdx;
|
|
26
|
+
defaultServicesReadyToUpdate;
|
|
27
|
+
servicesReadyToUpdate;
|
|
28
|
+
servicesShouldRestart;
|
|
29
|
+
logger;
|
|
30
|
+
serviceLoggers;
|
|
31
|
+
servicesLive;
|
|
32
|
+
servicesDead;
|
|
33
|
+
live = new Future(() => {
|
|
34
|
+
});
|
|
35
|
+
dead = new Future(() => {
|
|
36
|
+
});
|
|
37
|
+
restartTimes = [];
|
|
38
|
+
currentServiceDir;
|
|
39
|
+
updateServiceDir;
|
|
40
|
+
backupServiceDir;
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.options = options;
|
|
43
|
+
const { secret, flightdeckRootDir = resolve(homedir(), `services`) } = options;
|
|
44
|
+
const servicesEntries = toEntries(options.services);
|
|
45
|
+
this.services = fromEntries(servicesEntries.map(([serviceName]) => [serviceName, null]));
|
|
46
|
+
this.serviceIdx = fromEntries(servicesEntries.map(([serviceName], idx) => [serviceName, idx]));
|
|
47
|
+
this.defaultServicesReadyToUpdate = fromEntries(servicesEntries.map(([serviceName, { waitFor }]) => [
|
|
48
|
+
serviceName,
|
|
49
|
+
!waitFor
|
|
50
|
+
]));
|
|
51
|
+
this.servicesReadyToUpdate = { ...this.defaultServicesReadyToUpdate };
|
|
52
|
+
this.servicesShouldRestart = true;
|
|
53
|
+
this.logger = {
|
|
54
|
+
info: (...args) => {
|
|
55
|
+
console.log(`${this.options.packageName}:`, ...args);
|
|
56
|
+
},
|
|
57
|
+
warn: (...args) => {
|
|
58
|
+
console.warn(`${this.options.packageName}:`, ...args);
|
|
59
|
+
},
|
|
60
|
+
error: (...args) => {
|
|
61
|
+
console.error(`${this.options.packageName}:`, ...args);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
this.serviceLoggers = fromEntries(servicesEntries.map(([serviceName]) => [
|
|
65
|
+
serviceName,
|
|
66
|
+
{
|
|
67
|
+
info: (...args) => {
|
|
68
|
+
console.log(`${this.options.packageName}::${serviceName}:`, ...args);
|
|
69
|
+
},
|
|
70
|
+
warn: (...args) => {
|
|
71
|
+
console.warn(`${this.options.packageName}::${serviceName}:`, ...args);
|
|
72
|
+
},
|
|
73
|
+
error: (...args) => {
|
|
74
|
+
console.error(`${this.options.packageName}::${serviceName}:`, ...args);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
]));
|
|
78
|
+
this.servicesLive = servicesEntries.map(() => new Future(() => {
|
|
79
|
+
}));
|
|
80
|
+
this.servicesDead = servicesEntries.map(() => new Future(() => {
|
|
81
|
+
}));
|
|
82
|
+
this.live.use(Promise.all(this.servicesLive));
|
|
83
|
+
this.dead.use(Promise.all(this.servicesDead));
|
|
84
|
+
this.currentServiceDir = resolve(flightdeckRootDir, options.packageName, `current`);
|
|
85
|
+
this.backupServiceDir = resolve(flightdeckRootDir, options.packageName, `backup`);
|
|
86
|
+
this.updateServiceDir = resolve(flightdeckRootDir, options.packageName, `update`);
|
|
87
|
+
createServer((req, res) => {
|
|
88
|
+
let data = [];
|
|
89
|
+
req.on(`data`, (chunk) => {
|
|
90
|
+
data.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk));
|
|
91
|
+
}).on(`end`, () => {
|
|
92
|
+
const authHeader = req.headers.authorization;
|
|
93
|
+
try {
|
|
94
|
+
if (typeof req.url === `undefined`)
|
|
95
|
+
throw 400;
|
|
96
|
+
if (authHeader !== `Bearer ${secret}`)
|
|
97
|
+
throw 401;
|
|
98
|
+
const url = new URL(req.url, ORIGIN);
|
|
99
|
+
this.logger.info(req.method, url.pathname);
|
|
100
|
+
switch (req.method) {
|
|
101
|
+
case `POST`:
|
|
102
|
+
{
|
|
103
|
+
switch (url.pathname) {
|
|
104
|
+
case `/`:
|
|
105
|
+
{
|
|
106
|
+
res.writeHead(200);
|
|
107
|
+
res.end();
|
|
108
|
+
this.getLatestRelease();
|
|
109
|
+
if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
|
|
110
|
+
this.logger.info(`All services are ready to update!`);
|
|
111
|
+
this.stopAllServices();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const entry of toEntries(this.services)) {
|
|
115
|
+
const [serviceName, service] = entry;
|
|
116
|
+
if (service) {
|
|
117
|
+
if (this.options.services[serviceName].waitFor) {
|
|
118
|
+
service.emit(`updatesReady`);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
this.startService(serviceName);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
throw 404;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
default:
|
|
132
|
+
throw 405;
|
|
133
|
+
}
|
|
134
|
+
} catch (thrown) {
|
|
135
|
+
this.logger.error(thrown, req.url);
|
|
136
|
+
if (typeof thrown === `number`) {
|
|
137
|
+
res.writeHead(thrown);
|
|
138
|
+
res.end();
|
|
139
|
+
}
|
|
140
|
+
} finally {
|
|
141
|
+
data = [];
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}).listen(PORT, () => {
|
|
145
|
+
this.logger.info(`Server started on port ${PORT}`);
|
|
146
|
+
});
|
|
147
|
+
this.startAllServices();
|
|
148
|
+
}
|
|
149
|
+
startAllServices() {
|
|
150
|
+
this.logger.info(`Starting all services...`);
|
|
151
|
+
for (const [serviceName] of toEntries(this.services)) {
|
|
152
|
+
this.startService(serviceName);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
startService(serviceName) {
|
|
156
|
+
this.logger.info(`Starting service ${this.options.packageName}::${serviceName}, try ${this.safety}/2...`);
|
|
157
|
+
if (this.safety >= 2) {
|
|
158
|
+
throw new Error(`Out of tries...`);
|
|
159
|
+
}
|
|
160
|
+
this.safety++;
|
|
161
|
+
if (!existsSync(this.currentServiceDir)) {
|
|
162
|
+
this.logger.info(`Tried to start service but failed: could not find ${this.currentServiceDir}`);
|
|
163
|
+
this.getLatestRelease();
|
|
164
|
+
this.applyUpdate();
|
|
165
|
+
this.startService(serviceName);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const [executable, ...args] = this.options.services[serviceName].run;
|
|
169
|
+
const program = executable.startsWith(`./`) ? resolve(this.currentServiceDir, executable) : executable;
|
|
170
|
+
const serviceProcess = spawn(program, args, {
|
|
171
|
+
cwd: this.currentServiceDir,
|
|
172
|
+
env: import.meta.env
|
|
173
|
+
});
|
|
174
|
+
this.services[serviceName] = new ChildSocket(serviceProcess, `${this.options.packageName}::${serviceName}`, console);
|
|
175
|
+
this.services[serviceName].onAny((...messages) => {
|
|
176
|
+
this.logger.info(`\uD83D\uDCAC`, ...messages);
|
|
177
|
+
});
|
|
178
|
+
this.services[serviceName].on(`readyToUpdate`, () => {
|
|
179
|
+
this.serviceLoggers[serviceName].info(`Ready to update.`);
|
|
180
|
+
this.servicesReadyToUpdate[serviceName] = true;
|
|
181
|
+
if (toEntries(this.servicesReadyToUpdate).every(([, isReady]) => isReady)) {
|
|
182
|
+
this.logger.info(`All services are ready to update.`);
|
|
183
|
+
this.stopAllServices();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
this.services[serviceName].on(`alive`, () => {
|
|
187
|
+
this.servicesLive[this.serviceIdx[serviceName]].use(Promise.resolve());
|
|
188
|
+
this.servicesDead[this.serviceIdx[serviceName]] = new Future(() => {
|
|
189
|
+
});
|
|
190
|
+
if (this.dead.done) {
|
|
191
|
+
this.dead = new Future(() => {
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
this.dead.use(Promise.all(this.servicesDead));
|
|
195
|
+
});
|
|
196
|
+
this.services[serviceName].process.on(`close`, (exitCode) => {
|
|
197
|
+
this.serviceLoggers[serviceName].info(`Exited with code ${exitCode}`);
|
|
198
|
+
this.services[serviceName] = null;
|
|
199
|
+
if (!this.servicesShouldRestart) {
|
|
200
|
+
this.serviceLoggers[serviceName].info(`Will not be restarted.`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const updatesAreReady = existsSync(this.updateServiceDir);
|
|
204
|
+
if (updatesAreReady) {
|
|
205
|
+
this.serviceLoggers[serviceName].info(`Updating before startup...`);
|
|
206
|
+
this.restartTimes = [];
|
|
207
|
+
this.applyUpdate();
|
|
208
|
+
this.startService(serviceName);
|
|
209
|
+
} else {
|
|
210
|
+
const now = Date.now();
|
|
211
|
+
const fiveMinutesAgo = now - 5 * 60 * 1000;
|
|
212
|
+
this.restartTimes = this.restartTimes.filter((time) => time > fiveMinutesAgo);
|
|
213
|
+
this.restartTimes.push(now);
|
|
214
|
+
if (this.restartTimes.length < 5) {
|
|
215
|
+
this.serviceLoggers[serviceName].info(`Crashed. Restarting...`);
|
|
216
|
+
this.startService(serviceName);
|
|
217
|
+
} else {
|
|
218
|
+
this.serviceLoggers[serviceName].info(`Crashed 5 times in 5 minutes. Not restarting.`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
this.safety = 0;
|
|
223
|
+
}
|
|
224
|
+
applyUpdate() {
|
|
225
|
+
this.logger.info(`Applying update...`);
|
|
226
|
+
if (existsSync(this.updateServiceDir)) {
|
|
227
|
+
const runningServices = toEntries(this.services).filter(([, service]) => service);
|
|
228
|
+
if (runningServices.length > 0) {
|
|
229
|
+
this.logger.error(`Tried to apply update but failed. The following services are currently running: [${runningServices.map(([serviceName]) => serviceName).join(`, `)}]`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (existsSync(this.currentServiceDir)) {
|
|
233
|
+
if (!existsSync(this.backupServiceDir)) {
|
|
234
|
+
mkdirSync(this.backupServiceDir, { recursive: true });
|
|
235
|
+
} else {
|
|
236
|
+
rmSync(this.backupServiceDir, { recursive: true });
|
|
237
|
+
}
|
|
238
|
+
renameSync(this.currentServiceDir, this.backupServiceDir);
|
|
239
|
+
}
|
|
240
|
+
renameSync(this.updateServiceDir, this.currentServiceDir);
|
|
241
|
+
this.restartTimes = [];
|
|
242
|
+
this.servicesReadyToUpdate = { ...this.defaultServicesReadyToUpdate };
|
|
243
|
+
} else {
|
|
244
|
+
this.logger.error(`Tried to apply update but failed: could not find update directory ${this.updateServiceDir}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
getLatestRelease() {
|
|
248
|
+
this.logger.info(`Getting latest release...`);
|
|
249
|
+
try {
|
|
250
|
+
execSync(this.options.downloadPackageToUpdatesCmd.join(` `));
|
|
251
|
+
} catch (thrown) {
|
|
252
|
+
if (thrown instanceof Error) {
|
|
253
|
+
this.logger.error(`Failed to get the latest release: ${thrown.message}`);
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
stopAllServices() {
|
|
259
|
+
this.logger.info(`Stopping all services...`);
|
|
260
|
+
for (const [serviceName] of toEntries(this.services)) {
|
|
261
|
+
this.stopService(serviceName);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
stopService(serviceName) {
|
|
265
|
+
if (this.services[serviceName]) {
|
|
266
|
+
this.serviceLoggers[serviceName].info(`Stopping service...`);
|
|
267
|
+
this.services[serviceName].process.kill();
|
|
268
|
+
this.services[serviceName] = null;
|
|
269
|
+
this.servicesDead[this.serviceIdx[serviceName]].use(Promise.resolve());
|
|
270
|
+
this.servicesLive[this.serviceIdx[serviceName]] = new Future(() => {
|
|
271
|
+
});
|
|
272
|
+
if (this.live.done) {
|
|
273
|
+
this.live = new Future(() => {
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
this.live.use(Promise.all(this.servicesLive));
|
|
277
|
+
} else {
|
|
278
|
+
this.serviceLoggers[serviceName].error(`Tried to stop service, but it wasn't running.`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
shutdown() {
|
|
282
|
+
this.servicesShouldRestart = false;
|
|
283
|
+
this.stopAllServices();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/flightdeck.bin.ts
|
|
288
|
+
var FLIGHTDECK_MANUAL = {
|
|
289
|
+
optionsSchema: z.object({
|
|
290
|
+
secret: z.string(),
|
|
291
|
+
packageName: z.string(),
|
|
292
|
+
services: z.record(z.object({ run: z.array(z.string()), waitFor: z.boolean() })),
|
|
293
|
+
flightdeckRootDir: z.string(),
|
|
294
|
+
downloadPackageToUpdatesCmd: z.array(z.string())
|
|
295
|
+
}),
|
|
296
|
+
options: {
|
|
297
|
+
secret: {
|
|
298
|
+
flag: `x`,
|
|
299
|
+
required: true,
|
|
300
|
+
description: `Secret used to authenticate with the service.`,
|
|
301
|
+
example: `--secret=\"secret\"`
|
|
302
|
+
},
|
|
303
|
+
packageName: {
|
|
304
|
+
flag: `p`,
|
|
305
|
+
required: true,
|
|
306
|
+
description: `Name of the package.`,
|
|
307
|
+
example: `--packageName=\"my-app\"`
|
|
308
|
+
},
|
|
309
|
+
services: {
|
|
310
|
+
flag: `s`,
|
|
311
|
+
required: true,
|
|
312
|
+
description: `Map of service names to executables.`,
|
|
313
|
+
example: `--services="{\\"frontend\\":{\\"run\\":[\\"./app\\"],\\"waitFor\\":false},\\"backend\\":{\\"run\\":[\\"./backend\\"],\\"waitFor\\":true}}"`,
|
|
314
|
+
parse: JSON.parse
|
|
315
|
+
},
|
|
316
|
+
flightdeckRootDir: {
|
|
317
|
+
flag: `d`,
|
|
318
|
+
required: true,
|
|
319
|
+
description: `Directory where the service is stored.`,
|
|
320
|
+
example: `--flightdeckRootDir=\"./services/sample/repo/my-app/current\"`
|
|
321
|
+
},
|
|
322
|
+
downloadPackageToUpdatesCmd: {
|
|
323
|
+
flag: `u`,
|
|
324
|
+
required: true,
|
|
325
|
+
description: `Command to update the service.`,
|
|
326
|
+
example: `--downloadPackageToUpdatesCmd=\"./app\"`,
|
|
327
|
+
parse: parseArrayOption
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
var SCHEMA_MANUAL = {
|
|
332
|
+
optionsSchema: z.object({
|
|
333
|
+
outdir: z.string().optional()
|
|
334
|
+
}),
|
|
335
|
+
options: {
|
|
336
|
+
outdir: {
|
|
337
|
+
flag: `o`,
|
|
338
|
+
required: false,
|
|
339
|
+
description: `Directory to write the schema to.`,
|
|
340
|
+
example: `--outdir=./dist`
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
var parse = cli({
|
|
345
|
+
cliName: `flightdeck`,
|
|
346
|
+
routes: optional({ schema: null, $configPath: null }),
|
|
347
|
+
routeOptions: {
|
|
348
|
+
"": FLIGHTDECK_MANUAL,
|
|
349
|
+
$configPath: FLIGHTDECK_MANUAL,
|
|
350
|
+
schema: SCHEMA_MANUAL
|
|
351
|
+
},
|
|
352
|
+
discoverConfigPath: (args) => {
|
|
353
|
+
if (args[0] === `schema`) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const configPath = args[0] ?? path.join(process.cwd(), `flightdeck.config.json`);
|
|
357
|
+
return configPath;
|
|
358
|
+
}
|
|
359
|
+
}, console);
|
|
360
|
+
var { inputs, writeJsonSchema } = parse(process.argv);
|
|
361
|
+
switch (inputs.case) {
|
|
362
|
+
case `schema`:
|
|
363
|
+
{
|
|
364
|
+
const { outdir } = inputs.opts;
|
|
365
|
+
writeJsonSchema(outdir ?? `.`);
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
default: {
|
|
369
|
+
const flightDeck = new FlightDeck(inputs.opts);
|
|
370
|
+
process.on(`close`, async () => {
|
|
371
|
+
flightDeck.stopAllServices();
|
|
372
|
+
await flightDeck.dead;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -4,22 +4,35 @@
|
|
|
4
4
|
"secret": {
|
|
5
5
|
"type": "string"
|
|
6
6
|
},
|
|
7
|
-
"
|
|
7
|
+
"packageName": {
|
|
8
8
|
"type": "string"
|
|
9
9
|
},
|
|
10
|
-
"
|
|
11
|
-
"type": "
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
"services": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"additionalProperties": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"properties": {
|
|
15
|
+
"run": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "string"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"waitFor": {
|
|
22
|
+
"type": "boolean"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"required": [
|
|
26
|
+
"run",
|
|
27
|
+
"waitFor"
|
|
28
|
+
],
|
|
29
|
+
"additionalProperties": false
|
|
17
30
|
}
|
|
18
31
|
},
|
|
19
|
-
"
|
|
32
|
+
"flightdeckRootDir": {
|
|
20
33
|
"type": "string"
|
|
21
34
|
},
|
|
22
|
-
"
|
|
35
|
+
"downloadPackageToUpdatesCmd": {
|
|
23
36
|
"type": "array",
|
|
24
37
|
"items": {
|
|
25
38
|
"type": "string"
|
|
@@ -28,11 +41,10 @@
|
|
|
28
41
|
},
|
|
29
42
|
"required": [
|
|
30
43
|
"secret",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"updateCmd"
|
|
44
|
+
"packageName",
|
|
45
|
+
"services",
|
|
46
|
+
"flightdeckRootDir",
|
|
47
|
+
"downloadPackageToUpdatesCmd"
|
|
36
48
|
],
|
|
37
49
|
"additionalProperties": false,
|
|
38
50
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/klaxon.bin.ts
|
|
4
|
+
import {cli, required} from "comline";
|
|
5
|
+
import {z} from "zod";
|
|
6
|
+
|
|
7
|
+
// src/klaxon.lib.ts
|
|
8
|
+
async function alert({
|
|
9
|
+
secret,
|
|
10
|
+
endpoint
|
|
11
|
+
}) {
|
|
12
|
+
const response = await fetch(endpoint, {
|
|
13
|
+
method: `POST`,
|
|
14
|
+
headers: {
|
|
15
|
+
"Content-Type": `application/json`,
|
|
16
|
+
Authorization: `Bearer ${secret}`
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return response;
|
|
20
|
+
}
|
|
21
|
+
async function scramble({
|
|
22
|
+
packageConfig,
|
|
23
|
+
secretsConfig,
|
|
24
|
+
publishedPackages
|
|
25
|
+
}) {
|
|
26
|
+
const alertResults = [];
|
|
27
|
+
for (const publishedPackage of publishedPackages) {
|
|
28
|
+
if (publishedPackage.name in packageConfig) {
|
|
29
|
+
const name = publishedPackage.name;
|
|
30
|
+
const { endpoint } = packageConfig[name];
|
|
31
|
+
const secret = secretsConfig[name];
|
|
32
|
+
const alertResultPromise = alert({ secret, endpoint }).then((alertResult) => [name, alertResult]);
|
|
33
|
+
alertResults.push(alertResultPromise);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const alertResultsResolved = await Promise.all(alertResults);
|
|
37
|
+
const scrambleResult = Object.fromEntries(alertResultsResolved);
|
|
38
|
+
return scrambleResult;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/klaxon.bin.ts
|
|
42
|
+
var changesetsPublishedPackagesSchema = z.object({
|
|
43
|
+
packageConfig: z.record(z.string(), z.object({ endpoint: z.string() })),
|
|
44
|
+
secretsConfig: z.record(z.string(), z.string()),
|
|
45
|
+
publishedPackages: z.array(z.object({
|
|
46
|
+
name: z.string(),
|
|
47
|
+
version: z.string()
|
|
48
|
+
}))
|
|
49
|
+
});
|
|
50
|
+
var klaxon = cli({
|
|
51
|
+
cliName: `klaxon`,
|
|
52
|
+
routes: required({
|
|
53
|
+
scramble: null
|
|
54
|
+
}),
|
|
55
|
+
routeOptions: {
|
|
56
|
+
scramble: {
|
|
57
|
+
options: {
|
|
58
|
+
packageConfig: {
|
|
59
|
+
description: `Maps the names of your packages to the endpoints that klaxon will POST to.`,
|
|
60
|
+
example: `--packageConfig="{\\"my-app\\":{\\"endpoint\\":\\"https://my-app.com\\"}}"`,
|
|
61
|
+
flag: `c`,
|
|
62
|
+
parse: JSON.parse,
|
|
63
|
+
required: true
|
|
64
|
+
},
|
|
65
|
+
secretsConfig: {
|
|
66
|
+
description: `Maps the names of your packages to the secrets that klaxon will use to authenticate with their respective endpoints.`,
|
|
67
|
+
example: `--secretsConfig="{\\"my-app\\":\\"XXXX-XXXX-XXXX\\"}"`,
|
|
68
|
+
flag: `s`,
|
|
69
|
+
parse: JSON.parse,
|
|
70
|
+
required: true
|
|
71
|
+
},
|
|
72
|
+
publishedPackages: {
|
|
73
|
+
description: `The output of the "Publish" step in Changesets.`,
|
|
74
|
+
example: `--publishedPackages="[{\\"name\\":\\"my-app\\",\\"version\\":\\"0.0.0\\"}]"`,
|
|
75
|
+
flag: `p`,
|
|
76
|
+
parse: JSON.parse,
|
|
77
|
+
required: true
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
optionsSchema: changesetsPublishedPackagesSchema
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
var { inputs } = klaxon(process.argv);
|
|
85
|
+
await scramble(inputs.opts).then((scrambleResult) => {
|
|
86
|
+
console.log(scrambleResult);
|
|
87
|
+
});
|