@marcusrbrown/infra 0.13.1 → 0.13.3
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/cli.js +11965 -0
- package/package.json +7 -3
- package/src/__snapshots__/cli.test.ts.snap +0 -219
- package/src/__test__/mcp-ctx-fixture.test.ts +0 -108
- package/src/__test__/mcp-ctx-fixture.ts +0 -104
- package/src/cli.test.ts +0 -66
- package/src/cli.ts +0 -43
- package/src/commands/cliproxy/config.test.ts +0 -475
- package/src/commands/cliproxy/config.ts +0 -238
- package/src/commands/cliproxy/deploy.test.ts +0 -181
- package/src/commands/cliproxy/deploy.ts +0 -157
- package/src/commands/cliproxy/host.test.ts +0 -73
- package/src/commands/cliproxy/host.ts +0 -31
- package/src/commands/cliproxy/index.ts +0 -21
- package/src/commands/cliproxy/keys.test.ts +0 -326
- package/src/commands/cliproxy/keys.ts +0 -201
- package/src/commands/cliproxy/login.test.ts +0 -415
- package/src/commands/cliproxy/login.ts +0 -131
- package/src/commands/cliproxy/models.test.ts +0 -592
- package/src/commands/cliproxy/models.ts +0 -237
- package/src/commands/cliproxy/open.test.ts +0 -132
- package/src/commands/cliproxy/open.ts +0 -78
- package/src/commands/cliproxy/setup/gh.test.ts +0 -218
- package/src/commands/cliproxy/setup/gh.ts +0 -250
- package/src/commands/cliproxy/setup/preview.test.ts +0 -159
- package/src/commands/cliproxy/setup/preview.ts +0 -41
- package/src/commands/cliproxy/setup/prompts.test.ts +0 -58
- package/src/commands/cliproxy/setup/prompts.ts +0 -99
- package/src/commands/cliproxy/setup/providers.test.ts +0 -245
- package/src/commands/cliproxy/setup/providers.ts +0 -136
- package/src/commands/cliproxy/setup/smoke-test.test.ts +0 -821
- package/src/commands/cliproxy/setup/smoke-test.ts +0 -223
- package/src/commands/cliproxy/setup/templates.test.ts +0 -357
- package/src/commands/cliproxy/setup/templates.ts +0 -145
- package/src/commands/cliproxy/setup/validation.test.ts +0 -567
- package/src/commands/cliproxy/setup/validation.ts +0 -209
- package/src/commands/cliproxy/setup/workflow-analyzer.test.ts +0 -363
- package/src/commands/cliproxy/setup/workflow-analyzer.ts +0 -136
- package/src/commands/cliproxy/setup.test.ts +0 -2366
- package/src/commands/cliproxy/setup.ts +0 -809
- package/src/commands/cliproxy/shared.test.ts +0 -118
- package/src/commands/cliproxy/shared.ts +0 -84
- package/src/commands/cliproxy/status.test.ts +0 -1157
- package/src/commands/cliproxy/status.ts +0 -597
- package/src/commands/dashboard/deploy.test.ts +0 -215
- package/src/commands/dashboard/deploy.ts +0 -135
- package/src/commands/dashboard/host.test.ts +0 -79
- package/src/commands/dashboard/host.ts +0 -33
- package/src/commands/dashboard/index.ts +0 -13
- package/src/commands/dashboard/logs.test.ts +0 -317
- package/src/commands/dashboard/logs.ts +0 -180
- package/src/commands/dashboard/status.test.ts +0 -708
- package/src/commands/dashboard/status.ts +0 -293
- package/src/commands/gateway/backup.test.ts +0 -426
- package/src/commands/gateway/backup.ts +0 -205
- package/src/commands/gateway/deploy.test.ts +0 -491
- package/src/commands/gateway/deploy.ts +0 -173
- package/src/commands/gateway/host.test.ts +0 -89
- package/src/commands/gateway/host.ts +0 -33
- package/src/commands/gateway/index.ts +0 -17
- package/src/commands/gateway/logs.test.ts +0 -263
- package/src/commands/gateway/logs.ts +0 -160
- package/src/commands/gateway/restore.test.ts +0 -650
- package/src/commands/gateway/restore.ts +0 -308
- package/src/commands/gateway/status.test.ts +0 -755
- package/src/commands/gateway/status.ts +0 -255
- package/src/commands/keeweb/deploy.test.ts +0 -177
- package/src/commands/keeweb/deploy.ts +0 -191
- package/src/commands/keeweb/index.ts +0 -11
- package/src/commands/keeweb/open.test.ts +0 -54
- package/src/commands/keeweb/open.ts +0 -30
- package/src/commands/keeweb/status.test.ts +0 -509
- package/src/commands/keeweb/status.ts +0 -359
- package/src/commands/mcp.test.ts +0 -260
- package/src/commands/mcp.ts +0 -58
- package/src/commands/status.test.ts +0 -222
- package/src/commands/status.ts +0 -161
- package/src/commands/umami/deploy.test.ts +0 -202
- package/src/commands/umami/deploy.ts +0 -132
- package/src/commands/umami/host.test.ts +0 -79
- package/src/commands/umami/host.ts +0 -33
- package/src/commands/umami/index.ts +0 -13
- package/src/commands/umami/logs.test.ts +0 -191
- package/src/commands/umami/logs.ts +0 -163
- package/src/commands/umami/status.test.ts +0 -633
- package/src/commands/umami/status.ts +0 -285
- package/src/commands/vpn/client.test.ts +0 -949
- package/src/commands/vpn/client.ts +0 -467
- package/src/commands/vpn/deploy.test.ts +0 -208
- package/src/commands/vpn/deploy.ts +0 -143
- package/src/commands/vpn/host.test.ts +0 -89
- package/src/commands/vpn/host.ts +0 -36
- package/src/commands/vpn/index.ts +0 -15
- package/src/commands/vpn/logs.test.ts +0 -234
- package/src/commands/vpn/logs.ts +0 -136
- package/src/commands/vpn/peers.test.ts +0 -478
- package/src/commands/vpn/shared.test.ts +0 -454
- package/src/commands/vpn/shared.ts +0 -170
- package/src/commands/vpn/status.test.ts +0 -142
- package/src/commands/vpn/status.ts +0 -78
- package/src/conventions.test.ts +0 -1180
- package/src/lib/action-ctx.ts +0 -25
- package/src/lib/known-hosts.test.ts +0 -192
- package/src/lib/known-hosts.ts +0 -76
- package/src/lib/redact.ts +0 -21
- package/src/lib/ssh-identity.test.ts +0 -134
- package/src/lib/ssh-identity.ts +0 -76
- /package/{src/resources/known_hosts → dist/known_hosts-tk724cp0.} +0 -0
|
@@ -1,491 +0,0 @@
|
|
|
1
|
-
import {resolve} from 'node:path'
|
|
2
|
-
import {afterEach, beforeEach, describe, expect, it, spyOn} from 'bun:test'
|
|
3
|
-
|
|
4
|
-
import {getGatewayDeployEnv, validateGatewayRemotePreconditions} from './deploy'
|
|
5
|
-
|
|
6
|
-
const cliDir = resolve(import.meta.dir, '../../..')
|
|
7
|
-
|
|
8
|
-
const envKeys = [
|
|
9
|
-
'AWS_ACCESS_KEY_ID',
|
|
10
|
-
'AWS_SECRET_ACCESS_KEY',
|
|
11
|
-
'AWS_SESSION_TOKEN',
|
|
12
|
-
'DISCORD_APPLICATION_ID',
|
|
13
|
-
'DISCORD_GUILD_ID',
|
|
14
|
-
'DISCORD_TOKEN',
|
|
15
|
-
'GATEWAY_HOST',
|
|
16
|
-
'GATEWAY_PRESENCE_CHANNEL_ID',
|
|
17
|
-
'GATEWAY_TRIGGER_ROLE_ID',
|
|
18
|
-
'GATEWAY_WEBHOOK_SECRET',
|
|
19
|
-
'HOME',
|
|
20
|
-
'OBJECT_STORE_HOSTS',
|
|
21
|
-
'PATH',
|
|
22
|
-
'S3_BUCKET',
|
|
23
|
-
'S3_ENDPOINT',
|
|
24
|
-
'S3_REGION',
|
|
25
|
-
'SSH_AUTH_SOCK',
|
|
26
|
-
'WORKSPACE_OPENCODE_AUTH',
|
|
27
|
-
'WORKSPACE_OPENCODE_CONFIG',
|
|
28
|
-
'WORKSPACE_OPENCODE_MODEL',
|
|
29
|
-
'WORKSPACE_OPENCODE_TOKEN',
|
|
30
|
-
'WORKSPACE_OPENCODE_URL',
|
|
31
|
-
] as const
|
|
32
|
-
|
|
33
|
-
type ManagedEnvKey = (typeof envKeys)[number]
|
|
34
|
-
|
|
35
|
-
let originalEnv: Partial<Record<ManagedEnvKey, string | undefined>>
|
|
36
|
-
|
|
37
|
-
function restoreManagedEnv(): void {
|
|
38
|
-
for (const key of envKeys) {
|
|
39
|
-
const value = originalEnv[key]
|
|
40
|
-
|
|
41
|
-
if (value === undefined) {
|
|
42
|
-
delete process.env[key]
|
|
43
|
-
continue
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
process.env[key] = value
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function setManagedEnv(overrides: Partial<Record<ManagedEnvKey, string | undefined>>): void {
|
|
51
|
-
restoreManagedEnv()
|
|
52
|
-
|
|
53
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
54
|
-
if (value === undefined) {
|
|
55
|
-
delete process.env[key]
|
|
56
|
-
continue
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
process.env[key as ManagedEnvKey] = value
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function runDeployCommand(
|
|
64
|
-
args: string[],
|
|
65
|
-
envOverrides: Partial<Record<ManagedEnvKey, string | undefined>> = {},
|
|
66
|
-
): Promise<{stdout: string; stderr: string; exitCode: number}> {
|
|
67
|
-
const env: Record<string, string> = {}
|
|
68
|
-
|
|
69
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
70
|
-
if (value !== undefined) {
|
|
71
|
-
env[key] = value
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Apply overrides: undefined means explicitly unset the key
|
|
76
|
-
for (const [key, value] of Object.entries(envOverrides)) {
|
|
77
|
-
if (value === undefined) {
|
|
78
|
-
delete env[key]
|
|
79
|
-
} else {
|
|
80
|
-
env[key] = value
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Apply defaults only for keys not explicitly overridden
|
|
85
|
-
if (!Object.prototype.hasOwnProperty.call(envOverrides, 'HOME') && !env.HOME) {
|
|
86
|
-
env.HOME = '/tmp/test-home'
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!Object.prototype.hasOwnProperty.call(envOverrides, 'PATH') && !env.PATH) {
|
|
90
|
-
env.PATH = '/usr/bin:/bin'
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (!Object.prototype.hasOwnProperty.call(envOverrides, 'SSH_AUTH_SOCK') && !env.SSH_AUTH_SOCK) {
|
|
94
|
-
env.SSH_AUTH_SOCK = '/tmp/test-sock'
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
env.NO_COLOR = '1'
|
|
98
|
-
|
|
99
|
-
const proc = Bun.spawn(['bun', 'src/cli.ts', 'gateway', 'deploy', ...args], {
|
|
100
|
-
cwd: cliDir,
|
|
101
|
-
env,
|
|
102
|
-
stdout: 'pipe',
|
|
103
|
-
stderr: 'pipe',
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
107
|
-
new Response(proc.stdout).text(),
|
|
108
|
-
new Response(proc.stderr).text(),
|
|
109
|
-
proc.exited,
|
|
110
|
-
])
|
|
111
|
-
|
|
112
|
-
return {stdout, stderr, exitCode}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
describe('gateway deploy', () => {
|
|
116
|
-
beforeEach(() => {
|
|
117
|
-
originalEnv = Object.fromEntries(envKeys.map(key => [key, process.env[key]]))
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
afterEach(() => {
|
|
121
|
-
restoreManagedEnv()
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
describe('getGatewayDeployEnv', () => {
|
|
125
|
-
it('returns the expected deploy environment when required variables are present', () => {
|
|
126
|
-
setManagedEnv({
|
|
127
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
128
|
-
HOME: '/tmp/test-home',
|
|
129
|
-
PATH: '/usr/bin:/bin',
|
|
130
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
const env = getGatewayDeployEnv()
|
|
134
|
-
|
|
135
|
-
expect(env.GATEWAY_HOST).toBe('gateway.example.com')
|
|
136
|
-
expect(env.HOME).toBe('/tmp/test-home')
|
|
137
|
-
expect(env.PATH).toBe('/usr/bin:/bin')
|
|
138
|
-
expect(env.SSH_AUTH_SOCK).toBe('/tmp/test-sock')
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it('throws when SSH_AUTH_SOCK is missing', () => {
|
|
142
|
-
setManagedEnv({
|
|
143
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
144
|
-
HOME: '/tmp/test-home',
|
|
145
|
-
PATH: '/usr/bin:/bin',
|
|
146
|
-
SSH_AUTH_SOCK: undefined,
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
expect(() => getGatewayDeployEnv()).toThrow('SSH_AUTH_SOCK')
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('throws when PATH is missing', () => {
|
|
153
|
-
setManagedEnv({
|
|
154
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
155
|
-
HOME: '/tmp/test-home',
|
|
156
|
-
PATH: undefined,
|
|
157
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
expect(() => getGatewayDeployEnv()).toThrow('PATH')
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('passes required gateway env vars to the child process', () => {
|
|
164
|
-
setManagedEnv({
|
|
165
|
-
AWS_ACCESS_KEY_ID: 'test-key-id',
|
|
166
|
-
AWS_SECRET_ACCESS_KEY: 'test-secret',
|
|
167
|
-
AWS_SESSION_TOKEN: undefined,
|
|
168
|
-
DISCORD_APPLICATION_ID: 'test-app-id',
|
|
169
|
-
DISCORD_GUILD_ID: 'test-guild-id',
|
|
170
|
-
DISCORD_TOKEN: 'test-token',
|
|
171
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
172
|
-
HOME: '/tmp/test-home',
|
|
173
|
-
OBJECT_STORE_HOSTS: undefined,
|
|
174
|
-
PATH: '/usr/bin:/bin',
|
|
175
|
-
S3_BUCKET: 'test-bucket',
|
|
176
|
-
S3_ENDPOINT: undefined,
|
|
177
|
-
S3_REGION: 'us-east-1',
|
|
178
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
const env = getGatewayDeployEnv()
|
|
182
|
-
|
|
183
|
-
expect(env.DISCORD_TOKEN).toBe('test-token')
|
|
184
|
-
expect(env.AWS_ACCESS_KEY_ID).toBe('test-key-id')
|
|
185
|
-
expect(env.AWS_SECRET_ACCESS_KEY).toBe('test-secret')
|
|
186
|
-
expect(env.DISCORD_APPLICATION_ID).toBe('test-app-id')
|
|
187
|
-
expect(env.DISCORD_GUILD_ID).toBe('test-guild-id')
|
|
188
|
-
expect(env.S3_BUCKET).toBe('test-bucket')
|
|
189
|
-
expect(env.S3_REGION).toBe('us-east-1')
|
|
190
|
-
expect(env.GATEWAY_HOST).toBe('gateway.example.com')
|
|
191
|
-
// Optional vars present (empty string when unset)
|
|
192
|
-
expect(env.S3_ENDPOINT).toBe('')
|
|
193
|
-
expect(env.OBJECT_STORE_HOSTS).toBe('')
|
|
194
|
-
expect(env.AWS_SESSION_TOKEN).toBe('')
|
|
195
|
-
// Core vars still present
|
|
196
|
-
expect(env.PATH).toBe('/usr/bin:/bin')
|
|
197
|
-
expect(env.HOME).toBe('/tmp/test-home')
|
|
198
|
-
expect(env.SSH_AUTH_SOCK).toBe('/tmp/test-sock')
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('forwards S3_ENDPOINT and OBJECT_STORE_HOSTS when present in process.env', () => {
|
|
202
|
-
setManagedEnv({
|
|
203
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
204
|
-
HOME: '/tmp/test-home',
|
|
205
|
-
OBJECT_STORE_HOSTS: 'r2.example.com minio.example.com',
|
|
206
|
-
PATH: '/usr/bin:/bin',
|
|
207
|
-
S3_ENDPOINT: 'https://r2.example.com',
|
|
208
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
const env = getGatewayDeployEnv()
|
|
212
|
-
|
|
213
|
-
expect(env.S3_ENDPOINT).toBe('https://r2.example.com')
|
|
214
|
-
expect(env.OBJECT_STORE_HOSTS).toBe('r2.example.com minio.example.com')
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
it('forwards AWS_SESSION_TOKEN when present in process.env', () => {
|
|
218
|
-
setManagedEnv({
|
|
219
|
-
AWS_SESSION_TOKEN: 'sts-temporary-token-value',
|
|
220
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
221
|
-
HOME: '/tmp/test-home',
|
|
222
|
-
PATH: '/usr/bin:/bin',
|
|
223
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
const env = getGatewayDeployEnv()
|
|
227
|
-
|
|
228
|
-
expect(env.AWS_SESSION_TOKEN).toBe('sts-temporary-token-value')
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
it('forwards GH_APP_ID and GH_APP_PRIVATE_KEY when present in process.env', () => {
|
|
232
|
-
setManagedEnv({
|
|
233
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
234
|
-
HOME: '/tmp/test-home',
|
|
235
|
-
PATH: '/usr/bin:/bin',
|
|
236
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
237
|
-
})
|
|
238
|
-
process.env.GH_APP_ID = '999888'
|
|
239
|
-
process.env.GH_APP_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----\nfakekey\n-----END RSA PRIVATE KEY-----\n'
|
|
240
|
-
|
|
241
|
-
try {
|
|
242
|
-
const env = getGatewayDeployEnv()
|
|
243
|
-
expect(env.GH_APP_ID).toBe('999888')
|
|
244
|
-
expect(env.GH_APP_PRIVATE_KEY).toBe('-----BEGIN RSA PRIVATE KEY-----\nfakekey\n-----END RSA PRIVATE KEY-----\n')
|
|
245
|
-
} finally {
|
|
246
|
-
delete process.env.GH_APP_ID
|
|
247
|
-
delete process.env.GH_APP_PRIVATE_KEY
|
|
248
|
-
}
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
it('forwards DISCORD_PRIVILEGED_INTENTS when present in process.env', () => {
|
|
252
|
-
setManagedEnv({
|
|
253
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
254
|
-
HOME: '/tmp/test-home',
|
|
255
|
-
PATH: '/usr/bin:/bin',
|
|
256
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
257
|
-
})
|
|
258
|
-
process.env.DISCORD_PRIVILEGED_INTENTS = 'GUILD_MEMBERS'
|
|
259
|
-
|
|
260
|
-
try {
|
|
261
|
-
const env = getGatewayDeployEnv()
|
|
262
|
-
expect(env.DISCORD_PRIVILEGED_INTENTS).toBe('GUILD_MEMBERS')
|
|
263
|
-
} finally {
|
|
264
|
-
delete process.env.DISCORD_PRIVILEGED_INTENTS
|
|
265
|
-
}
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
it('DISCORD_PRIVILEGED_INTENTS unset → forwarded as empty string', () => {
|
|
269
|
-
setManagedEnv({
|
|
270
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
271
|
-
HOME: '/tmp/test-home',
|
|
272
|
-
PATH: '/usr/bin:/bin',
|
|
273
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
274
|
-
})
|
|
275
|
-
delete process.env.DISCORD_PRIVILEGED_INTENTS
|
|
276
|
-
|
|
277
|
-
const env = getGatewayDeployEnv()
|
|
278
|
-
expect(env.DISCORD_PRIVILEGED_INTENTS).toBe('')
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
// ── workspace mention-loop var parity ─────────────────────────────────────
|
|
282
|
-
|
|
283
|
-
it('forwards WORKSPACE_OPENCODE_TOKEN when set', () => {
|
|
284
|
-
setManagedEnv({
|
|
285
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
286
|
-
HOME: '/tmp/test-home',
|
|
287
|
-
PATH: '/usr/bin:/bin',
|
|
288
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
289
|
-
WORKSPACE_OPENCODE_TOKEN: 'ws-tok-123',
|
|
290
|
-
})
|
|
291
|
-
const env = getGatewayDeployEnv()
|
|
292
|
-
expect(env.WORKSPACE_OPENCODE_TOKEN).toBe('ws-tok-123')
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
it('forwards WORKSPACE_OPENCODE_AUTH when set', () => {
|
|
296
|
-
setManagedEnv({
|
|
297
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
298
|
-
HOME: '/tmp/test-home',
|
|
299
|
-
PATH: '/usr/bin:/bin',
|
|
300
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
301
|
-
WORKSPACE_OPENCODE_AUTH: '{"provider":{}}',
|
|
302
|
-
})
|
|
303
|
-
const env = getGatewayDeployEnv()
|
|
304
|
-
expect(env.WORKSPACE_OPENCODE_AUTH).toBe('{"provider":{}}')
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
it('forwards WORKSPACE_OPENCODE_URL when set', () => {
|
|
308
|
-
setManagedEnv({
|
|
309
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
310
|
-
HOME: '/tmp/test-home',
|
|
311
|
-
PATH: '/usr/bin:/bin',
|
|
312
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
313
|
-
WORKSPACE_OPENCODE_URL: 'http://workspace:9200',
|
|
314
|
-
})
|
|
315
|
-
const env = getGatewayDeployEnv()
|
|
316
|
-
expect(env.WORKSPACE_OPENCODE_URL).toBe('http://workspace:9200')
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
it('WORKSPACE_OPENCODE_URL unset → forwarded as empty string (optional)', () => {
|
|
320
|
-
setManagedEnv({
|
|
321
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
322
|
-
HOME: '/tmp/test-home',
|
|
323
|
-
PATH: '/usr/bin:/bin',
|
|
324
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
325
|
-
WORKSPACE_OPENCODE_URL: undefined,
|
|
326
|
-
})
|
|
327
|
-
const env = getGatewayDeployEnv()
|
|
328
|
-
expect(env.WORKSPACE_OPENCODE_URL).toBe('')
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
it('forwards WORKSPACE_OPENCODE_MODEL when set', () => {
|
|
332
|
-
setManagedEnv({
|
|
333
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
334
|
-
HOME: '/tmp/test-home',
|
|
335
|
-
PATH: '/usr/bin:/bin',
|
|
336
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
337
|
-
WORKSPACE_OPENCODE_MODEL: 'anthropic/claude-sonnet-4-6',
|
|
338
|
-
})
|
|
339
|
-
const env = getGatewayDeployEnv()
|
|
340
|
-
expect(env.WORKSPACE_OPENCODE_MODEL).toBe('anthropic/claude-sonnet-4-6')
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
it('forwards WORKSPACE_OPENCODE_CONFIG when set', () => {
|
|
344
|
-
setManagedEnv({
|
|
345
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
346
|
-
HOME: '/tmp/test-home',
|
|
347
|
-
PATH: '/usr/bin:/bin',
|
|
348
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
349
|
-
WORKSPACE_OPENCODE_CONFIG: '{"provider":{}}',
|
|
350
|
-
})
|
|
351
|
-
const env = getGatewayDeployEnv()
|
|
352
|
-
expect(env.WORKSPACE_OPENCODE_CONFIG).toBe('{"provider":{}}')
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
it('forwards GATEWAY_TRIGGER_ROLE_ID when set', () => {
|
|
356
|
-
setManagedEnv({
|
|
357
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
358
|
-
HOME: '/tmp/test-home',
|
|
359
|
-
PATH: '/usr/bin:/bin',
|
|
360
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
361
|
-
GATEWAY_TRIGGER_ROLE_ID: '987654321098765432',
|
|
362
|
-
})
|
|
363
|
-
const env = getGatewayDeployEnv()
|
|
364
|
-
expect(env.GATEWAY_TRIGGER_ROLE_ID).toBe('987654321098765432')
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
// ── announce opt-in vars parity ───────────────────────────────────────────
|
|
368
|
-
|
|
369
|
-
it('forwards GATEWAY_WEBHOOK_SECRET when set', () => {
|
|
370
|
-
setManagedEnv({
|
|
371
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
372
|
-
GATEWAY_WEBHOOK_SECRET: 'hmac-secret-value',
|
|
373
|
-
HOME: '/tmp/test-home',
|
|
374
|
-
PATH: '/usr/bin:/bin',
|
|
375
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
376
|
-
})
|
|
377
|
-
const env = getGatewayDeployEnv()
|
|
378
|
-
expect(env.GATEWAY_WEBHOOK_SECRET).toBe('hmac-secret-value')
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
it('GATEWAY_WEBHOOK_SECRET unset → forwarded as empty string', () => {
|
|
382
|
-
setManagedEnv({
|
|
383
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
384
|
-
GATEWAY_WEBHOOK_SECRET: undefined,
|
|
385
|
-
HOME: '/tmp/test-home',
|
|
386
|
-
PATH: '/usr/bin:/bin',
|
|
387
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
388
|
-
})
|
|
389
|
-
const env = getGatewayDeployEnv()
|
|
390
|
-
expect(env.GATEWAY_WEBHOOK_SECRET).toBe('')
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
it('forwards GATEWAY_PRESENCE_CHANNEL_ID when set', () => {
|
|
394
|
-
setManagedEnv({
|
|
395
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
396
|
-
GATEWAY_PRESENCE_CHANNEL_ID: '111222333444555666',
|
|
397
|
-
HOME: '/tmp/test-home',
|
|
398
|
-
PATH: '/usr/bin:/bin',
|
|
399
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
400
|
-
})
|
|
401
|
-
const env = getGatewayDeployEnv()
|
|
402
|
-
expect(env.GATEWAY_PRESENCE_CHANNEL_ID).toBe('111222333444555666')
|
|
403
|
-
})
|
|
404
|
-
|
|
405
|
-
it('GATEWAY_PRESENCE_CHANNEL_ID unset → forwarded as empty string', () => {
|
|
406
|
-
setManagedEnv({
|
|
407
|
-
GATEWAY_HOST: 'gateway.example.com',
|
|
408
|
-
GATEWAY_PRESENCE_CHANNEL_ID: undefined,
|
|
409
|
-
HOME: '/tmp/test-home',
|
|
410
|
-
PATH: '/usr/bin:/bin',
|
|
411
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
412
|
-
})
|
|
413
|
-
const env = getGatewayDeployEnv()
|
|
414
|
-
expect(env.GATEWAY_PRESENCE_CHANNEL_ID).toBe('')
|
|
415
|
-
})
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
describe('validateGatewayRemotePreconditions', () => {
|
|
419
|
-
it('throws when gh is not available', () => {
|
|
420
|
-
const spy = spyOn(Bun, 'which').mockReturnValue(null)
|
|
421
|
-
|
|
422
|
-
try {
|
|
423
|
-
expect(() => validateGatewayRemotePreconditions()).toThrow('gh')
|
|
424
|
-
} finally {
|
|
425
|
-
spy.mockRestore()
|
|
426
|
-
}
|
|
427
|
-
})
|
|
428
|
-
|
|
429
|
-
it('does not throw when gh is available', () => {
|
|
430
|
-
const spy = spyOn(Bun, 'which').mockReturnValue('/usr/bin/gh')
|
|
431
|
-
|
|
432
|
-
try {
|
|
433
|
-
expect(() => validateGatewayRemotePreconditions()).not.toThrow()
|
|
434
|
-
} finally {
|
|
435
|
-
spy.mockRestore()
|
|
436
|
-
}
|
|
437
|
-
})
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
describe('--remote (default) dry-run', () => {
|
|
441
|
-
it('prints planned actions without invoking gh', async () => {
|
|
442
|
-
const {stdout, exitCode} = await runDeployCommand(['--dry-run'], {
|
|
443
|
-
HOME: '/tmp/test-home',
|
|
444
|
-
PATH: process.env.PATH ?? '/usr/bin:/bin',
|
|
445
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
expect(exitCode).toBe(0)
|
|
449
|
-
expect(stdout).toContain('Dry run')
|
|
450
|
-
expect(stdout).toContain('Deploy Gateway')
|
|
451
|
-
})
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
describe('--local --dry-run', () => {
|
|
455
|
-
it('prints planned local actions without spawning bun run', async () => {
|
|
456
|
-
const {stdout, exitCode} = await runDeployCommand(['--local', '--dry-run'], {
|
|
457
|
-
HOME: '/tmp/test-home',
|
|
458
|
-
PATH: process.env.PATH ?? '/usr/bin:/bin',
|
|
459
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
460
|
-
})
|
|
461
|
-
|
|
462
|
-
expect(exitCode).toBe(0)
|
|
463
|
-
expect(stdout).toContain('Dry run')
|
|
464
|
-
expect(stdout).toContain('local')
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
it('succeeds even when SSH_AUTH_SOCK is unset', async () => {
|
|
468
|
-
const {stdout, exitCode} = await runDeployCommand(['--local', '--dry-run'], {
|
|
469
|
-
HOME: '/tmp/test-home',
|
|
470
|
-
PATH: process.env.PATH ?? '/usr/bin:/bin',
|
|
471
|
-
SSH_AUTH_SOCK: undefined,
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
expect(exitCode).toBe(0)
|
|
475
|
-
expect(stdout).toContain('Dry run')
|
|
476
|
-
})
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
describe('--local --force-recreate --dry-run', () => {
|
|
480
|
-
it('includes --force-recreate in the planned command', async () => {
|
|
481
|
-
const {stdout, exitCode} = await runDeployCommand(['--local', '--force-recreate', '--dry-run'], {
|
|
482
|
-
HOME: '/tmp/test-home',
|
|
483
|
-
PATH: process.env.PATH ?? '/usr/bin:/bin',
|
|
484
|
-
SSH_AUTH_SOCK: '/tmp/test-sock',
|
|
485
|
-
})
|
|
486
|
-
|
|
487
|
-
expect(exitCode).toBe(0)
|
|
488
|
-
expect(stdout).toContain('--force-recreate')
|
|
489
|
-
})
|
|
490
|
-
})
|
|
491
|
-
})
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import type {goke} from 'goke'
|
|
2
|
-
|
|
3
|
-
import {z} from 'zod'
|
|
4
|
-
|
|
5
|
-
declare const process: {
|
|
6
|
-
env: Record<string, string | undefined>
|
|
7
|
-
exitCode?: number
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const REPO = 'marcusrbrown/infra'
|
|
11
|
-
const WORKFLOW_NAME = 'Deploy Gateway'
|
|
12
|
-
const WORKFLOW_URL = 'https://github.com/marcusrbrown/infra/actions/workflows/deploy-gateway.yaml'
|
|
13
|
-
|
|
14
|
-
type CliInstance = ReturnType<typeof goke>
|
|
15
|
-
|
|
16
|
-
// ─── Env helpers ─────────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
export function getGatewayDeployEnv(): Record<string, string> {
|
|
19
|
-
const path = process.env.PATH
|
|
20
|
-
const home = process.env.HOME
|
|
21
|
-
const sshAuthSock = process.env.SSH_AUTH_SOCK
|
|
22
|
-
|
|
23
|
-
if (!path) {
|
|
24
|
-
throw new Error('PATH is required for local deploy')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!home) {
|
|
28
|
-
throw new Error('HOME is required for local deploy')
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (!sshAuthSock) {
|
|
32
|
-
throw new Error('SSH_AUTH_SOCK is required for local deploy. Start ssh-agent and load your deploy key first.')
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
PATH: path,
|
|
37
|
-
HOME: home,
|
|
38
|
-
SSH_AUTH_SOCK: sshAuthSock,
|
|
39
|
-
GATEWAY_HOST: process.env.GATEWAY_HOST ?? '',
|
|
40
|
-
DISCORD_TOKEN: process.env.DISCORD_TOKEN ?? '',
|
|
41
|
-
DISCORD_APPLICATION_ID: process.env.DISCORD_APPLICATION_ID ?? '',
|
|
42
|
-
DISCORD_GUILD_ID: process.env.DISCORD_GUILD_ID ?? '',
|
|
43
|
-
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID ?? '',
|
|
44
|
-
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY ?? '',
|
|
45
|
-
AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN ?? '',
|
|
46
|
-
S3_BUCKET: process.env.S3_BUCKET ?? '',
|
|
47
|
-
S3_REGION: process.env.S3_REGION ?? '',
|
|
48
|
-
S3_ENDPOINT: process.env.S3_ENDPOINT ?? '',
|
|
49
|
-
OBJECT_STORE_HOSTS: process.env.OBJECT_STORE_HOSTS ?? '',
|
|
50
|
-
GH_APP_ID: process.env.GH_APP_ID ?? '',
|
|
51
|
-
GH_APP_PRIVATE_KEY: process.env.GH_APP_PRIVATE_KEY ?? '',
|
|
52
|
-
DISCORD_PRIVILEGED_INTENTS: process.env.DISCORD_PRIVILEGED_INTENTS ?? '',
|
|
53
|
-
WORKSPACE_OPENCODE_TOKEN: process.env.WORKSPACE_OPENCODE_TOKEN ?? '',
|
|
54
|
-
WORKSPACE_OPENCODE_AUTH: process.env.WORKSPACE_OPENCODE_AUTH ?? '',
|
|
55
|
-
WORKSPACE_OPENCODE_URL: process.env.WORKSPACE_OPENCODE_URL ?? '',
|
|
56
|
-
WORKSPACE_OPENCODE_MODEL: process.env.WORKSPACE_OPENCODE_MODEL ?? '',
|
|
57
|
-
WORKSPACE_OPENCODE_CONFIG: process.env.WORKSPACE_OPENCODE_CONFIG ?? '',
|
|
58
|
-
GATEWAY_TRIGGER_ROLE_ID: process.env.GATEWAY_TRIGGER_ROLE_ID ?? '',
|
|
59
|
-
GATEWAY_WEBHOOK_SECRET: process.env.GATEWAY_WEBHOOK_SECRET ?? '',
|
|
60
|
-
GATEWAY_PRESENCE_CHANNEL_ID: process.env.GATEWAY_PRESENCE_CHANNEL_ID ?? '',
|
|
61
|
-
GATEWAY_OPERATOR_BIND_HOST: process.env.GATEWAY_OPERATOR_BIND_HOST ?? '',
|
|
62
|
-
GATEWAY_OPERATOR_BIND_PORT: process.env.GATEWAY_OPERATOR_BIND_PORT ?? '',
|
|
63
|
-
GATEWAY_OPERATOR_PUBLIC_ORIGIN: process.env.GATEWAY_OPERATOR_PUBLIC_ORIGIN ?? '',
|
|
64
|
-
GATEWAY_OPERATOR_GITHUB_CLIENT_ID: process.env.GATEWAY_OPERATOR_GITHUB_CLIENT_ID ?? '',
|
|
65
|
-
GATEWAY_OPERATOR_GITHUB_CLIENT_SECRET: process.env.GATEWAY_OPERATOR_GITHUB_CLIENT_SECRET ?? '',
|
|
66
|
-
GATEWAY_OPERATOR_CSRF_SECRET: process.env.GATEWAY_OPERATOR_CSRF_SECRET ?? '',
|
|
67
|
-
GATEWAY_OPERATOR_ALLOWLIST: process.env.GATEWAY_OPERATOR_ALLOWLIST ?? '',
|
|
68
|
-
GATEWAY_OPERATOR_OAUTH_ALLOWED_RETURN_PATHS: process.env.GATEWAY_OPERATOR_OAUTH_ALLOWED_RETURN_PATHS ?? '',
|
|
69
|
-
GATEWAY_OPERATOR_OAUTH_STATE_TTL_MS: process.env.GATEWAY_OPERATOR_OAUTH_STATE_TTL_MS ?? '',
|
|
70
|
-
GATEWAY_OPERATOR_OAUTH_MAX_OUTSTANDING_ATTEMPTS: process.env.GATEWAY_OPERATOR_OAUTH_MAX_OUTSTANDING_ATTEMPTS ?? '',
|
|
71
|
-
GATEWAY_VPC_IP: process.env.GATEWAY_VPC_IP ?? '',
|
|
72
|
-
DASHBOARD_VPC_IP: process.env.DASHBOARD_VPC_IP ?? '',
|
|
73
|
-
GATEWAY_IMAGE_DIGEST: process.env.GATEWAY_IMAGE_DIGEST ?? '',
|
|
74
|
-
WORKSPACE_IMAGE_DIGEST: process.env.WORKSPACE_IMAGE_DIGEST ?? '',
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function validateGatewayRemotePreconditions(): void {
|
|
79
|
-
if (!Bun.which('gh')) {
|
|
80
|
-
throw new Error('gh CLI is required for remote deploy. Install gh and run `gh auth login`.')
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ─── Command registration ─────────────────────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
export function registerGatewayDeploy(cli: CliInstance): void {
|
|
87
|
-
cli
|
|
88
|
-
.command(
|
|
89
|
-
'gateway deploy',
|
|
90
|
-
'Deploy the gateway. Default mode triggers the GitHub Deploy Gateway workflow, while --local runs apps/gateway deploy directly with Bun.',
|
|
91
|
-
)
|
|
92
|
-
.option(
|
|
93
|
-
'--local',
|
|
94
|
-
z
|
|
95
|
-
.boolean()
|
|
96
|
-
.default(false)
|
|
97
|
-
.describe('Run local deployment with Bun using apps/gateway instead of triggering GitHub Actions.'),
|
|
98
|
-
)
|
|
99
|
-
.option(
|
|
100
|
-
'--dry-run',
|
|
101
|
-
z
|
|
102
|
-
.boolean()
|
|
103
|
-
.default(false)
|
|
104
|
-
.describe(
|
|
105
|
-
'Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.',
|
|
106
|
-
),
|
|
107
|
-
)
|
|
108
|
-
.option(
|
|
109
|
-
'--force-recreate',
|
|
110
|
-
z.boolean().default(false).describe('Force recreate containers. Forwarded only in local mode.'),
|
|
111
|
-
)
|
|
112
|
-
.example('# Trigger remote GitHub Actions deploy (default mode)')
|
|
113
|
-
.example('infra gateway deploy')
|
|
114
|
-
.example('# Validate local deploy preconditions and planned command')
|
|
115
|
-
.example('infra gateway deploy --local --dry-run')
|
|
116
|
-
.example('# Run local deploy with explicit SSH agent context')
|
|
117
|
-
.example('infra gateway deploy --local')
|
|
118
|
-
.action(async options => {
|
|
119
|
-
if (options.local) {
|
|
120
|
-
const command = [
|
|
121
|
-
'bun',
|
|
122
|
-
'run',
|
|
123
|
-
'--cwd',
|
|
124
|
-
'apps/gateway',
|
|
125
|
-
'deploy',
|
|
126
|
-
...(options.forceRecreate ? ['--force-recreate'] : []),
|
|
127
|
-
]
|
|
128
|
-
|
|
129
|
-
if (options.dryRun) {
|
|
130
|
-
console.log('Dry run: local gateway deploy')
|
|
131
|
-
console.log(`- command: ${command.join(' ')}`)
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const env = getGatewayDeployEnv()
|
|
136
|
-
|
|
137
|
-
const child = Bun.spawn(command, {
|
|
138
|
-
env,
|
|
139
|
-
stdout: 'inherit',
|
|
140
|
-
stderr: 'inherit',
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
const exitCode = await child.exited
|
|
144
|
-
if (exitCode !== 0) {
|
|
145
|
-
throw new Error(`Local deploy failed with exit code ${exitCode}`)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
validateGatewayRemotePreconditions()
|
|
152
|
-
|
|
153
|
-
if (options.dryRun) {
|
|
154
|
-
console.log('Dry run: remote gateway deploy')
|
|
155
|
-
console.log(`- command: gh workflow run "${WORKFLOW_NAME}" --repo ${REPO}`)
|
|
156
|
-
console.log(`- workflow URL: ${WORKFLOW_URL}`)
|
|
157
|
-
return
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const child = Bun.spawn(['gh', 'workflow', 'run', WORKFLOW_NAME, '--repo', REPO], {
|
|
161
|
-
stdout: 'inherit',
|
|
162
|
-
stderr: 'inherit',
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
const exitCode = await child.exited
|
|
166
|
-
if (exitCode !== 0) {
|
|
167
|
-
throw new Error(`Failed to trigger "${WORKFLOW_NAME}" workflow (exit code ${exitCode})`)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
console.log(`Workflow triggered: ${WORKFLOW_URL}`)
|
|
171
|
-
console.log('Approve the gateway environment deployment in GitHub Actions to continue.')
|
|
172
|
-
})
|
|
173
|
-
}
|