@take-out/scripts 0.1.15 → 0.1.16
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/package.json +4 -4
- package/src/exec-with-env.ts +1 -1
- package/src/helpers/deploy-lock.ts +58 -0
- package/src/helpers/get-test-env.ts +3 -0
- package/src/helpers/uncloud-deploy.ts +36 -13
- package/src/run.ts +11 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@take-out/scripts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/run.ts",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^0.8.2",
|
|
32
|
-
"@take-out/helpers": "0.1.
|
|
32
|
+
"@take-out/helpers": "0.1.16",
|
|
33
33
|
"picocolors": "^1.1.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"vxrn": "^1.6.
|
|
36
|
+
"vxrn": "^1.6.16"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"vxrn": {
|
|
@@ -41,6 +41,6 @@
|
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"vxrn": "1.6.
|
|
44
|
+
"vxrn": "1.6.16"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/src/exec-with-env.ts
CHANGED
|
@@ -37,7 +37,7 @@ export async function execWithEnvironment(
|
|
|
37
37
|
...(USE_LOCAL_SERVER && {
|
|
38
38
|
ONE_SERVER_URL: devEnv?.ONE_SERVER_URL,
|
|
39
39
|
// vite reads from .env.production for us unless defined into process.env
|
|
40
|
-
|
|
40
|
+
VITE_ZERO_HOSTNAME: 'start.chat',
|
|
41
41
|
}),
|
|
42
42
|
} as any as Record<string, string>
|
|
43
43
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deploy lock helper — prevents concurrent deploys with auto-expiry
|
|
3
|
+
* shared across repos via @take-out/scripts/helpers/deploy-lock
|
|
4
|
+
*
|
|
5
|
+
* uses mkdir for atomic lock acquisition (no race window)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { run } from './run'
|
|
9
|
+
|
|
10
|
+
const DEFAULT_LOCK_PATH = '/tmp/deploy.lock'
|
|
11
|
+
const DEFAULT_MAX_AGE_MIN = 15
|
|
12
|
+
|
|
13
|
+
interface DeployLockOptions {
|
|
14
|
+
path?: string
|
|
15
|
+
maxAgeMin?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function acquireDeployLock(
|
|
19
|
+
ssh: string,
|
|
20
|
+
opts?: DeployLockOptions
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const lockPath = opts?.path || DEFAULT_LOCK_PATH
|
|
23
|
+
const maxAge = opts?.maxAgeMin || DEFAULT_MAX_AGE_MIN
|
|
24
|
+
|
|
25
|
+
// mkdir is atomic — if two deploys race, only one succeeds
|
|
26
|
+
const lockCmd = [
|
|
27
|
+
// clean stale locks first
|
|
28
|
+
`find ${lockPath} -maxdepth 0 -mmin +${maxAge} -exec rm -rf {} \\; 2>/dev/null || true`,
|
|
29
|
+
// atomic acquire via mkdir (fails if dir already exists)
|
|
30
|
+
`mkdir ${lockPath} 2>/dev/null && echo "OK" || echo "LOCKED"`,
|
|
31
|
+
].join('; ')
|
|
32
|
+
|
|
33
|
+
const { stdout } = await run(`${ssh} "${lockCmd}"`, {
|
|
34
|
+
captureOutput: true,
|
|
35
|
+
silent: true,
|
|
36
|
+
})
|
|
37
|
+
const result = stdout.trim()
|
|
38
|
+
|
|
39
|
+
if (result === 'LOCKED') {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`another deploy is in progress (${lockPath} exists on server). ` +
|
|
42
|
+
`if stale, it will auto-expire after ${maxAge} minutes, ` +
|
|
43
|
+
`or remove manually: ${ssh} "rm -rf ${lockPath}"`
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function releaseDeployLock(
|
|
49
|
+
ssh: string,
|
|
50
|
+
opts?: DeployLockOptions
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
const lockPath = opts?.path || DEFAULT_LOCK_PATH
|
|
53
|
+
try {
|
|
54
|
+
await run(`${ssh} "rm -rf ${lockPath}"`, { silent: true })
|
|
55
|
+
} catch {
|
|
56
|
+
// best-effort, lock will auto-expire
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -29,6 +29,9 @@ export async function getTestEnv() {
|
|
|
29
29
|
ZERO_LOG_LEVEL: 'warn',
|
|
30
30
|
}),
|
|
31
31
|
DO_NOT_TRACK: '1',
|
|
32
|
+
// force localhost urls so cross-subdomain cookies don't activate in test
|
|
33
|
+
BETTER_AUTH_URL: `http://localhost:${appPort}`,
|
|
34
|
+
ONE_SERVER_URL: `http://localhost:${appPort}`,
|
|
32
35
|
ZERO_MUTATE_URL: `http://${dockerHost}:${appPort}/api/zero/push`,
|
|
33
36
|
ZERO_QUERY_URL: `http://${dockerHost}:${appPort}/api/zero/pull`,
|
|
34
37
|
ZERO_UPSTREAM_DB: `${dockerDbBase}/postgres`,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* generic uncloud deployment helpers for ci/cd
|
|
3
|
+
* shared across repos via @take-out/scripts/helpers/uncloud-deploy
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
import
|
|
6
|
+
import { existsSync } from 'node:fs'
|
|
7
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
6
8
|
import { homedir } from 'node:os'
|
|
7
9
|
import { join } from 'node:path'
|
|
8
10
|
|
|
@@ -82,26 +84,47 @@ export async function installUncloudCLI(version = '0.16.0') {
|
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
export
|
|
86
|
-
|
|
87
|
+
export interface SetupSSHKeyOptions {
|
|
88
|
+
// env var containing the key path or content (default: DEPLOY_SSH_KEY)
|
|
89
|
+
envVar?: string
|
|
90
|
+
// filename to write in ~/.ssh/ (default: uncloud_deploy)
|
|
91
|
+
keyName?: string
|
|
92
|
+
// host to add to known_hosts (default: process.env.DEPLOY_HOST)
|
|
93
|
+
host?: string
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* resolve an ssh key from env - handles both file paths (local) and
|
|
98
|
+
* raw/base64 key content (CI). writes to ~/.ssh/{keyName} and updates
|
|
99
|
+
* the env var to point to the file.
|
|
100
|
+
*/
|
|
101
|
+
export async function setupSSHKey(options: SetupSSHKeyOptions = {}) {
|
|
102
|
+
const envVar = options.envVar || 'DEPLOY_SSH_KEY'
|
|
103
|
+
const keyName = options.keyName || 'uncloud_deploy'
|
|
104
|
+
const host = options.host || process.env.DEPLOY_HOST
|
|
105
|
+
|
|
106
|
+
if (!process.env[envVar]) {
|
|
87
107
|
return
|
|
88
108
|
}
|
|
89
109
|
|
|
90
|
-
|
|
110
|
+
// expand ~ to home directory (node doesn't do this automatically)
|
|
111
|
+
const sshKeyValue = process.env[envVar]!.replace(/^~/, homedir())
|
|
91
112
|
|
|
92
113
|
// check if it's a path to an existing file (local usage) or key content (CI usage)
|
|
93
|
-
if (
|
|
114
|
+
if (existsSync(sshKeyValue)) {
|
|
115
|
+
// local usage - ensure env has resolved path (not ~ prefix)
|
|
116
|
+
process.env[envVar] = sshKeyValue
|
|
94
117
|
console.info(` using ssh key from: ${sshKeyValue}`)
|
|
95
118
|
return
|
|
96
119
|
}
|
|
97
120
|
|
|
98
|
-
// CI usage -
|
|
121
|
+
// CI usage - env var contains the actual key content
|
|
99
122
|
console.info('🔑 setting up ssh key from environment...')
|
|
100
123
|
const sshDir = join(homedir(), '.ssh')
|
|
101
|
-
const keyPath = join(sshDir,
|
|
124
|
+
const keyPath = join(sshDir, keyName)
|
|
102
125
|
|
|
103
|
-
if (!
|
|
104
|
-
await
|
|
126
|
+
if (!existsSync(sshDir)) {
|
|
127
|
+
await mkdir(sshDir, { recursive: true })
|
|
105
128
|
}
|
|
106
129
|
|
|
107
130
|
// decode base64-encoded keys (github secrets often store keys as base64)
|
|
@@ -119,13 +142,13 @@ export async function setupSSHKey() {
|
|
|
119
142
|
keyContent += '\n'
|
|
120
143
|
}
|
|
121
144
|
|
|
122
|
-
await
|
|
145
|
+
await writeFile(keyPath, keyContent, { mode: 0o600 })
|
|
123
146
|
|
|
124
147
|
// add host to known_hosts
|
|
125
|
-
if (
|
|
148
|
+
if (host) {
|
|
126
149
|
try {
|
|
127
150
|
await run(
|
|
128
|
-
`ssh-keyscan -H ${
|
|
151
|
+
`ssh-keyscan -H ${host} >> ${join(sshDir, 'known_hosts')}`,
|
|
129
152
|
{ silent: true, timeout: time.ms.seconds(10) }
|
|
130
153
|
)
|
|
131
154
|
} catch {
|
|
@@ -134,6 +157,6 @@ export async function setupSSHKey() {
|
|
|
134
157
|
}
|
|
135
158
|
|
|
136
159
|
// override env var to point to the file we created
|
|
137
|
-
process.env
|
|
160
|
+
process.env[envVar] = keyPath
|
|
138
161
|
console.info(` ssh key written to ${keyPath}`)
|
|
139
162
|
}
|
package/src/run.ts
CHANGED
|
@@ -261,7 +261,7 @@ const runScript = async (
|
|
|
261
261
|
prefixLabel: string = name,
|
|
262
262
|
restarts = 0,
|
|
263
263
|
extraArgs: string[] = [],
|
|
264
|
-
managedIndex?: number
|
|
264
|
+
managedIndex?: number
|
|
265
265
|
) => {
|
|
266
266
|
const index = managedIndex ?? managedProcesses.length
|
|
267
267
|
|
|
@@ -389,9 +389,7 @@ function computeShortcuts() {
|
|
|
389
389
|
const lengths = new Array(managedProcesses.length).fill(1) as number[]
|
|
390
390
|
|
|
391
391
|
for (let round = 0; round < 5; round++) {
|
|
392
|
-
const shortcuts = initials.map(
|
|
393
|
-
(init, i) => init.slice(0, lengths[i]) || init
|
|
394
|
-
)
|
|
392
|
+
const shortcuts = initials.map((init, i) => init.slice(0, lengths[i]) || init)
|
|
395
393
|
|
|
396
394
|
let hasCollision = false
|
|
397
395
|
const groups = new Map<string, number[]>()
|
|
@@ -457,7 +455,9 @@ async function killProcess(index: number) {
|
|
|
457
455
|
if (!managed) return
|
|
458
456
|
|
|
459
457
|
if (managed.killing) {
|
|
460
|
-
console.info(
|
|
458
|
+
console.info(
|
|
459
|
+
`\x1b[2m ${managed.shortcut} ${managed.prefixLabel} already stopped\x1b[0m`
|
|
460
|
+
)
|
|
461
461
|
return
|
|
462
462
|
}
|
|
463
463
|
|
|
@@ -518,7 +518,9 @@ function setupKeyboardShortcuts() {
|
|
|
518
518
|
for (const managed of managedProcesses) {
|
|
519
519
|
const color = colors[managed.index % colors.length]
|
|
520
520
|
const stopped = managed.killing ? `${dim} (stopped)` : ''
|
|
521
|
-
console.info(
|
|
521
|
+
console.info(
|
|
522
|
+
`${dim} ${reset}${color}${managed.shortcut}${reset}${dim} ${managed.prefixLabel}${stopped}${reset}`
|
|
523
|
+
)
|
|
522
524
|
}
|
|
523
525
|
console.info()
|
|
524
526
|
}
|
|
@@ -615,7 +617,9 @@ function printShortcutHint() {
|
|
|
615
617
|
if (managedProcesses.length === 0) return
|
|
616
618
|
|
|
617
619
|
const dim = '\x1b[2m'
|
|
618
|
-
console.info(
|
|
620
|
+
console.info(
|
|
621
|
+
`${dim} ctrl+r restart · ctrl+k kill · ctrl+l clear · ctrl+c exit${reset}`
|
|
622
|
+
)
|
|
619
623
|
console.info()
|
|
620
624
|
}
|
|
621
625
|
|