@wyxos/zephyr 0.3.3 → 0.3.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/package.json
CHANGED
|
@@ -3,6 +3,113 @@ import {planLaravelDeploymentTasks} from './plan-laravel-deployment-tasks.mjs'
|
|
|
3
3
|
|
|
4
4
|
const PRERENDERED_MAINTENANCE_VIEW = 'errors::503'
|
|
5
5
|
const PRERENDERED_MAINTENANCE_FILE = 'resources/views/errors/503.blade.php'
|
|
6
|
+
const LARAVEL_WRITABLE_PATHS = [
|
|
7
|
+
'bootstrap/cache',
|
|
8
|
+
'storage/framework/cache',
|
|
9
|
+
'storage/framework/views',
|
|
10
|
+
'storage/framework/sessions'
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
function escapeForSingleQuotes(value) {
|
|
14
|
+
return value.replace(/'/g, "'\\''")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isGroupWritable(mode) {
|
|
18
|
+
if (typeof mode !== 'string' || mode.length < 2) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const groupDigit = mode.at(-2)
|
|
23
|
+
const parsed = Number.parseInt(groupDigit, 8)
|
|
24
|
+
return Number.isInteger(parsed) && (parsed & 2) === 2
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shouldInspectLaravelWritablePaths(steps = []) {
|
|
28
|
+
return steps.some((step) => step.label === 'Clear Laravel caches')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function inspectLaravelWritablePath(ssh, remoteCwd, relativePath) {
|
|
32
|
+
const escapedPath = escapeForSingleQuotes(relativePath)
|
|
33
|
+
const command = [
|
|
34
|
+
`if [ ! -e '${escapedPath}' ]; then`,
|
|
35
|
+
' printf "__MISSING__";',
|
|
36
|
+
'else',
|
|
37
|
+
` WRITABLE="no"; [ -w '${escapedPath}' ] && WRITABLE="yes";`,
|
|
38
|
+
` OWNER=$(stat -c '%U' '${escapedPath}' 2>/dev/null || printf '?');`,
|
|
39
|
+
` GROUP=$(stat -c '%G' '${escapedPath}' 2>/dev/null || printf '?');`,
|
|
40
|
+
` MODE=$(stat -c '%a' '${escapedPath}' 2>/dev/null || printf '?');`,
|
|
41
|
+
' printf "%s|%s|%s|%s" "$WRITABLE" "$OWNER" "$GROUP" "$MODE";',
|
|
42
|
+
'fi'
|
|
43
|
+
].join(' ')
|
|
44
|
+
|
|
45
|
+
const result = await ssh.execCommand(command, {cwd: remoteCwd})
|
|
46
|
+
const output = result.stdout.trim()
|
|
47
|
+
|
|
48
|
+
if (output === '__MISSING__') {
|
|
49
|
+
return {
|
|
50
|
+
path: relativePath,
|
|
51
|
+
exists: false,
|
|
52
|
+
writable: false,
|
|
53
|
+
owner: null,
|
|
54
|
+
group: null,
|
|
55
|
+
mode: null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const [writableFlag, owner = '?', group = '?', mode = '?'] = output.split('|')
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
path: relativePath,
|
|
63
|
+
exists: true,
|
|
64
|
+
writable: writableFlag === 'yes',
|
|
65
|
+
owner,
|
|
66
|
+
group,
|
|
67
|
+
mode
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function validateLaravelWritablePaths({
|
|
72
|
+
ssh,
|
|
73
|
+
remoteCwd,
|
|
74
|
+
sshUser,
|
|
75
|
+
steps,
|
|
76
|
+
logProcessing,
|
|
77
|
+
logWarning
|
|
78
|
+
} = {}) {
|
|
79
|
+
if (!shouldInspectLaravelWritablePaths(steps)) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
logProcessing?.(`Checking Laravel writable directories for deploy user ${sshUser || 'current SSH user'}...`)
|
|
84
|
+
|
|
85
|
+
const inspections = []
|
|
86
|
+
for (const relativePath of LARAVEL_WRITABLE_PATHS) {
|
|
87
|
+
inspections.push(await inspectLaravelWritablePath(ssh, remoteCwd, relativePath))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const blockedPaths = inspections.filter((inspection) => inspection.exists && !inspection.writable)
|
|
91
|
+
if (blockedPaths.length > 0) {
|
|
92
|
+
const details = blockedPaths
|
|
93
|
+
.map((inspection) => ` - ${inspection.path} (owner ${inspection.owner}:${inspection.group}, mode ${inspection.mode})`)
|
|
94
|
+
.join('\n')
|
|
95
|
+
|
|
96
|
+
throw new Error(
|
|
97
|
+
'Laravel cache-related deployment tasks cannot run because the SSH deploy user cannot write to required directories:\n' +
|
|
98
|
+
`${details}\n` +
|
|
99
|
+
'Fix permissions before releasing. Typical fix:\n' +
|
|
100
|
+
'sudo chown -R $USER:www-data bootstrap/cache storage/framework/cache storage/framework/views storage/framework/sessions\n' +
|
|
101
|
+
'sudo chmod -R ug+rwX bootstrap/cache storage/framework/cache storage/framework/views storage/framework/sessions'
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const riskyPaths = inspections.filter((inspection) => inspection.exists && inspection.writable && !isGroupWritable(inspection.mode))
|
|
106
|
+
for (const inspection of riskyPaths) {
|
|
107
|
+
logWarning?.(
|
|
108
|
+
`${inspection.path} is writable by the deploy user (${inspection.owner}:${inspection.group}, mode ${inspection.mode}), ` +
|
|
109
|
+
'but it is not group-writable. Web-created cache files may cause later permission drift.'
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
6
113
|
|
|
7
114
|
async function detectRemoteLaravelProject(ssh, remoteCwd) {
|
|
8
115
|
const laravelCheck = await ssh.execCommand(
|
|
@@ -290,6 +397,15 @@ export async function buildRemoteDeploymentPlan({
|
|
|
290
397
|
maintenanceUpCommand: maintenanceModePlan.upCommand
|
|
291
398
|
})
|
|
292
399
|
|
|
400
|
+
await validateLaravelWritablePaths({
|
|
401
|
+
ssh,
|
|
402
|
+
remoteCwd,
|
|
403
|
+
sshUser: config.sshUser,
|
|
404
|
+
steps,
|
|
405
|
+
logProcessing,
|
|
406
|
+
logWarning
|
|
407
|
+
})
|
|
408
|
+
|
|
293
409
|
const usefulSteps = steps.length > 1
|
|
294
410
|
const pendingSnapshot = !usefulSteps
|
|
295
411
|
? null
|
|
@@ -320,4 +436,4 @@ export async function buildRemoteDeploymentPlan({
|
|
|
320
436
|
usefulSteps,
|
|
321
437
|
pendingSnapshot
|
|
322
438
|
}
|
|
323
|
-
}
|
|
439
|
+
}
|