@take-out/scripts 0.3.1 → 0.4.1-1775377981053
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 +3 -3
- package/src/env-update.ts +45 -79
- package/src/helpers/deploy-lock.ts +50 -0
- package/src/helpers/env-load.ts +4 -17
- package/src/helpers/get-test-env.ts +4 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@take-out/scripts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1-1775377981053",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/cmd.ts",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^0.8.2",
|
|
32
|
-
"@take-out/helpers": "0.
|
|
33
|
-
"@take-out/run": "0.
|
|
32
|
+
"@take-out/helpers": "0.4.1-1775377981053",
|
|
33
|
+
"@take-out/run": "0.4.1-1775377981053",
|
|
34
34
|
"picocolors": "^1.1.1"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/src/env-update.ts
CHANGED
|
@@ -4,17 +4,9 @@ import fs from 'node:fs'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
|
|
6
6
|
import { cmd } from './cmd'
|
|
7
|
-
import { resolveDepVersion } from './helpers/resolve-dep-version'
|
|
8
7
|
|
|
9
8
|
const PRESETS: Record<string, string[]> = {
|
|
10
|
-
takeout: [
|
|
11
|
-
'.github/workflows/*.yml',
|
|
12
|
-
'src/uncloud/*.yml',
|
|
13
|
-
'.env*',
|
|
14
|
-
'src/env.ts',
|
|
15
|
-
'src/constants/env-server.ts',
|
|
16
|
-
'src/server/env-server.ts',
|
|
17
|
-
],
|
|
9
|
+
takeout: ['.github/workflows/*.yml', 'src/uncloud/*.yml', '.env*'],
|
|
18
10
|
}
|
|
19
11
|
|
|
20
12
|
const IGNORE_DIRS = new Set([
|
|
@@ -27,26 +19,45 @@ const IGNORE_DIRS = new Set([
|
|
|
27
19
|
'.docker',
|
|
28
20
|
])
|
|
29
21
|
|
|
30
|
-
await cmd`sync environment variables from
|
|
22
|
+
await cmd`sync environment variables from src/env.ts to matching files`
|
|
31
23
|
.args('--preset string')
|
|
32
24
|
.run(async ({ args }) => {
|
|
33
|
-
|
|
34
|
-
const
|
|
25
|
+
// import env var definitions from src/env.ts (the single source of truth)
|
|
26
|
+
const envModule = await import(path.resolve('src/env.ts'))
|
|
27
|
+
const envVars = envModule.envVars as Record<string, string | symbol>
|
|
28
|
+
const envDeps = (envModule.envDeps || {}) as Record<string, string>
|
|
35
29
|
|
|
36
|
-
if (!envVars ||
|
|
37
|
-
console.error('
|
|
30
|
+
if (!envVars || typeof envVars !== 'object') {
|
|
31
|
+
console.error('no envVars export found in src/env.ts')
|
|
38
32
|
process.exit(1)
|
|
39
33
|
}
|
|
40
34
|
|
|
41
|
-
//
|
|
35
|
+
// the expected symbol from @take-out/env
|
|
36
|
+
const expectedSymbol = Symbol.for('take-out/env/expected')
|
|
37
|
+
|
|
38
|
+
// resolve dep versions from package.json dependencies
|
|
39
|
+
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
|
|
42
40
|
const depResolved: Record<string, string> = {}
|
|
43
|
-
for (const [key,
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
depResolved[key] =
|
|
41
|
+
for (const [key, depName] of Object.entries(envDeps)) {
|
|
42
|
+
const version = packageJson.dependencies?.[depName]?.replace(/^[\^~]/, '')
|
|
43
|
+
if (version) {
|
|
44
|
+
depResolved[key] = version
|
|
45
|
+
// add dep-resolved vars to envVars with resolved value
|
|
46
|
+
envVars[key] = version
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// normalize envVars: expected symbol → true (required), string → string
|
|
51
|
+
// for compatibility with strategy functions
|
|
52
|
+
type EnvEntry = { key: string; required: boolean; defaultValue: string }
|
|
53
|
+
const entries: EnvEntry[] = Object.entries(envVars)
|
|
54
|
+
.map(([key, value]) => ({
|
|
55
|
+
key,
|
|
56
|
+
required: value === expectedSymbol,
|
|
57
|
+
defaultValue: typeof value === 'string' ? value : '',
|
|
58
|
+
}))
|
|
59
|
+
.sort((a, b) => a.key.localeCompare(b.key))
|
|
60
|
+
|
|
50
61
|
// load .gitignore patterns
|
|
51
62
|
const gitignorePatterns: string[] = []
|
|
52
63
|
try {
|
|
@@ -102,13 +113,13 @@ await cmd`sync environment variables from package.json to matching files`
|
|
|
102
113
|
)
|
|
103
114
|
|
|
104
115
|
function walk(dir: string) {
|
|
105
|
-
let
|
|
116
|
+
let dirEntries: fs.Dirent[]
|
|
106
117
|
try {
|
|
107
|
-
|
|
118
|
+
dirEntries = fs.readdirSync(dir, { withFileTypes: true })
|
|
108
119
|
} catch {
|
|
109
120
|
return
|
|
110
121
|
}
|
|
111
|
-
for (const entry of
|
|
122
|
+
for (const entry of dirEntries) {
|
|
112
123
|
const full = dir === '.' ? entry.name : `${dir}/${entry.name}`
|
|
113
124
|
if (isRecursive && isIgnored(full)) continue
|
|
114
125
|
if (entry.isDirectory()) {
|
|
@@ -156,37 +167,23 @@ await cmd`sync environment variables from package.json to matching files`
|
|
|
156
167
|
const markerEnd = '🔒 end - this is generated by "bun env:update"'
|
|
157
168
|
const yamlStart = `# ${markerStart}`
|
|
158
169
|
const yamlEnd = `# ${markerEnd}`
|
|
159
|
-
const jsStart = `// ${markerStart}`
|
|
160
|
-
const jsEnd = `// ${markerEnd}`
|
|
161
|
-
|
|
162
170
|
// .env auto-generated section markers
|
|
163
171
|
const dotenvSectionStart = '# ---- BEGIN AUTO-GENERATED (DO NOT EDIT) ----'
|
|
164
172
|
const dotenvSectionEnd = '# ---- END AUTO-GENERATED ----'
|
|
165
173
|
|
|
166
|
-
type Strategy =
|
|
167
|
-
| 'yaml-markers'
|
|
168
|
-
| 'js-markers'
|
|
169
|
-
| 'dotenv-section'
|
|
170
|
-
| 'dotenv-inline'
|
|
171
|
-
| 'ts-inline'
|
|
174
|
+
type Strategy = 'yaml-markers' | 'dotenv-section' | 'dotenv-inline'
|
|
172
175
|
|
|
173
176
|
function detectStrategy(filePath: string, content: string): Strategy | null {
|
|
174
177
|
if (content.includes(yamlStart) && content.includes(yamlEnd)) {
|
|
175
178
|
return 'yaml-markers'
|
|
176
179
|
}
|
|
177
|
-
if (content.includes(jsStart) && content.includes(jsEnd)) {
|
|
178
|
-
return 'js-markers'
|
|
179
|
-
}
|
|
180
180
|
const basename = path.basename(filePath)
|
|
181
181
|
if (basename === '.env') {
|
|
182
182
|
return 'dotenv-section'
|
|
183
183
|
}
|
|
184
|
-
if (/^\.env/.test(basename)) {
|
|
184
|
+
if (/^\.env/.test(basename) && Object.keys(depResolved).length > 0) {
|
|
185
185
|
return 'dotenv-inline'
|
|
186
186
|
}
|
|
187
|
-
if (filePath.endsWith('.ts') && Object.keys(depResolved).length > 0) {
|
|
188
|
-
return 'ts-inline'
|
|
189
|
-
}
|
|
190
187
|
return null
|
|
191
188
|
}
|
|
192
189
|
|
|
@@ -219,10 +216,10 @@ await cmd`sync environment variables from package.json to matching files`
|
|
|
219
216
|
const isMap = /x-[\w-]+:.*&[\w-]+/s.test(beforeMarker)
|
|
220
217
|
const isGithubActions = filePath.includes('.github/workflows')
|
|
221
218
|
|
|
222
|
-
const envLines =
|
|
223
|
-
.map((
|
|
224
|
-
const dep =
|
|
225
|
-
const
|
|
219
|
+
const envLines = entries
|
|
220
|
+
.map(({ key, defaultValue }) => {
|
|
221
|
+
const dep = depResolved[key]
|
|
222
|
+
const val = dep || defaultValue
|
|
226
223
|
|
|
227
224
|
if (isGithubActions) {
|
|
228
225
|
return dep
|
|
@@ -231,40 +228,23 @@ await cmd`sync environment variables from package.json to matching files`
|
|
|
231
228
|
}
|
|
232
229
|
|
|
233
230
|
return isMap
|
|
234
|
-
? `${indent}${key}: \${${key}:-${
|
|
235
|
-
: `${indent}- ${key}=\${${key}:-${
|
|
231
|
+
? `${indent}${key}: \${${key}:-${val}}`
|
|
232
|
+
: `${indent}- ${key}=\${${key}:-${val}}`
|
|
236
233
|
})
|
|
237
234
|
.join('\n')
|
|
238
235
|
|
|
239
236
|
return replaceMarkerSection(content, yamlStart, yamlEnd, envLines)
|
|
240
237
|
}
|
|
241
238
|
|
|
242
|
-
function applyJsMarkers(_filePath: string, content: string): string {
|
|
243
|
-
const envExports = Object.entries(envVars)
|
|
244
|
-
.map(([key, value]) => {
|
|
245
|
-
if (typeof value === 'string' && value.startsWith('$dep:')) {
|
|
246
|
-
return `export const ${key} = ensureEnv('${key}')`
|
|
247
|
-
}
|
|
248
|
-
return `export const ${key} = ensureEnv('${key}'${typeof value === 'string' ? `, ${JSON.stringify(value)}` : ''})`
|
|
249
|
-
})
|
|
250
|
-
.join('\n')
|
|
251
|
-
|
|
252
|
-
return replaceMarkerSection(content, jsStart, jsEnd, envExports)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
239
|
function applyDotenvSection(_filePath: string, content: string): string {
|
|
256
240
|
const lines: string[] = [dotenvSectionStart]
|
|
257
241
|
|
|
258
|
-
for (const
|
|
259
|
-
const dep =
|
|
242
|
+
for (const { key, required, defaultValue } of entries) {
|
|
243
|
+
const dep = depResolved[key]
|
|
260
244
|
if (dep) {
|
|
261
245
|
lines.push(`${key}=${dep}`)
|
|
262
|
-
} else if (
|
|
263
|
-
|
|
264
|
-
`could not resolve dependency version for ${value.slice('$dep:'.length)}`,
|
|
265
|
-
)
|
|
266
|
-
} else if (typeof value === 'string' && value !== '') {
|
|
267
|
-
lines.push(`${key}=${value}`)
|
|
246
|
+
} else if (!required && defaultValue !== '') {
|
|
247
|
+
lines.push(`${key}=${defaultValue}`)
|
|
268
248
|
}
|
|
269
249
|
}
|
|
270
250
|
|
|
@@ -297,24 +277,10 @@ await cmd`sync environment variables from package.json to matching files`
|
|
|
297
277
|
return result
|
|
298
278
|
}
|
|
299
279
|
|
|
300
|
-
function applyTsInline(_filePath: string, content: string): string {
|
|
301
|
-
let result = content
|
|
302
|
-
for (const [key, value] of Object.entries(depResolved)) {
|
|
303
|
-
const re = new RegExp(`(${key}:\\s*)(['"])([^'"]+)\\2`, 'g')
|
|
304
|
-
result = result.replace(re, (_match, prefix, quote, oldValue) => {
|
|
305
|
-
if (oldValue !== value) return `${prefix}${quote}${value}${quote}`
|
|
306
|
-
return _match
|
|
307
|
-
})
|
|
308
|
-
}
|
|
309
|
-
return result
|
|
310
|
-
}
|
|
311
|
-
|
|
312
280
|
const strategies: Record<Strategy, (filePath: string, content: string) => string> = {
|
|
313
281
|
'yaml-markers': applyYamlMarkers,
|
|
314
|
-
'js-markers': applyJsMarkers,
|
|
315
282
|
'dotenv-section': applyDotenvSection,
|
|
316
283
|
'dotenv-inline': applyDotenvInline,
|
|
317
|
-
'ts-inline': applyTsInline,
|
|
318
284
|
}
|
|
319
285
|
|
|
320
286
|
let updated = 0
|
|
@@ -56,3 +56,53 @@ export async function releaseDeployLock(
|
|
|
56
56
|
// best-effort, lock will auto-expire
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* run `fn` with the deploy lock held, releasing on ANY exit path —
|
|
62
|
+
* normal return, thrown error, or process signal (SIGTERM from CI cancel,
|
|
63
|
+
* SIGINT from ctrl-c, SIGHUP from terminal close).
|
|
64
|
+
*
|
|
65
|
+
* prefer this over raw acquire/release: a bare try/finally does NOT catch
|
|
66
|
+
* signals, so a CI-cancelled deploy would leak the lock for 15 minutes and
|
|
67
|
+
* block the next run. this helper installs signal handlers that release
|
|
68
|
+
* before exit, then removes them once `fn` completes.
|
|
69
|
+
*/
|
|
70
|
+
export async function withDeployLock<T>(
|
|
71
|
+
ssh: string,
|
|
72
|
+
fn: () => Promise<T>,
|
|
73
|
+
opts?: DeployLockOptions,
|
|
74
|
+
): Promise<T> {
|
|
75
|
+
await acquireDeployLock(ssh, opts)
|
|
76
|
+
|
|
77
|
+
let released = false
|
|
78
|
+
const release = async () => {
|
|
79
|
+
if (released) return
|
|
80
|
+
released = true
|
|
81
|
+
await releaseDeployLock(ssh, opts)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// signal handlers — fire-and-forget release with a safety timeout so the
|
|
85
|
+
// process always exits even if the ssh release hangs
|
|
86
|
+
const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGHUP']
|
|
87
|
+
const handlers = new Map<NodeJS.Signals, () => void>()
|
|
88
|
+
for (const sig of signals) {
|
|
89
|
+
const handler = () => {
|
|
90
|
+
const done = release().catch(() => {})
|
|
91
|
+
const timeout = new Promise<void>((r) => setTimeout(r, 5_000))
|
|
92
|
+
Promise.race([done, timeout]).finally(() => {
|
|
93
|
+
process.exit(sig === 'SIGINT' ? 130 : 143)
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
handlers.set(sig, handler)
|
|
97
|
+
process.once(sig, handler)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
return await fn()
|
|
102
|
+
} finally {
|
|
103
|
+
for (const [sig, handler] of handlers) {
|
|
104
|
+
process.off(sig, handler)
|
|
105
|
+
}
|
|
106
|
+
await release()
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/helpers/env-load.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
1
|
import { join } from 'node:path'
|
|
3
2
|
|
|
4
3
|
export async function loadEnv(
|
|
5
4
|
environment: 'development' | 'production' | 'dev-prod',
|
|
6
|
-
options?: { optional?: string[]
|
|
5
|
+
options?: { optional?: string[] },
|
|
7
6
|
) {
|
|
8
7
|
const { loadEnv: vxrnLoadEnv } = await import('vxrn/loadEnv')
|
|
9
8
|
|
|
10
9
|
// loads env into process.env
|
|
11
10
|
await vxrnLoadEnv(environment)
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const Environment =
|
|
12
|
+
// import src/env.ts to get the env config applied (side effect: populates process.env)
|
|
13
|
+
const envModule = await import(join(process.cwd(), 'src/env.ts'))
|
|
14
|
+
const Environment = envModule.server || {}
|
|
16
15
|
|
|
17
16
|
// validate
|
|
18
17
|
for (const key in Environment) {
|
|
@@ -26,15 +25,3 @@ export async function loadEnv(
|
|
|
26
25
|
|
|
27
26
|
return Environment
|
|
28
27
|
}
|
|
29
|
-
|
|
30
|
-
function resolveEnvServerPath(): string {
|
|
31
|
-
const candidates = ['src/constants/env-server.ts', 'src/server/env-server.ts']
|
|
32
|
-
for (const candidate of candidates) {
|
|
33
|
-
const full = join(process.cwd(), candidate)
|
|
34
|
-
if (fs.existsSync(full)) {
|
|
35
|
-
return full
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
// fall back to first candidate for error message
|
|
39
|
-
return join(process.cwd(), 'src/constants/env-server.ts')
|
|
40
|
-
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
1
|
import { join } from 'node:path'
|
|
3
2
|
|
|
4
3
|
import { loadEnv } from './env-load'
|
|
@@ -7,12 +6,10 @@ import { getDockerHost } from './get-docker-host'
|
|
|
7
6
|
export async function getTestEnv() {
|
|
8
7
|
const dockerHost = getDockerHost()
|
|
9
8
|
const devEnv = await loadEnv('development')
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
join(process.cwd(), 'src/constants/env-server')
|
|
15
|
-
const serverEnvFallback = await import(envPath)
|
|
9
|
+
|
|
10
|
+
// import src/env.ts for server env fallback values
|
|
11
|
+
const envModule = await import(join(process.cwd(), 'src/env.ts'))
|
|
12
|
+
const serverEnvFallback = envModule.server || {}
|
|
16
13
|
|
|
17
14
|
// ports come from VITE_PORT_* in .env.development (loaded by loadEnv)
|
|
18
15
|
const dbPort = process.env.VITE_PORT_POSTGRES || '5433'
|