@wyxos/zephyr 0.2.27 → 0.3.0
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/README.md +55 -2
- package/bin/zephyr.mjs +3 -1
- package/package.json +7 -2
- package/src/application/configuration/app-details.mjs +89 -0
- package/src/application/configuration/app-selection.mjs +87 -0
- package/src/application/configuration/preset-selection.mjs +59 -0
- package/src/application/configuration/select-deployment-target.mjs +165 -0
- package/src/application/configuration/server-selection.mjs +87 -0
- package/src/application/configuration/service.mjs +109 -0
- package/src/application/deploy/build-remote-deployment-plan.mjs +174 -0
- package/src/application/deploy/bump-local-package-version.mjs +81 -0
- package/src/application/deploy/execute-remote-deployment-plan.mjs +61 -0
- package/src/{utils/task-planner.mjs → application/deploy/plan-laravel-deployment-tasks.mjs} +5 -4
- package/src/application/deploy/prepare-local-deployment.mjs +52 -0
- package/src/application/deploy/resolve-local-deployment-context.mjs +17 -0
- package/src/application/deploy/resolve-pending-snapshot.mjs +45 -0
- package/src/application/deploy/run-deployment.mjs +147 -0
- package/src/application/deploy/run-local-deployment-checks.mjs +80 -0
- package/src/application/release/release-node-package.mjs +340 -0
- package/src/application/release/release-packagist-package.mjs +223 -0
- package/src/config/project.mjs +13 -0
- package/src/deploy/local-repo.mjs +187 -67
- package/src/deploy/preflight.mjs +10 -1
- package/src/deploy/remote-exec.mjs +2 -3
- package/src/index.mjs +27 -85
- package/src/main.mjs +80 -627
- package/src/release/shared.mjs +104 -0
- package/src/release-node.mjs +20 -481
- package/src/release-packagist.mjs +20 -291
- package/src/runtime/app-context.mjs +36 -0
- package/src/targets/index.mjs +24 -0
- package/src/utils/command.mjs +67 -5
- package/src/utils/output.mjs +41 -16
- package/src/utils/config-flow.mjs +0 -284
- /package/src/{utils/php-version.mjs → infrastructure/php/version.mjs} +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureSshDetails as ensureSshDetailsBase,
|
|
3
|
+
promptSshDetails as promptSshDetailsBase
|
|
4
|
+
} from '../../ssh/keys.mjs'
|
|
5
|
+
import {saveProjectConfig} from '../../config/project.mjs'
|
|
6
|
+
import {saveServers} from '../../config/servers.mjs'
|
|
7
|
+
import {generateId} from '../../utils/id.mjs'
|
|
8
|
+
import {defaultProjectPath, listGitBranches, promptAppDetails} from './app-details.mjs'
|
|
9
|
+
import {selectApp} from './app-selection.mjs'
|
|
10
|
+
import {selectPreset} from './preset-selection.mjs'
|
|
11
|
+
import {promptServerDetails, selectServer} from './server-selection.mjs'
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
defaultProjectPath,
|
|
15
|
+
listGitBranches,
|
|
16
|
+
promptAppDetails,
|
|
17
|
+
promptServerDetails,
|
|
18
|
+
selectServer,
|
|
19
|
+
selectApp,
|
|
20
|
+
selectPreset
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assertConfigurationDeps({
|
|
24
|
+
runPrompt,
|
|
25
|
+
runCommandCapture,
|
|
26
|
+
logProcessing,
|
|
27
|
+
logSuccess,
|
|
28
|
+
logWarning
|
|
29
|
+
} = {}) {
|
|
30
|
+
if (!runPrompt || !runCommandCapture || !logProcessing || !logSuccess || !logWarning) {
|
|
31
|
+
throw new Error('createConfigurationService requires prompt, command, and logger dependencies.')
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createConfigurationService(deps = {}) {
|
|
36
|
+
assertConfigurationDeps(deps)
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
runPrompt,
|
|
40
|
+
runCommandCapture,
|
|
41
|
+
logProcessing,
|
|
42
|
+
logSuccess,
|
|
43
|
+
logWarning
|
|
44
|
+
} = deps
|
|
45
|
+
|
|
46
|
+
const listBranches = (currentDir) => listGitBranches({
|
|
47
|
+
currentDir,
|
|
48
|
+
runCommandCapture,
|
|
49
|
+
logWarning
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const promptSshDetails = (currentDir, existing = {}) => promptSshDetailsBase(currentDir, existing, {
|
|
53
|
+
runPrompt
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const promptServerDetailsBound = (existingServers = []) => promptServerDetails({
|
|
57
|
+
existingServers,
|
|
58
|
+
runPrompt
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const promptAppDetailsBound = (currentDir, existing = {}) => promptAppDetails({
|
|
62
|
+
currentDir,
|
|
63
|
+
existing,
|
|
64
|
+
runPrompt,
|
|
65
|
+
listGitBranches: listBranches,
|
|
66
|
+
resolveDefaultProjectPath: defaultProjectPath,
|
|
67
|
+
promptSshDetails
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
ensureSshDetails(config, currentDir) {
|
|
72
|
+
return ensureSshDetailsBase(config, currentDir, {runPrompt, logProcessing})
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
selectServer(servers) {
|
|
76
|
+
return selectServer({
|
|
77
|
+
servers,
|
|
78
|
+
runPrompt,
|
|
79
|
+
logProcessing,
|
|
80
|
+
logSuccess,
|
|
81
|
+
persistServers: saveServers,
|
|
82
|
+
promptServerDetails: promptServerDetailsBound
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
selectApp(projectConfig, server, currentDir) {
|
|
87
|
+
return selectApp({
|
|
88
|
+
projectConfig,
|
|
89
|
+
server,
|
|
90
|
+
currentDir,
|
|
91
|
+
runPrompt,
|
|
92
|
+
logWarning,
|
|
93
|
+
logProcessing,
|
|
94
|
+
logSuccess,
|
|
95
|
+
persistProjectConfig: saveProjectConfig,
|
|
96
|
+
createId: generateId,
|
|
97
|
+
promptAppDetails: promptAppDetailsBound
|
|
98
|
+
})
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
selectPreset(projectConfig, servers) {
|
|
102
|
+
return selectPreset({
|
|
103
|
+
projectConfig,
|
|
104
|
+
servers,
|
|
105
|
+
runPrompt
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import {findPhpBinary} from '../../infrastructure/php/version.mjs'
|
|
2
|
+
import {planLaravelDeploymentTasks} from './plan-laravel-deployment-tasks.mjs'
|
|
3
|
+
|
|
4
|
+
async function detectRemoteLaravelProject(ssh, remoteCwd) {
|
|
5
|
+
const laravelCheck = await ssh.execCommand(
|
|
6
|
+
'if [ -f artisan ] && [ -f composer.json ] && grep -q "laravel/framework" composer.json; then echo "yes"; else echo "no"; fi',
|
|
7
|
+
{cwd: remoteCwd}
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
return laravelCheck.stdout.trim() === 'yes'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function collectChangedFiles({
|
|
14
|
+
config,
|
|
15
|
+
snapshot,
|
|
16
|
+
remoteIsLaravel,
|
|
17
|
+
executeRemote,
|
|
18
|
+
logProcessing
|
|
19
|
+
} = {}) {
|
|
20
|
+
if (snapshot?.changedFiles) {
|
|
21
|
+
logProcessing?.('Resuming deployment with saved task snapshot.')
|
|
22
|
+
return snapshot.changedFiles
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!remoteIsLaravel) {
|
|
26
|
+
return []
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await executeRemote(`Fetch latest changes for ${config.branch}`, `git fetch origin ${config.branch}`)
|
|
30
|
+
|
|
31
|
+
const diffResult = await executeRemote(
|
|
32
|
+
'Inspect pending changes',
|
|
33
|
+
`git diff --name-only HEAD..origin/${config.branch}`,
|
|
34
|
+
{printStdout: false}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const changedFiles = diffResult.stdout
|
|
38
|
+
.split(/\r?\n/)
|
|
39
|
+
.map((line) => line.trim())
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
|
|
42
|
+
if (changedFiles.length > 0) {
|
|
43
|
+
const preview = changedFiles
|
|
44
|
+
.slice(0, 20)
|
|
45
|
+
.map((file) => ` - ${file}`)
|
|
46
|
+
.join('\n')
|
|
47
|
+
|
|
48
|
+
logProcessing?.(
|
|
49
|
+
`Detected ${changedFiles.length} changed file(s):\n${preview}${changedFiles.length > 20 ? '\n - ...' : ''}`
|
|
50
|
+
)
|
|
51
|
+
} else {
|
|
52
|
+
logProcessing?.('No upstream file changes detected.')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return changedFiles
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function detectHorizonConfiguration({
|
|
59
|
+
ssh,
|
|
60
|
+
remoteCwd,
|
|
61
|
+
remoteIsLaravel,
|
|
62
|
+
changedFiles
|
|
63
|
+
} = {}) {
|
|
64
|
+
const hasPhpChanges = remoteIsLaravel && changedFiles.some((file) => file.endsWith('.php'))
|
|
65
|
+
|
|
66
|
+
if (!hasPhpChanges) {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const horizonCheck = await ssh.execCommand(
|
|
71
|
+
'if [ -f config/horizon.php ]; then echo "yes"; else echo "no"; fi',
|
|
72
|
+
{cwd: remoteCwd}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return horizonCheck.stdout.trim() === 'yes'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function resolvePhpCommand({
|
|
79
|
+
requiredPhpVersion,
|
|
80
|
+
ssh,
|
|
81
|
+
remoteCwd,
|
|
82
|
+
logProcessing,
|
|
83
|
+
logWarning
|
|
84
|
+
} = {}) {
|
|
85
|
+
if (!requiredPhpVersion) {
|
|
86
|
+
return 'php'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const phpCommand = await findPhpBinary(ssh, remoteCwd, requiredPhpVersion)
|
|
91
|
+
if (phpCommand !== 'php') {
|
|
92
|
+
logProcessing?.(`Detected PHP requirement: ${requiredPhpVersion}, using ${phpCommand}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return phpCommand
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logWarning?.(`Could not find PHP binary for version ${requiredPhpVersion}: ${error.message}`)
|
|
98
|
+
return 'php'
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function buildRemoteDeploymentPlan({
|
|
103
|
+
config,
|
|
104
|
+
snapshot = null,
|
|
105
|
+
requiredPhpVersion = null,
|
|
106
|
+
ssh,
|
|
107
|
+
remoteCwd,
|
|
108
|
+
executeRemote,
|
|
109
|
+
logProcessing,
|
|
110
|
+
logSuccess,
|
|
111
|
+
logWarning
|
|
112
|
+
} = {}) {
|
|
113
|
+
const remoteIsLaravel = await detectRemoteLaravelProject(ssh, remoteCwd)
|
|
114
|
+
|
|
115
|
+
if (remoteIsLaravel) {
|
|
116
|
+
logSuccess?.('Laravel project detected.')
|
|
117
|
+
} else {
|
|
118
|
+
logWarning?.('Laravel project not detected; skipping Laravel-specific maintenance tasks.')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const changedFiles = await collectChangedFiles({
|
|
122
|
+
config,
|
|
123
|
+
snapshot,
|
|
124
|
+
remoteIsLaravel,
|
|
125
|
+
executeRemote,
|
|
126
|
+
logProcessing
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const horizonConfigured = await detectHorizonConfiguration({
|
|
130
|
+
ssh,
|
|
131
|
+
remoteCwd,
|
|
132
|
+
remoteIsLaravel,
|
|
133
|
+
changedFiles
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const phpCommand = await resolvePhpCommand({
|
|
137
|
+
requiredPhpVersion,
|
|
138
|
+
ssh,
|
|
139
|
+
remoteCwd,
|
|
140
|
+
logProcessing,
|
|
141
|
+
logWarning
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const steps = planLaravelDeploymentTasks({
|
|
145
|
+
branch: config.branch,
|
|
146
|
+
isLaravel: remoteIsLaravel,
|
|
147
|
+
changedFiles,
|
|
148
|
+
horizonConfigured,
|
|
149
|
+
phpCommand
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const usefulSteps = steps.length > 1
|
|
153
|
+
const pendingSnapshot = !usefulSteps
|
|
154
|
+
? null
|
|
155
|
+
: snapshot ?? {
|
|
156
|
+
serverName: config.serverName,
|
|
157
|
+
branch: config.branch,
|
|
158
|
+
projectPath: config.projectPath,
|
|
159
|
+
sshUser: config.sshUser,
|
|
160
|
+
createdAt: new Date().toISOString(),
|
|
161
|
+
changedFiles,
|
|
162
|
+
taskLabels: steps.map((step) => step.label)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
remoteIsLaravel,
|
|
167
|
+
changedFiles,
|
|
168
|
+
horizonConfigured,
|
|
169
|
+
phpCommand,
|
|
170
|
+
steps,
|
|
171
|
+
usefulSteps,
|
|
172
|
+
pendingSnapshot
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {commandExists} from '../../utils/command.mjs'
|
|
5
|
+
|
|
6
|
+
async function readPackageJson(rootDir) {
|
|
7
|
+
const packageJsonPath = path.join(rootDir, 'package.json')
|
|
8
|
+
const raw = await fs.readFile(packageJsonPath, 'utf8')
|
|
9
|
+
return JSON.parse(raw)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function isGitIgnored(rootDir, filePath, {runCommand} = {}) {
|
|
13
|
+
try {
|
|
14
|
+
await runCommand('git', ['check-ignore', '-q', filePath], {cwd: rootDir})
|
|
15
|
+
return true
|
|
16
|
+
} catch {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function bumpLocalPackageVersion(rootDir, {
|
|
22
|
+
versionArg = null,
|
|
23
|
+
runCommand,
|
|
24
|
+
logProcessing,
|
|
25
|
+
logSuccess,
|
|
26
|
+
logWarning
|
|
27
|
+
} = {}) {
|
|
28
|
+
if (!commandExists('npm')) {
|
|
29
|
+
logWarning?.('npm is not available in PATH. Skipping npm version bump.')
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let pkg
|
|
34
|
+
try {
|
|
35
|
+
pkg = await readPackageJson(rootDir)
|
|
36
|
+
} catch {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!pkg?.version) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const releaseValue = (versionArg && String(versionArg).trim().length > 0)
|
|
45
|
+
? String(versionArg).trim()
|
|
46
|
+
: 'patch'
|
|
47
|
+
|
|
48
|
+
logProcessing?.(`Bumping npm package version (${releaseValue})...`)
|
|
49
|
+
await runCommand('npm', ['version', releaseValue, '--no-git-tag-version', '--force'], {cwd: rootDir})
|
|
50
|
+
|
|
51
|
+
const updatedPkg = await readPackageJson(rootDir)
|
|
52
|
+
const nextVersion = updatedPkg?.version ?? pkg.version
|
|
53
|
+
const didVersionChange = nextVersion !== pkg.version
|
|
54
|
+
|
|
55
|
+
const filesToStage = ['package.json']
|
|
56
|
+
const packageLockPath = path.join(rootDir, 'package-lock.json')
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await fs.access(packageLockPath)
|
|
60
|
+
const ignored = await isGitIgnored(rootDir, 'package-lock.json', {runCommand})
|
|
61
|
+
if (!ignored) {
|
|
62
|
+
filesToStage.push('package-lock.json')
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// package-lock.json does not exist
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await runCommand('git', ['add', ...filesToStage], {cwd: rootDir})
|
|
69
|
+
|
|
70
|
+
if (!didVersionChange) {
|
|
71
|
+
logWarning?.('Version did not change after npm version. Skipping version commit.')
|
|
72
|
+
return updatedPkg
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await runCommand('git', ['commit', '-m', `chore: bump version to ${nextVersion}`, '--', ...filesToStage], {
|
|
76
|
+
cwd: rootDir
|
|
77
|
+
})
|
|
78
|
+
logSuccess?.(`Version updated to ${nextVersion}.`)
|
|
79
|
+
|
|
80
|
+
return updatedPkg
|
|
81
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {clearPendingTasksSnapshot, savePendingTasksSnapshot} from '../../deploy/snapshots.mjs'
|
|
2
|
+
import {PENDING_TASKS_FILE} from '../../utils/paths.mjs'
|
|
3
|
+
|
|
4
|
+
async function persistPendingSnapshot(rootDir, pendingSnapshot, executeRemote) {
|
|
5
|
+
await savePendingTasksSnapshot(rootDir, pendingSnapshot)
|
|
6
|
+
|
|
7
|
+
const payload = Buffer.from(JSON.stringify(pendingSnapshot)).toString('base64')
|
|
8
|
+
await executeRemote(
|
|
9
|
+
'Record pending deployment tasks',
|
|
10
|
+
`mkdir -p .zephyr && echo '${payload}' | base64 --decode > .zephyr/${PENDING_TASKS_FILE}`,
|
|
11
|
+
{printStdout: false}
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function logScheduledTasks(steps, {logProcessing} = {}) {
|
|
16
|
+
if (steps.length === 1) {
|
|
17
|
+
logProcessing?.('No additional maintenance tasks scheduled beyond git pull.')
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const extraTasks = steps
|
|
22
|
+
.slice(1)
|
|
23
|
+
.map((step) => step.label)
|
|
24
|
+
.join(', ')
|
|
25
|
+
|
|
26
|
+
logProcessing?.(`Additional tasks scheduled: ${extraTasks}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function executeRemoteDeploymentPlan({
|
|
30
|
+
rootDir,
|
|
31
|
+
executeRemote,
|
|
32
|
+
steps,
|
|
33
|
+
usefulSteps,
|
|
34
|
+
pendingSnapshot = null,
|
|
35
|
+
logProcessing
|
|
36
|
+
} = {}) {
|
|
37
|
+
if (usefulSteps && pendingSnapshot) {
|
|
38
|
+
await persistPendingSnapshot(rootDir, pendingSnapshot, executeRemote)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
logScheduledTasks(steps, {logProcessing})
|
|
42
|
+
|
|
43
|
+
let completed = false
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
for (const step of steps) {
|
|
47
|
+
await executeRemote(step.label, step.command)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
completed = true
|
|
51
|
+
} finally {
|
|
52
|
+
if (usefulSteps && completed) {
|
|
53
|
+
await executeRemote(
|
|
54
|
+
'Clear pending deployment snapshot',
|
|
55
|
+
`rm -f .zephyr/${PENDING_TASKS_FILE}`,
|
|
56
|
+
{printStdout: false, allowFailure: true}
|
|
57
|
+
)
|
|
58
|
+
await clearPendingTasksSnapshot(rootDir)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -52,10 +52,12 @@ export function planLaravelDeploymentTasks({
|
|
|
52
52
|
|
|
53
53
|
if (shouldRunComposer) {
|
|
54
54
|
// Composer is a PHP script, so we need to run it with the correct PHP version
|
|
55
|
-
//
|
|
55
|
+
// Deployments should be lockfile-based and reproducible.
|
|
56
|
+
// `composer update --no-dev` still resolves require-dev and can fail on production PHP versions.
|
|
57
|
+
// Prefer `composer install --no-dev` and fail loudly if composer.lock is missing.
|
|
56
58
|
steps.push({
|
|
57
|
-
label: '
|
|
58
|
-
command: `if [ -f composer.phar ]; then ${phpCommand} composer.phar
|
|
59
|
+
label: 'Install Composer dependencies',
|
|
60
|
+
command: `if [ ! -f composer.lock ]; then echo "composer.lock is missing; commit composer.lock for reproducible deploys." >&2; exit 1; fi; if [ -f composer.phar ]; then ${phpCommand} composer.phar install --no-dev --no-interaction --prefer-dist --optimize-autoloader; elif command -v composer >/dev/null 2>&1; then ${phpCommand} $(command -v composer) install --no-dev --no-interaction --prefer-dist --optimize-autoloader; else ${phpCommand} composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader; fi`
|
|
59
61
|
})
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -96,4 +98,3 @@ export function planLaravelDeploymentTasks({
|
|
|
96
98
|
|
|
97
99
|
return steps
|
|
98
100
|
}
|
|
99
|
-
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import {ensureLocalRepositoryState} from '../../deploy/local-repo.mjs'
|
|
4
|
+
import {bumpLocalPackageVersion} from './bump-local-package-version.mjs'
|
|
5
|
+
import {resolveLocalDeploymentContext} from './resolve-local-deployment-context.mjs'
|
|
6
|
+
import {runLocalDeploymentChecks} from './run-local-deployment-checks.mjs'
|
|
7
|
+
|
|
8
|
+
export async function prepareLocalDeployment(config, {
|
|
9
|
+
snapshot = null,
|
|
10
|
+
rootDir = process.cwd(),
|
|
11
|
+
versionArg = null,
|
|
12
|
+
runPrompt,
|
|
13
|
+
runCommand,
|
|
14
|
+
runCommandCapture,
|
|
15
|
+
logProcessing,
|
|
16
|
+
logSuccess,
|
|
17
|
+
logWarning
|
|
18
|
+
} = {}) {
|
|
19
|
+
const context = await resolveLocalDeploymentContext(rootDir)
|
|
20
|
+
|
|
21
|
+
if (!snapshot && context.isLaravel) {
|
|
22
|
+
await bumpLocalPackageVersion(rootDir, {
|
|
23
|
+
versionArg,
|
|
24
|
+
runCommand,
|
|
25
|
+
logProcessing,
|
|
26
|
+
logSuccess,
|
|
27
|
+
logWarning
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await ensureLocalRepositoryState(config.branch, rootDir, {
|
|
32
|
+
runPrompt,
|
|
33
|
+
runCommand,
|
|
34
|
+
runCommandCapture,
|
|
35
|
+
logProcessing,
|
|
36
|
+
logSuccess,
|
|
37
|
+
logWarning
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
await runLocalDeploymentChecks({
|
|
41
|
+
rootDir,
|
|
42
|
+
isLaravel: context.isLaravel,
|
|
43
|
+
hasHook: context.hasHook,
|
|
44
|
+
runCommand,
|
|
45
|
+
runCommandCapture,
|
|
46
|
+
logProcessing,
|
|
47
|
+
logSuccess,
|
|
48
|
+
logWarning
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return context
|
|
52
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as preflight from '../../deploy/preflight.mjs'
|
|
2
|
+
import {getPhpVersionRequirement} from '../../infrastructure/php/version.mjs'
|
|
3
|
+
|
|
4
|
+
export async function resolveLocalDeploymentContext(rootDir) {
|
|
5
|
+
let requiredPhpVersion = null
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
requiredPhpVersion = await getPhpVersionRequirement(rootDir)
|
|
9
|
+
} catch {
|
|
10
|
+
// composer.json might not exist or be unreadable
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const isLaravel = await preflight.isLocalLaravelProject(rootDir)
|
|
14
|
+
const hasHook = await preflight.hasPrePushHook(rootDir)
|
|
15
|
+
|
|
16
|
+
return {requiredPhpVersion, isLaravel, hasHook}
|
|
17
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {clearPendingTasksSnapshot, loadPendingTasksSnapshot} from '../../deploy/snapshots.mjs'
|
|
2
|
+
|
|
3
|
+
export async function resolvePendingSnapshot(rootDir, deploymentConfig, {
|
|
4
|
+
runPrompt,
|
|
5
|
+
logProcessing,
|
|
6
|
+
logWarning
|
|
7
|
+
} = {}) {
|
|
8
|
+
const existingSnapshot = await loadPendingTasksSnapshot(rootDir)
|
|
9
|
+
|
|
10
|
+
if (!existingSnapshot) {
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const matchesSelection =
|
|
15
|
+
existingSnapshot.serverName === deploymentConfig.serverName &&
|
|
16
|
+
existingSnapshot.branch === deploymentConfig.branch
|
|
17
|
+
|
|
18
|
+
const messageLines = [
|
|
19
|
+
'Pending deployment tasks were detected from a previous run.',
|
|
20
|
+
`Server: ${existingSnapshot.serverName}`,
|
|
21
|
+
`Branch: ${existingSnapshot.branch}`
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
if (existingSnapshot.taskLabels && existingSnapshot.taskLabels.length > 0) {
|
|
25
|
+
messageLines.push(`Tasks: ${existingSnapshot.taskLabels.join(', ')}`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const {resumePendingTasks} = await runPrompt([
|
|
29
|
+
{
|
|
30
|
+
type: 'confirm',
|
|
31
|
+
name: 'resumePendingTasks',
|
|
32
|
+
message: `${messageLines.join(' | ')}. Resume using this plan?`,
|
|
33
|
+
default: matchesSelection
|
|
34
|
+
}
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
if (resumePendingTasks) {
|
|
38
|
+
logProcessing?.('Resuming deployment using saved task snapshot...')
|
|
39
|
+
return existingSnapshot
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await clearPendingTasksSnapshot(rootDir)
|
|
43
|
+
logWarning?.('Discarded pending deployment snapshot.')
|
|
44
|
+
return null
|
|
45
|
+
}
|