Skip to content

Commit eb05111

Browse files
committed
feat: split files into services & types
1 parent 71527ff commit eb05111

File tree

7 files changed

+452
-357
lines changed

7 files changed

+452
-357
lines changed

src/index.ts

Lines changed: 9 additions & 357 deletions
Original file line numberDiff line numberDiff line change
@@ -1,365 +1,17 @@
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';
64

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> {
506
try {
51-
const inputs = getInputs();
7+
const config = new ConfigManager();
8+
const deploymentService = new DeploymentService(config);
529

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();
7011
} 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}`);
7513
}
7614
}
7715

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-
36416
// Automatically run the deploy function when the script is executed
365-
deploy();
17+
run();

0 commit comments

Comments
 (0)