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