@wyxos/zephyr 0.2.21 → 0.2.23
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 +144 -144
- package/bin/zephyr.mjs +29 -29
- package/package.json +58 -58
- package/src/config/project.mjs +118 -118
- package/src/config/servers.mjs +57 -57
- package/src/dependency-scanner.mjs +412 -433
- package/src/deploy/local-repo.mjs +215 -215
- package/src/deploy/locks.mjs +171 -171
- package/src/deploy/preflight.mjs +117 -117
- package/src/deploy/remote-exec.mjs +99 -99
- package/src/deploy/snapshots.mjs +35 -35
- package/src/index.mjs +91 -91
- package/src/main.mjs +677 -652
- package/src/project/bootstrap.mjs +147 -147
- package/src/runtime/local-command.mjs +18 -18
- package/src/runtime/prompt.mjs +14 -14
- package/src/runtime/ssh-client.mjs +14 -14
- package/src/ssh/index.mjs +8 -8
- package/src/ssh/keys.mjs +146 -146
- package/src/ssh/ssh.mjs +134 -134
- package/src/utils/command.mjs +92 -92
- package/src/utils/config-flow.mjs +284 -284
- package/src/utils/git.mjs +91 -91
- package/src/utils/id.mjs +6 -6
- package/src/utils/log-file.mjs +76 -76
- package/src/utils/output.mjs +29 -29
- package/src/utils/paths.mjs +28 -28
- package/src/utils/php-version.mjs +137 -0
- package/src/utils/remote-path.mjs +23 -23
- package/src/utils/task-planner.mjs +99 -96
- package/src/version-checker.mjs +162 -162
|
@@ -1,215 +1,215 @@
|
|
|
1
|
-
import { getCurrentBranch as getCurrentBranchImpl, getUpstreamRef as getUpstreamRefImpl } from '../utils/git.mjs'
|
|
2
|
-
|
|
3
|
-
export async function getCurrentBranch(rootDir) {
|
|
4
|
-
const branch = await getCurrentBranchImpl(rootDir)
|
|
5
|
-
return branch ?? ''
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export async function getGitStatus(rootDir, { runCommandCapture } = {}) {
|
|
9
|
-
const output = await runCommandCapture('git', ['status', '--porcelain'], { cwd: rootDir })
|
|
10
|
-
return output.trim()
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function hasStagedChanges(statusOutput) {
|
|
14
|
-
if (!statusOutput || statusOutput.length === 0) {
|
|
15
|
-
return false
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const lines = statusOutput.split('\n').filter((line) => line.trim().length > 0)
|
|
19
|
-
|
|
20
|
-
return lines.some((line) => {
|
|
21
|
-
const firstChar = line[0]
|
|
22
|
-
return firstChar && firstChar !== ' ' && firstChar !== '?'
|
|
23
|
-
})
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function hasUncommittedChanges(rootDir, { getGitStatus: getGitStatusFn } = {}) {
|
|
27
|
-
const status = await getGitStatusFn(rootDir)
|
|
28
|
-
return status.length > 0
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function getUpstreamRef(rootDir) {
|
|
32
|
-
return await getUpstreamRefImpl(rootDir)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function ensureCommittedChangesPushed(targetBranch, rootDir, {
|
|
36
|
-
runCommand,
|
|
37
|
-
runCommandCapture,
|
|
38
|
-
logProcessing,
|
|
39
|
-
logSuccess,
|
|
40
|
-
logWarning,
|
|
41
|
-
getUpstreamRef: getUpstreamRefFn = getUpstreamRef
|
|
42
|
-
} = {}) {
|
|
43
|
-
const upstreamRef = await getUpstreamRefFn(rootDir)
|
|
44
|
-
|
|
45
|
-
if (!upstreamRef) {
|
|
46
|
-
logWarning?.(`Branch ${targetBranch} does not track a remote upstream; skipping automatic push of committed changes.`)
|
|
47
|
-
return { pushed: false, upstreamRef: null }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const [remoteName, ...upstreamParts] = upstreamRef.split('/')
|
|
51
|
-
const upstreamBranch = upstreamParts.join('/')
|
|
52
|
-
|
|
53
|
-
if (!remoteName || !upstreamBranch) {
|
|
54
|
-
logWarning?.(`Unable to determine remote destination for ${targetBranch}. Skipping automatic push.`)
|
|
55
|
-
return { pushed: false, upstreamRef }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
await runCommand('git', ['fetch', remoteName], { cwd: rootDir, silent: true })
|
|
60
|
-
} catch (error) {
|
|
61
|
-
logWarning?.(`Unable to fetch from ${remoteName} before push: ${error.message}`)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let remoteExists = true
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
await runCommand('git', ['show-ref', '--verify', '--quiet', `refs/remotes/${upstreamRef}`], {
|
|
68
|
-
cwd: rootDir,
|
|
69
|
-
silent: true
|
|
70
|
-
})
|
|
71
|
-
} catch {
|
|
72
|
-
remoteExists = false
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let aheadCount = 0
|
|
76
|
-
let behindCount = 0
|
|
77
|
-
|
|
78
|
-
if (remoteExists) {
|
|
79
|
-
const aheadOutput = await runCommandCapture('git', ['rev-list', '--count', `${upstreamRef}..HEAD`], { cwd: rootDir })
|
|
80
|
-
aheadCount = parseInt(aheadOutput.trim() || '0', 10)
|
|
81
|
-
|
|
82
|
-
const behindOutput = await runCommandCapture('git', ['rev-list', '--count', `HEAD..${upstreamRef}`], { cwd: rootDir })
|
|
83
|
-
behindCount = parseInt(behindOutput.trim() || '0', 10)
|
|
84
|
-
} else {
|
|
85
|
-
aheadCount = 1
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (Number.isFinite(behindCount) && behindCount > 0) {
|
|
89
|
-
throw new Error(
|
|
90
|
-
`Local branch ${targetBranch} is behind ${upstreamRef} by ${behindCount} commit${behindCount === 1 ? '' : 's'}. Pull or rebase before deployment.`
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (!Number.isFinite(aheadCount) || aheadCount <= 0) {
|
|
95
|
-
return { pushed: false, upstreamRef }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const commitLabel = aheadCount === 1 ? 'commit' : 'commits'
|
|
99
|
-
logProcessing?.(`Found ${aheadCount} ${commitLabel} not yet pushed to ${upstreamRef}. Pushing before deployment...`)
|
|
100
|
-
|
|
101
|
-
await runCommandCapture('git', ['push', remoteName, `${targetBranch}:${upstreamBranch}`], { cwd: rootDir })
|
|
102
|
-
logSuccess?.(`Pushed committed changes to ${upstreamRef}.`)
|
|
103
|
-
|
|
104
|
-
return { pushed: true, upstreamRef }
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export async function ensureLocalRepositoryState(targetBranch, rootDir = process.cwd(), {
|
|
108
|
-
runPrompt,
|
|
109
|
-
runCommand,
|
|
110
|
-
runCommandCapture,
|
|
111
|
-
logProcessing,
|
|
112
|
-
logSuccess,
|
|
113
|
-
logWarning,
|
|
114
|
-
getCurrentBranch: getCurrentBranchFn = getCurrentBranch,
|
|
115
|
-
getGitStatus: getGitStatusFn = (dir) => getGitStatus(dir, { runCommandCapture }),
|
|
116
|
-
ensureCommittedChangesPushed: ensureCommittedChangesPushedFn = (branch, dir) =>
|
|
117
|
-
ensureCommittedChangesPushed(branch, dir, {
|
|
118
|
-
runCommand,
|
|
119
|
-
runCommandCapture,
|
|
120
|
-
logProcessing,
|
|
121
|
-
logSuccess,
|
|
122
|
-
logWarning
|
|
123
|
-
})
|
|
124
|
-
} = {}) {
|
|
125
|
-
if (!targetBranch) {
|
|
126
|
-
throw new Error('Deployment branch is not defined in the release configuration.')
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const currentBranch = await getCurrentBranchFn(rootDir)
|
|
130
|
-
|
|
131
|
-
if (!currentBranch) {
|
|
132
|
-
throw new Error('Unable to determine the current git branch. Ensure this is a git repository.')
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const initialStatus = await getGitStatusFn(rootDir)
|
|
136
|
-
const hasPendingChanges = initialStatus.length > 0
|
|
137
|
-
|
|
138
|
-
const statusReport = await runCommandCapture('git', ['status', '--short', '--branch'], { cwd: rootDir })
|
|
139
|
-
const lines = statusReport.split(/\r?\n/)
|
|
140
|
-
const branchLine = lines[0] || ''
|
|
141
|
-
const aheadMatch = branchLine.match(/ahead (\d+)/)
|
|
142
|
-
const behindMatch = branchLine.match(/behind (\d+)/)
|
|
143
|
-
const aheadCount = aheadMatch ? parseInt(aheadMatch[1], 10) : 0
|
|
144
|
-
const behindCount = behindMatch ? parseInt(behindMatch[1], 10) : 0
|
|
145
|
-
|
|
146
|
-
if (aheadCount > 0) {
|
|
147
|
-
logWarning?.(`Local branch ${currentBranch} is ahead of upstream by ${aheadCount} commit${aheadCount === 1 ? '' : 's'}.`)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (behindCount > 0) {
|
|
151
|
-
logProcessing?.(`Synchronizing local branch ${currentBranch} with its upstream...`)
|
|
152
|
-
try {
|
|
153
|
-
await runCommand('git', ['pull', '--ff-only'], { cwd: rootDir })
|
|
154
|
-
logSuccess?.('Local branch fast-forwarded with upstream changes.')
|
|
155
|
-
} catch (error) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
`Unable to fast-forward ${currentBranch} with upstream changes. Resolve conflicts manually, then rerun the deployment.\n${error.message}`
|
|
158
|
-
)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (currentBranch !== targetBranch) {
|
|
163
|
-
if (hasPendingChanges) {
|
|
164
|
-
throw new Error(
|
|
165
|
-
`Local repository has uncommitted changes on ${currentBranch}. Commit or stash them before switching to ${targetBranch}.`
|
|
166
|
-
)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
logProcessing?.(`Switching local repository from ${currentBranch} to ${targetBranch}...`)
|
|
170
|
-
await runCommand('git', ['checkout', targetBranch], { cwd: rootDir })
|
|
171
|
-
logSuccess?.(`Checked out ${targetBranch} locally.`)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const statusAfterCheckout = currentBranch === targetBranch ? initialStatus : await getGitStatusFn(rootDir)
|
|
175
|
-
|
|
176
|
-
if (statusAfterCheckout.length === 0) {
|
|
177
|
-
await ensureCommittedChangesPushedFn(targetBranch, rootDir)
|
|
178
|
-
logProcessing?.('Local repository is clean. Proceeding with deployment.')
|
|
179
|
-
return
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (!hasStagedChanges(statusAfterCheckout)) {
|
|
183
|
-
await ensureCommittedChangesPushedFn(targetBranch, rootDir)
|
|
184
|
-
logProcessing?.('No staged changes detected. Unstaged or untracked files will not affect deployment. Proceeding with deployment.')
|
|
185
|
-
return
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
logWarning?.(`Staged changes detected on ${targetBranch}. A commit is required before deployment.`)
|
|
189
|
-
|
|
190
|
-
const { commitMessage } = await runPrompt([
|
|
191
|
-
{
|
|
192
|
-
type: 'input',
|
|
193
|
-
name: 'commitMessage',
|
|
194
|
-
message: 'Enter a commit message for pending changes before deployment',
|
|
195
|
-
validate: (value) => (value && value.trim().length > 0 ? true : 'Commit message cannot be empty.')
|
|
196
|
-
}
|
|
197
|
-
])
|
|
198
|
-
|
|
199
|
-
const message = commitMessage.trim()
|
|
200
|
-
|
|
201
|
-
logProcessing?.('Committing staged changes before deployment...')
|
|
202
|
-
await runCommand('git', ['commit', '-m', message], { cwd: rootDir })
|
|
203
|
-
await runCommand('git', ['push', 'origin', targetBranch], { cwd: rootDir })
|
|
204
|
-
logSuccess?.(`Committed and pushed changes to origin/${targetBranch}.`)
|
|
205
|
-
|
|
206
|
-
const finalStatus = await getGitStatusFn(rootDir)
|
|
207
|
-
|
|
208
|
-
if (finalStatus.length > 0) {
|
|
209
|
-
throw new Error('Local repository still has uncommitted changes after commit. Aborting deployment.')
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
await ensureCommittedChangesPushedFn(targetBranch, rootDir)
|
|
213
|
-
logProcessing?.('Local repository is clean after committing pending changes.')
|
|
214
|
-
}
|
|
215
|
-
|
|
1
|
+
import { getCurrentBranch as getCurrentBranchImpl, getUpstreamRef as getUpstreamRefImpl } from '../utils/git.mjs'
|
|
2
|
+
|
|
3
|
+
export async function getCurrentBranch(rootDir) {
|
|
4
|
+
const branch = await getCurrentBranchImpl(rootDir)
|
|
5
|
+
return branch ?? ''
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function getGitStatus(rootDir, { runCommandCapture } = {}) {
|
|
9
|
+
const output = await runCommandCapture('git', ['status', '--porcelain'], { cwd: rootDir })
|
|
10
|
+
return output.trim()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function hasStagedChanges(statusOutput) {
|
|
14
|
+
if (!statusOutput || statusOutput.length === 0) {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const lines = statusOutput.split('\n').filter((line) => line.trim().length > 0)
|
|
19
|
+
|
|
20
|
+
return lines.some((line) => {
|
|
21
|
+
const firstChar = line[0]
|
|
22
|
+
return firstChar && firstChar !== ' ' && firstChar !== '?'
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function hasUncommittedChanges(rootDir, { getGitStatus: getGitStatusFn } = {}) {
|
|
27
|
+
const status = await getGitStatusFn(rootDir)
|
|
28
|
+
return status.length > 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getUpstreamRef(rootDir) {
|
|
32
|
+
return await getUpstreamRefImpl(rootDir)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function ensureCommittedChangesPushed(targetBranch, rootDir, {
|
|
36
|
+
runCommand,
|
|
37
|
+
runCommandCapture,
|
|
38
|
+
logProcessing,
|
|
39
|
+
logSuccess,
|
|
40
|
+
logWarning,
|
|
41
|
+
getUpstreamRef: getUpstreamRefFn = getUpstreamRef
|
|
42
|
+
} = {}) {
|
|
43
|
+
const upstreamRef = await getUpstreamRefFn(rootDir)
|
|
44
|
+
|
|
45
|
+
if (!upstreamRef) {
|
|
46
|
+
logWarning?.(`Branch ${targetBranch} does not track a remote upstream; skipping automatic push of committed changes.`)
|
|
47
|
+
return { pushed: false, upstreamRef: null }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const [remoteName, ...upstreamParts] = upstreamRef.split('/')
|
|
51
|
+
const upstreamBranch = upstreamParts.join('/')
|
|
52
|
+
|
|
53
|
+
if (!remoteName || !upstreamBranch) {
|
|
54
|
+
logWarning?.(`Unable to determine remote destination for ${targetBranch}. Skipping automatic push.`)
|
|
55
|
+
return { pushed: false, upstreamRef }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await runCommand('git', ['fetch', remoteName], { cwd: rootDir, silent: true })
|
|
60
|
+
} catch (error) {
|
|
61
|
+
logWarning?.(`Unable to fetch from ${remoteName} before push: ${error.message}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let remoteExists = true
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await runCommand('git', ['show-ref', '--verify', '--quiet', `refs/remotes/${upstreamRef}`], {
|
|
68
|
+
cwd: rootDir,
|
|
69
|
+
silent: true
|
|
70
|
+
})
|
|
71
|
+
} catch {
|
|
72
|
+
remoteExists = false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let aheadCount = 0
|
|
76
|
+
let behindCount = 0
|
|
77
|
+
|
|
78
|
+
if (remoteExists) {
|
|
79
|
+
const aheadOutput = await runCommandCapture('git', ['rev-list', '--count', `${upstreamRef}..HEAD`], { cwd: rootDir })
|
|
80
|
+
aheadCount = parseInt(aheadOutput.trim() || '0', 10)
|
|
81
|
+
|
|
82
|
+
const behindOutput = await runCommandCapture('git', ['rev-list', '--count', `HEAD..${upstreamRef}`], { cwd: rootDir })
|
|
83
|
+
behindCount = parseInt(behindOutput.trim() || '0', 10)
|
|
84
|
+
} else {
|
|
85
|
+
aheadCount = 1
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (Number.isFinite(behindCount) && behindCount > 0) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Local branch ${targetBranch} is behind ${upstreamRef} by ${behindCount} commit${behindCount === 1 ? '' : 's'}. Pull or rebase before deployment.`
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!Number.isFinite(aheadCount) || aheadCount <= 0) {
|
|
95
|
+
return { pushed: false, upstreamRef }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const commitLabel = aheadCount === 1 ? 'commit' : 'commits'
|
|
99
|
+
logProcessing?.(`Found ${aheadCount} ${commitLabel} not yet pushed to ${upstreamRef}. Pushing before deployment...`)
|
|
100
|
+
|
|
101
|
+
await runCommandCapture('git', ['push', remoteName, `${targetBranch}:${upstreamBranch}`], { cwd: rootDir })
|
|
102
|
+
logSuccess?.(`Pushed committed changes to ${upstreamRef}.`)
|
|
103
|
+
|
|
104
|
+
return { pushed: true, upstreamRef }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function ensureLocalRepositoryState(targetBranch, rootDir = process.cwd(), {
|
|
108
|
+
runPrompt,
|
|
109
|
+
runCommand,
|
|
110
|
+
runCommandCapture,
|
|
111
|
+
logProcessing,
|
|
112
|
+
logSuccess,
|
|
113
|
+
logWarning,
|
|
114
|
+
getCurrentBranch: getCurrentBranchFn = getCurrentBranch,
|
|
115
|
+
getGitStatus: getGitStatusFn = (dir) => getGitStatus(dir, { runCommandCapture }),
|
|
116
|
+
ensureCommittedChangesPushed: ensureCommittedChangesPushedFn = (branch, dir) =>
|
|
117
|
+
ensureCommittedChangesPushed(branch, dir, {
|
|
118
|
+
runCommand,
|
|
119
|
+
runCommandCapture,
|
|
120
|
+
logProcessing,
|
|
121
|
+
logSuccess,
|
|
122
|
+
logWarning
|
|
123
|
+
})
|
|
124
|
+
} = {}) {
|
|
125
|
+
if (!targetBranch) {
|
|
126
|
+
throw new Error('Deployment branch is not defined in the release configuration.')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const currentBranch = await getCurrentBranchFn(rootDir)
|
|
130
|
+
|
|
131
|
+
if (!currentBranch) {
|
|
132
|
+
throw new Error('Unable to determine the current git branch. Ensure this is a git repository.')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const initialStatus = await getGitStatusFn(rootDir)
|
|
136
|
+
const hasPendingChanges = initialStatus.length > 0
|
|
137
|
+
|
|
138
|
+
const statusReport = await runCommandCapture('git', ['status', '--short', '--branch'], { cwd: rootDir })
|
|
139
|
+
const lines = statusReport.split(/\r?\n/)
|
|
140
|
+
const branchLine = lines[0] || ''
|
|
141
|
+
const aheadMatch = branchLine.match(/ahead (\d+)/)
|
|
142
|
+
const behindMatch = branchLine.match(/behind (\d+)/)
|
|
143
|
+
const aheadCount = aheadMatch ? parseInt(aheadMatch[1], 10) : 0
|
|
144
|
+
const behindCount = behindMatch ? parseInt(behindMatch[1], 10) : 0
|
|
145
|
+
|
|
146
|
+
if (aheadCount > 0) {
|
|
147
|
+
logWarning?.(`Local branch ${currentBranch} is ahead of upstream by ${aheadCount} commit${aheadCount === 1 ? '' : 's'}.`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (behindCount > 0) {
|
|
151
|
+
logProcessing?.(`Synchronizing local branch ${currentBranch} with its upstream...`)
|
|
152
|
+
try {
|
|
153
|
+
await runCommand('git', ['pull', '--ff-only'], { cwd: rootDir })
|
|
154
|
+
logSuccess?.('Local branch fast-forwarded with upstream changes.')
|
|
155
|
+
} catch (error) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Unable to fast-forward ${currentBranch} with upstream changes. Resolve conflicts manually, then rerun the deployment.\n${error.message}`
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (currentBranch !== targetBranch) {
|
|
163
|
+
if (hasPendingChanges) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Local repository has uncommitted changes on ${currentBranch}. Commit or stash them before switching to ${targetBranch}.`
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
logProcessing?.(`Switching local repository from ${currentBranch} to ${targetBranch}...`)
|
|
170
|
+
await runCommand('git', ['checkout', targetBranch], { cwd: rootDir })
|
|
171
|
+
logSuccess?.(`Checked out ${targetBranch} locally.`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const statusAfterCheckout = currentBranch === targetBranch ? initialStatus : await getGitStatusFn(rootDir)
|
|
175
|
+
|
|
176
|
+
if (statusAfterCheckout.length === 0) {
|
|
177
|
+
await ensureCommittedChangesPushedFn(targetBranch, rootDir)
|
|
178
|
+
logProcessing?.('Local repository is clean. Proceeding with deployment.')
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!hasStagedChanges(statusAfterCheckout)) {
|
|
183
|
+
await ensureCommittedChangesPushedFn(targetBranch, rootDir)
|
|
184
|
+
logProcessing?.('No staged changes detected. Unstaged or untracked files will not affect deployment. Proceeding with deployment.')
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
logWarning?.(`Staged changes detected on ${targetBranch}. A commit is required before deployment.`)
|
|
189
|
+
|
|
190
|
+
const { commitMessage } = await runPrompt([
|
|
191
|
+
{
|
|
192
|
+
type: 'input',
|
|
193
|
+
name: 'commitMessage',
|
|
194
|
+
message: 'Enter a commit message for pending changes before deployment',
|
|
195
|
+
validate: (value) => (value && value.trim().length > 0 ? true : 'Commit message cannot be empty.')
|
|
196
|
+
}
|
|
197
|
+
])
|
|
198
|
+
|
|
199
|
+
const message = commitMessage.trim()
|
|
200
|
+
|
|
201
|
+
logProcessing?.('Committing staged changes before deployment...')
|
|
202
|
+
await runCommand('git', ['commit', '-m', message], { cwd: rootDir })
|
|
203
|
+
await runCommand('git', ['push', 'origin', targetBranch], { cwd: rootDir })
|
|
204
|
+
logSuccess?.(`Committed and pushed changes to origin/${targetBranch}.`)
|
|
205
|
+
|
|
206
|
+
const finalStatus = await getGitStatusFn(rootDir)
|
|
207
|
+
|
|
208
|
+
if (finalStatus.length > 0) {
|
|
209
|
+
throw new Error('Local repository still has uncommitted changes after commit. Aborting deployment.')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await ensureCommittedChangesPushedFn(targetBranch, rootDir)
|
|
213
|
+
logProcessing?.('Local repository is clean after committing pending changes.')
|
|
214
|
+
}
|
|
215
|
+
|