|
1 | | -import * as core from '@actions/core'; |
2 | | -import * as github from '@actions/github'; |
3 | | -import { NodeSSH } from 'node-ssh'; |
4 | | -import axios from 'axios'; |
5 | | -import dotenv from 'dotenv'; |
| 1 | +import { ConfigManager } from './services/ConfigManager'; |
| 2 | +import { DeploymentService } from './services/DeploymentService'; |
| 3 | +import { log } from './utils/log'; |
6 | 4 |
|
7 | | -const ssh = new NodeSSH(); |
8 | | - |
9 | | -const deployDate = new Date().toISOString(); |
10 | | - |
11 | | -// Load environment variables from .env file in development mode |
12 | | -if (process.env.NODE_ENV !== 'production') { |
13 | | - dotenv.config(); |
14 | | -} |
15 | | - |
16 | | -interface Inputs { |
17 | | - target: string; |
18 | | - sha: string; |
19 | | - deploy_branch: string; |
20 | | - envFile?: string; |
21 | | - commandScriptBeforeCheckFolders?: string; |
22 | | - commandScriptAfterCheckFolders?: string; |
23 | | - commandScriptBeforeDownload?: string; |
24 | | - commandScriptAfterDownload?: string; |
25 | | - commandScriptBeforeActivate?: string; |
26 | | - commandScriptAfterActivate?: string; |
27 | | - githubRepoOwner: string; |
28 | | - githubRepo: string; |
29 | | -} |
30 | | - |
31 | | -interface Paths { |
32 | | - target: string; |
33 | | - sha: string; |
34 | | - releasePath: string; |
35 | | - activeReleasePath: string; |
36 | | -} |
37 | | - |
38 | | -let paths: Paths; |
39 | | - |
40 | | -interface ConnectionOptions { |
41 | | - host: string; |
42 | | - username: string; |
43 | | - port?: number | 22; |
44 | | - password?: string; |
45 | | - privateKey?: string; |
46 | | - passphrase?: string; |
47 | | -} |
48 | | - |
49 | | -async function deploy() { |
| 5 | +async function run(): Promise<void> { |
50 | 6 | try { |
51 | | - const inputs = getInputs(); |
| 7 | + const config = new ConfigManager(); |
| 8 | + const deploymentService = new DeploymentService(config); |
52 | 9 |
|
53 | | - const connectionOptions = getConnectionOptions(); |
54 | | - |
55 | | - validateConfig(inputs); |
56 | | - |
57 | | - validateConnectionOptions(connectionOptions); |
58 | | - |
59 | | - logInputs(inputs, connectionOptions); |
60 | | - |
61 | | - await checkSponsorship(inputs.githubRepoOwner); |
62 | | - |
63 | | - await sshOperations.connect(connectionOptions); |
64 | | - |
65 | | - await prepareDeployment(inputs); |
66 | | - |
67 | | - await activateRelease(inputs); |
68 | | - |
69 | | - log('Deployment completed successfully!'); |
| 10 | + await deploymentService.deploy(); |
70 | 11 | } catch (error: any) { |
71 | | - console.error(`Error: ${error.message}`); |
72 | | - core.setFailed(error.message); |
73 | | - } finally { |
74 | | - ssh.dispose(); |
| 12 | + log(`Deployment failed: ${error.message}`); |
75 | 13 | } |
76 | 14 | } |
77 | 15 |
|
78 | | -function getConnectionOptions(): ConnectionOptions { |
79 | | - return { |
80 | | - host: process.env.HOST || core.getInput('host'), |
81 | | - username: process.env.REMOTE_USERNAME || core.getInput('username'), |
82 | | - port: parseInt(process.env.PORT || core.getInput('port') || '22'), |
83 | | - password: process.env.PASSWORD || core.getInput('password'), |
84 | | - // Replace escaped newlines with actual newlines |
85 | | - privateKey: (process.env.SSH_KEY || core.getInput('ssh_key')).replace( |
86 | | - /\\n/g, |
87 | | - '\n' |
88 | | - ), |
89 | | - passphrase: process.env.SSH_PASSPHRASE || core.getInput('ssh_passphrase'), // Add passphrase |
90 | | - }; |
91 | | -} |
92 | | - |
93 | | -function getInputs(): Inputs { |
94 | | - return { |
95 | | - target: process.env.TARGET || core.getInput('target'), |
96 | | - sha: process.env.SHA || core.getInput('sha'), |
97 | | - deploy_branch: |
98 | | - process.env.GITHUB_DEPLOY_BRANCH || core.getInput('deploy_branch'), |
99 | | - envFile: process.env.ENV_FILE || core.getInput('env_file'), |
100 | | - commandScriptBeforeCheckFolders: |
101 | | - process.env.COMMAND_SCRIPT_BEFORE_CHECK_FOLDERS || |
102 | | - core.getInput('command_script_before_check_folders'), |
103 | | - commandScriptAfterCheckFolders: |
104 | | - process.env.COMMAND_SCRIPT_AFTER_CHECK_FOLDERS || |
105 | | - core.getInput('command_script_after_check_folders'), |
106 | | - commandScriptBeforeDownload: |
107 | | - process.env.COMMAND_SCRIPT_BEFORE_DOWNLOAD || |
108 | | - core.getInput('command_script_before_download'), |
109 | | - commandScriptAfterDownload: |
110 | | - process.env.COMMAND_SCRIPT_AFTER_DOWNLOAD || |
111 | | - core.getInput('command_script_after_download'), |
112 | | - commandScriptBeforeActivate: |
113 | | - process.env.COMMAND_SCRIPT_BEFORE_ACTIVATE || |
114 | | - core.getInput('command_script_before_activate'), |
115 | | - commandScriptAfterActivate: |
116 | | - process.env.COMMAND_SCRIPT_AFTER_ACTIVATE || |
117 | | - core.getInput('command_script_after_activate'), |
118 | | - githubRepoOwner: |
119 | | - process.env.GITHUB_REPO_OWNER || |
120 | | - github.context.payload.repository?.owner?.login || |
121 | | - '', |
122 | | - githubRepo: |
123 | | - process.env.GITHUB_REPO || github.context.payload.repository?.name || '', |
124 | | - }; |
125 | | -} |
126 | | - |
127 | | -function validateConfig(inputs: Inputs) { |
128 | | - if (!inputs.target) throw new Error('Target directory is required.'); |
129 | | - if (!inputs.sha) throw new Error('SHA is required.'); |
130 | | -} |
131 | | - |
132 | | -function validateConnectionOptions(connectionOptions: ConnectionOptions) { |
133 | | - if (!connectionOptions.host) throw new Error('Host is required.'); |
134 | | - if (!connectionOptions.username) throw new Error('Username is required.'); |
135 | | - if ( |
136 | | - connectionOptions.port && |
137 | | - (isNaN(connectionOptions.port) || |
138 | | - connectionOptions.port < 1 || |
139 | | - connectionOptions.port > 65535) |
140 | | - ) { |
141 | | - throw new Error('Port must be a valid number between 1 and 65535.'); |
142 | | - } |
143 | | -} |
144 | | - |
145 | | -function log(message: string) { |
146 | | - const timestamp = new Date().toISOString(); |
147 | | - console.log(`[${timestamp}] ${message}`); |
148 | | -} |
149 | | - |
150 | | -function logInputs(inputs: Inputs, connectionOptions: ConnectionOptions) { |
151 | | - log(`Host: ${connectionOptions.host}`); |
152 | | - log(`Target: ${inputs.target}`); |
153 | | - log(`SHA: ${inputs.sha}`); |
154 | | - log(`GitHub Repo Owner: ${inputs.githubRepoOwner}`); |
155 | | -} |
156 | | - |
157 | | -async function checkSponsorship(githubRepoOwner: string) { |
158 | | - try { |
159 | | - const response = await axios.post( |
160 | | - 'https://deployer.flowsahl.com/api/check-github-sponsorship', |
161 | | - { |
162 | | - github_username: githubRepoOwner, |
163 | | - } |
164 | | - ); |
165 | | - log('Thanks for sponsoring us :)'); |
166 | | - } catch (error: any) { |
167 | | - handleSponsorshipError(error); |
168 | | - } |
169 | | -} |
170 | | - |
171 | | -function handleSponsorshipError(error: any) { |
172 | | - if (error.response) { |
173 | | - if (error.response.status === 403) { |
174 | | - throw new Error( |
175 | | - 'You are not a sponsor. Please consider sponsoring us to use this action: https://github.com/sponsors/FlowSahl. Start sponsoring us and try again [1$ or more].' |
176 | | - ); |
177 | | - } else if (error.response.status === 500) { |
178 | | - log( |
179 | | - 'An internal server error occurred while checking sponsorship, but the deployment will continue.' |
180 | | - ); |
181 | | - } else { |
182 | | - log( |
183 | | - `Sponsorship check failed with status ${error.response.status}: ${error.response.data}` |
184 | | - ); |
185 | | - throw new Error('Sponsorship check failed. Please try again later.'); |
186 | | - } |
187 | | - } else { |
188 | | - log('An unknown error occurred during the sponsorship check.'); |
189 | | - // throw error; |
190 | | - } |
191 | | -} |
192 | | - |
193 | | -const sshOperations = { |
194 | | - async connect({ |
195 | | - host, |
196 | | - username, |
197 | | - port, |
198 | | - password, |
199 | | - privateKey, |
200 | | - passphrase, |
201 | | - }: ConnectionOptions) { |
202 | | - log('Connecting to the server...'); |
203 | | - |
204 | | - const connectionOptions: ConnectionOptions = { |
205 | | - host, |
206 | | - username, |
207 | | - port: port, |
208 | | - privateKey: privateKey ? privateKey : undefined, |
209 | | - passphrase: passphrase ? passphrase : undefined, |
210 | | - password: password ? password : undefined, |
211 | | - }; |
212 | | - |
213 | | - try { |
214 | | - if (password) { |
215 | | - log('SSH key authentication password set Successfully'); |
216 | | - } |
217 | | - |
218 | | - await ssh.connect(connectionOptions); |
219 | | - } catch (keyError: any) { |
220 | | - log(`Failed to connect with SSH key: ${keyError.message}`); |
221 | | - throw keyError; |
222 | | - } |
223 | | - }, |
224 | | - |
225 | | - async execute(command: string, showCommandLog: boolean = false) { |
226 | | - try { |
227 | | - command = prepareCommand(command); |
228 | | - |
229 | | - if (showCommandLog) log(`Executing command: ${command}`); |
230 | | - |
231 | | - const result = await ssh.execCommand(command); |
232 | | - |
233 | | - if (result.stdout) log(result.stdout); |
234 | | - if (result.stderr) console.error(result.stderr); |
235 | | - if (result.code !== 0) |
236 | | - throw new Error(`Command failed: ${command} - ${result.stderr}`); |
237 | | - } catch (error: any) { |
238 | | - throw new Error( |
239 | | - `Failed to execute command: ${command} - ${error.message}` |
240 | | - ); |
241 | | - } |
242 | | - }, |
243 | | -}; |
244 | | - |
245 | | -function prepareCommand(command: string): string { |
246 | | - return command |
247 | | - .replace(/\$THIS_RELEASE_PATH/g, paths.releasePath) |
248 | | - .replace(/\$ACTIVE_RELEASE_PATH/g, `${paths.target}/current`); |
249 | | -} |
250 | | - |
251 | | -async function prepareDeployment(inputs: Inputs) { |
252 | | - paths = getPaths(inputs.target, inputs.sha); |
253 | | - |
254 | | - await runOptionalScript( |
255 | | - inputs.commandScriptBeforeCheckFolders, |
256 | | - 'before check folders' |
257 | | - ); |
258 | | - |
259 | | - await checkAndPrepareFolders(paths); |
260 | | - |
261 | | - await runOptionalScript( |
262 | | - inputs.commandScriptAfterCheckFolders, |
263 | | - 'after check folders' |
264 | | - ); |
265 | | - |
266 | | - await cloneAndPrepareRepository(inputs, paths); |
267 | | - |
268 | | - await syncEnvironmentFile(inputs.envFile, paths); |
269 | | - |
270 | | - await linkStorage(paths); |
271 | | - |
272 | | - await runOptionalScript(inputs.commandScriptAfterDownload, 'after download'); |
273 | | -} |
274 | | - |
275 | | -function getPaths(target: string, sha: string): Paths { |
276 | | - return { |
277 | | - target: target, |
278 | | - sha: sha, |
279 | | - releasePath: `${target}/releases/${sha}-${deployDate}`, |
280 | | - activeReleasePath: `${target}/current`, |
281 | | - }; |
282 | | -} |
283 | | - |
284 | | -async function runOptionalScript( |
285 | | - script: string | undefined, |
286 | | - description: string |
287 | | -) { |
288 | | - if (script && script !== 'false') { |
289 | | - log(`Running script ${description}: ${script}`); |
290 | | - await sshOperations.execute(script); |
291 | | - } |
292 | | -} |
293 | | - |
294 | | -async function checkAndPrepareFolders(paths: Paths) { |
295 | | - log('Checking the folders...'); |
296 | | - const folders = [ |
297 | | - `${paths.target}/releases`, |
298 | | - `${paths.target}/storage`, |
299 | | - `${paths.target}/storage/app`, |
300 | | - `${paths.target}/storage/app/public`, |
301 | | - `${paths.target}/storage/logs`, |
302 | | - `${paths.target}/storage/framework`, |
303 | | - `${paths.target}/storage/framework/cache`, |
304 | | - `${paths.target}/storage/framework/sessions`, |
305 | | - `${paths.target}/storage/framework/views`, |
306 | | - ]; |
307 | | - await sshOperations.execute(`mkdir -p ${folders.join(' ')}`); |
308 | | - await sshOperations.execute(`rm -rf ${paths.target}/releases/${paths.sha}`); |
309 | | -} |
310 | | - |
311 | | -async function cloneAndPrepareRepository(inputs: Inputs, paths: Paths) { |
312 | | - await runOptionalScript(inputs.commandScriptBeforeDownload, 'before clone'); |
313 | | - |
314 | | - const repoUrl = `git@github.com:${inputs.githubRepoOwner}/${inputs.githubRepo}.git`; |
315 | | - |
316 | | - await sshOperations.execute(`cd ${inputs.target}`); |
317 | | - await sshOperations.execute(`rm -rf ${paths.releasePath}`); |
318 | | - log(`Cloning Repo: ${repoUrl}`); |
319 | | - await sshOperations.execute( |
320 | | - `git clone -b ${inputs.deploy_branch} ${repoUrl} ${paths.releasePath}`, |
321 | | - false |
322 | | - ); |
323 | | - await sshOperations.execute(`cd ${paths.releasePath}`); |
324 | | -} |
325 | | - |
326 | | -async function syncEnvironmentFile(envFile: string | undefined, paths: Paths) { |
327 | | - if (envFile) { |
328 | | - log('Syncing .env file'); |
329 | | - await sshOperations.execute(`echo '${envFile}' > ${paths.target}/.env`); |
330 | | - await sshOperations.execute( |
331 | | - `ln -sfn ${paths.target}/.env ${paths.releasePath}/.env` |
332 | | - ); |
333 | | - } |
334 | | -} |
335 | | - |
336 | | -async function linkStorage(paths: Paths) { |
337 | | - log('Linking the current release with storage'); |
338 | | - await sshOperations.execute( |
339 | | - `ln -sfn ${paths.target}/storage ${paths.releasePath}/storage` |
340 | | - ); |
341 | | -} |
342 | | - |
343 | | -async function activateRelease(inputs: Inputs) { |
344 | | - const paths = getPaths(inputs.target, inputs.sha); |
345 | | - |
346 | | - await runOptionalScript( |
347 | | - inputs.commandScriptBeforeActivate, |
348 | | - 'before activate' |
349 | | - ); |
350 | | - |
351 | | - log('Activating the release'); |
352 | | - await sshOperations.execute( |
353 | | - `ln -sfn ${paths.releasePath} ${paths.activeReleasePath}` |
354 | | - ); |
355 | | - await sshOperations.execute( |
356 | | - `ls -1dt ${inputs.target}/releases/*/ | tail -n +4 | xargs rm -rf` |
357 | | - ); |
358 | | - |
359 | | - await runOptionalScript(inputs.commandScriptAfterActivate, 'after activate'); |
360 | | -} |
361 | | - |
362 | | -export { deploy }; |
363 | | - |
364 | 16 | // Automatically run the deploy function when the script is executed |
365 | | -deploy(); |
| 17 | +run(); |
0 commit comments