Skip to content

Commit b09cd69

Browse files
authored
Merge pull request #5 from FlowSahl/feat/refactor-code-1-1-0
Refactor code
2 parents 3ecb002 + b56d33c commit b09cd69

File tree

8 files changed

+225
-92
lines changed

8 files changed

+225
-92
lines changed

README.md

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,50 @@
11
# Laravel Zero Downtime Deployment
22

33
## Overview
4-
This GitHub Action helps you deploy your project to a remote server with zero downtime, ensuring that your application remains available during deployments.
4+
This GitHub Action helps you deploy your Laravel project to a remote server with zero downtime, ensuring that your application remains available during deployments. It offers flexibility, security, and ease of use, making it ideal for projects of all sizes.
55

66
## Features
77
- **Zero Downtime Deployment**: Ensure uninterrupted service during deployments.
8-
- **Easy Integration**: Simple setup and integration into your existing workflow.
9-
- **Flexible Deployment**: Suitable for projects of all sizes, from personal projects to enterprise applications.
10-
- **Custom Scripts**: Run custom scripts before and after key deployment steps.
11-
- **Secure**: Uses GitHub Secrets for sensitive data like server credentials and GitHub tokens.
12-
- **Environment File Sync**: Sync environment variables with the remote server.
8+
- **Modular and Maintainable Code**: Well-organized code structure with TypeScript, making it easy to extend and maintain.
9+
- **Customizable Workflow**: Easily integrate custom scripts at various stages of the deployment process.
10+
- **Environment Validation**: Robust environment configuration validation using `joi`.
11+
- **Secure Deployment**: Uses GitHub Secrets to securely manage sensitive data like server credentials and GitHub tokens.
12+
- **Environment File Sync**: Automatically sync environment variables with the remote server.
13+
14+
## How It Works
15+
16+
The Laravel Zero Downtime Deployment action follows a series of carefully structured steps to ensure that your application remains online throughout the deployment process:
17+
18+
### Steps in the Deployment Process:
19+
20+
1. **Preparation of Directories:**
21+
- The action starts by preparing the necessary directories on the remote server. This includes creating a new directory for the release and ensuring that required subdirectories (e.g., storage, logs) are available.
22+
23+
2. **Optional Pre-Folder Script Execution:**
24+
- If specified, a custom script is executed before the folders are checked and prepared. This can be useful for tasks like cleaning up old files or performing pre-checks.
25+
26+
3. **Cloning the Repository:**
27+
- The specified branch of your GitHub repository is cloned into the newly prepared release directory on the remote server. This ensures that the latest code is deployed.
28+
29+
4. **Environment File Synchronization:**
30+
- The `.env` file is synchronized between your local setup and the remote server. This ensures that your application’s environment variables are consistent across deployments.
31+
32+
5. **Linking the Storage Directory:**
33+
- The storage directory is linked from the new release directory to ensure that persistent data (like uploaded files) is shared across all releases.
34+
35+
6. **Optional Post-Download Script Execution:**
36+
- If specified, a custom script is executed after the repository is cloned and the environment is set up. This can be used for tasks like installing dependencies, running database migrations, or optimizing the application.
37+
38+
7. **Activating the New Release:**
39+
- The symbolic link to the current release is updated to point to the new release directory. This is the step where the new version of your application goes live without any downtime.
40+
41+
8. **Cleaning Up Old Releases:**
42+
- Old release directories are cleaned up, typically keeping only the last few releases to save space on the server.
43+
44+
9. **Optional Post-Activation Script Execution:**
45+
- If specified, a custom script is executed after the new release is activated. This is often used to perform final optimizations or notify external services of the new deployment.
46+
47+
By following these steps, the action ensures that your application is deployed smoothly, with zero downtime and minimal risk.
1348

1449
## Inputs
1550

@@ -103,6 +138,17 @@ You can provide custom scripts to run at various stages of the deployment. Below
103138
- **Before Activating Release**: `command_script_before_activate`
104139
- **After Activating Release**: `command_script_after_activate`
105140

141+
## Testing
142+
To ensure the reliability of your deployment process, unit and feature tests have been included in the codebase using Jest. Tests cover various components such as the `DeploymentService`, `ConfigManager`, and `sshUtils`. Running these tests can help identify issues early in the development process.
143+
144+
To run the tests:
145+
146+
```bash
147+
npm run test
148+
```
149+
150+
This will execute the suite of unit and feature tests, ensuring that all parts of the deployment process function correctly.
151+
106152
## Troubleshooting
107153
If you encounter issues, check the GitHub Actions logs for detailed error messages. Ensure that:
108154
- SSH credentials are correct.

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ async function run(): Promise<void> {
77
const config = new ConfigManager();
88
const deploymentService = new DeploymentService(config);
99

10+
log(`Starting deployment with configuration: ${JSON.stringify(config)}`);
11+
1012
await deploymentService.deploy();
1113
} catch (error: any) {
1214
log(`Deployment failed: ${error.message}`);
15+
if (error.stack) {
16+
log(error.stack);
17+
}
18+
process.exit(1); // Indicate failure
1319
}
1420
}
1521

src/services/ConfigManager.ts

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,49 +11,67 @@ export class ConfigManager {
1111
dotenv.config();
1212
}
1313

14-
validateConfig(this.getInputs());
15-
validateConnectionOptions(this.getConnectionOptions());
14+
try {
15+
validateConfig(this.getInputs());
16+
validateConnectionOptions(this.getConnectionOptions());
17+
} catch (error: any) {
18+
core.setFailed(`Configuration validation failed: ${error.message}`);
19+
throw error; // Re-throw if necessary for upstream handling
20+
}
21+
}
22+
23+
private getInputOrEnv(key: string, envKey: string): string {
24+
return process.env[envKey] || core.getInput(key);
1625
}
1726

1827
getInputs(): Inputs {
1928
return {
20-
target: process.env.TARGET || core.getInput('target'),
21-
sha: process.env.SHA || core.getInput('sha'),
22-
deploy_branch: process.env.GITHUB_DEPLOY_BRANCH || core.getInput('deploy_branch'),
23-
envFile: process.env.ENV_FILE || core.getInput('env_file'),
24-
commandScriptBeforeCheckFolders:
25-
process.env.COMMAND_SCRIPT_BEFORE_CHECK_FOLDERS || core.getInput('command_script_before_check_folders'),
26-
commandScriptAfterCheckFolders:
27-
process.env.COMMAND_SCRIPT_AFTER_CHECK_FOLDERS || core.getInput('command_script_after_check_folders'),
28-
commandScriptBeforeDownload:
29-
process.env.COMMAND_SCRIPT_BEFORE_DOWNLOAD || core.getInput('command_script_before_download'),
30-
commandScriptAfterDownload:
31-
process.env.COMMAND_SCRIPT_AFTER_DOWNLOAD || core.getInput('command_script_after_download'),
32-
commandScriptBeforeActivate:
33-
process.env.COMMAND_SCRIPT_BEFORE_ACTIVATE || core.getInput('command_script_before_activate'),
34-
commandScriptAfterActivate:
35-
process.env.COMMAND_SCRIPT_AFTER_ACTIVATE || core.getInput('command_script_after_activate'),
36-
githubRepoOwner: process.env.GITHUB_REPO_OWNER || github.context.payload.repository?.owner?.login || '',
37-
githubRepo: process.env.GITHUB_REPO || github.context.payload.repository?.name || '',
29+
target: this.getInputOrEnv('target', 'TARGET'),
30+
sha: this.getInputOrEnv('sha', 'SHA'),
31+
deploy_branch: this.getInputOrEnv('deploy_branch', 'GITHUB_DEPLOY_BRANCH'),
32+
envFile: this.getInputOrEnv('env_file', 'ENV_FILE'),
33+
commandScriptBeforeCheckFolders: this.getInputOrEnv(
34+
'command_script_before_check_folders',
35+
'COMMAND_SCRIPT_BEFORE_CHECK_FOLDERS'
36+
),
37+
commandScriptAfterCheckFolders: this.getInputOrEnv(
38+
'command_script_after_check_folders',
39+
'COMMAND_SCRIPT_AFTER_CHECK_FOLDERS'
40+
),
41+
commandScriptBeforeDownload: this.getInputOrEnv(
42+
'command_script_before_download',
43+
'COMMAND_SCRIPT_BEFORE_DOWNLOAD'
44+
),
45+
commandScriptAfterDownload: this.getInputOrEnv('command_script_after_download', 'COMMAND_SCRIPT_AFTER_DOWNLOAD'),
46+
commandScriptBeforeActivate: this.getInputOrEnv(
47+
'command_script_before_activate',
48+
'COMMAND_SCRIPT_BEFORE_ACTIVATE'
49+
),
50+
commandScriptAfterActivate: this.getInputOrEnv('command_script_after_activate', 'COMMAND_SCRIPT_AFTER_ACTIVATE'),
51+
githubRepoOwner:
52+
this.getInputOrEnv('github_repo_owner', 'GITHUB_REPO_OWNER') ||
53+
github.context.payload.repository?.owner?.login ||
54+
'',
55+
githubRepo: this.getInputOrEnv('github_repo', 'GITHUB_REPO') || github.context.payload.repository?.name || '',
3856
};
3957
}
4058

4159
getConnectionOptions(): ConnectionOptions {
4260
return {
43-
host: process.env.HOST || core.getInput('host'),
44-
username: process.env.REMOTE_USERNAME || core.getInput('username'),
45-
port: parseInt(process.env.PORT || core.getInput('port') || '22'),
46-
password: process.env.PASSWORD || core.getInput('password'),
47-
privateKey: (process.env.SSH_KEY || core.getInput('ssh_key')).replace(/\\n/g, '\n'),
48-
passphrase: process.env.SSH_PASSPHRASE || core.getInput('ssh_passphrase'),
61+
host: this.getInputOrEnv('host', 'HOST'),
62+
username: this.getInputOrEnv('username', 'REMOTE_USERNAME'),
63+
port: parseInt(this.getInputOrEnv('port', 'PORT')),
64+
password: this.getInputOrEnv('password', 'PASSWORD'),
65+
privateKey: this.getInputOrEnv('ssh_key', 'SSH_KEY').replace(/\\n/g, '\n'),
66+
passphrase: this.getInputOrEnv('ssh_passphrase', 'SSH_PASSPHRASE'),
4967
};
5068
}
5169

5270
getTarget(): string {
53-
return process.env.TARGET || core.getInput('target');
71+
return this.getInputOrEnv('target', 'TARGET');
5472
}
5573

5674
getSha(): string {
57-
return process.env.SHA || core.getInput('sha');
75+
return this.getInputOrEnv('sha', 'SHA');
5876
}
5977
}

src/services/DeploymentService.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class DeploymentService {
1717

1818
async deploy(): Promise<void> {
1919
try {
20-
await this.checkSponsorship(this.config.getInputs().githubRepoOwner);
20+
await this.checkSponsorship(this.config.getInputs().githubRepoOwner ?? '');
2121

2222
logInputs(this.config.getInputs(), this.config.getConnectionOptions());
2323

@@ -34,10 +34,11 @@ export class DeploymentService {
3434
if (error instanceof Error) {
3535
// Type guard for Error
3636
log(`Deployment failed: ${error.message}`);
37+
log(error.stack?.toString() ?? 'No Error Stack trace'); // Stack trace for detailed error information
3738
} else {
3839
log('An unknown error occurred during deployment.');
3940
}
40-
core.setFailed(error.message);
41+
core.setFailed(error.message || 'An unknown error occurred');
4142
throw error; // Re-throw the error after handling
4243
} finally {
4344
sshOperations.dispose();
@@ -67,12 +68,15 @@ export class DeploymentService {
6768
log(`Sponsorship check failed with status ${error.response.status}: ${error.response.data}`);
6869
throw new Error('Sponsorship check failed. Please try again later.');
6970
}
71+
} else if (axios.isAxiosError(error)) {
72+
log(`Axios error: ${error.message}`);
7073
} else {
74+
log('Non-Axios error occurred during sponsorship check');
7175
log('An unknown error occurred during the sponsorship check.');
7276
// throw error;
7377
}
7478
}
75-
79+
7680
private async prepareDeployment(): Promise<void> {
7781
// 1. Run any user-specified script before checking folders
7882
await this.runOptionalScript(this.config.getInputs().commandScriptBeforeCheckFolders, 'before check folders');
@@ -114,8 +118,10 @@ export class DeploymentService {
114118
`${paths.target}/storage/framework/views`,
115119
];
116120

117-
await sshOperations.execute(`mkdir -p ${folders.join(' ')}`, paths);
118-
await sshOperations.execute(`rm -rf ${paths.target}/releases/${paths.sha}`, paths);
121+
await Promise.all([
122+
sshOperations.execute(`mkdir -p ${folders.join(' ')}`, paths),
123+
sshOperations.execute(`rm -rf ${paths.target}/releases/${paths.sha}`, paths),
124+
]);
119125
}
120126

121127
private async cloneAndPrepareRepository(inputs: Inputs, paths: Paths): Promise<void> {
@@ -144,6 +150,8 @@ export class DeploymentService {
144150
if (script && script !== 'false') {
145151
log(`Running script ${description}: ${script}`);
146152
await sshOperations.execute(script, this.paths);
153+
} else {
154+
log(`No script to run for ${description}`);
147155
}
148156
}
149157

src/types.ts

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
1-
// src/types.ts
21
export interface Inputs {
3-
target: string;
4-
sha: string;
5-
deploy_branch: string;
6-
envFile?: string;
7-
commandScriptBeforeCheckFolders?: string;
8-
commandScriptAfterCheckFolders?: string;
9-
commandScriptBeforeDownload?: string;
10-
commandScriptAfterDownload?: string;
11-
commandScriptBeforeActivate?: string;
12-
commandScriptAfterActivate?: string;
13-
githubRepoOwner: string;
14-
githubRepo: string;
2+
target: string; // The target directory on the server where the deployment will occur
3+
sha: string; // The specific commit SHA to be deployed
4+
deploy_branch: string; // The branch of the repository to deploy
5+
envFile?: string; // Optional content of the environment file to be used in the deployment
6+
commandScriptBeforeCheckFolders?: string; // Custom script to run before checking folders
7+
commandScriptAfterCheckFolders?: string; // Custom script to run after checking folders
8+
commandScriptBeforeDownload?: string; // Custom script to run before downloading the release
9+
commandScriptAfterDownload?: string; // Custom script to run after downloading the release
10+
commandScriptBeforeActivate?: string; // Custom script to run before activating the release
11+
commandScriptAfterActivate?: string; // Custom script to run after activating the release
12+
githubRepoOwner: string; // The owner of the GitHub repository
13+
githubRepo: string; // The name of the GitHub repository
1514
}
1615

16+
/** Represents the paths used during the deployment process */
1717
export interface Paths {
18-
target: string;
19-
sha: string;
20-
releasePath: string;
21-
activeReleasePath: string;
18+
target: string; // The base target directory
19+
sha: string; // The SHA of the commit being deployed
20+
releasePath: string; // The path to the specific release
21+
activeReleasePath: string; // The path to the active release
2222
}
2323

24+
/** Represents the SSH connection options */
2425
export interface ConnectionOptions {
25-
host: string;
26-
username: string;
27-
port?: number | 22;
28-
password?: string;
29-
privateKey?: string;
30-
passphrase?: string;
26+
host: string; // The host of the server to connect to
27+
username: string; // The username to use for the SSH connection
28+
port?: number | 22; // The port to use for the SSH connection (defaults to 22)
29+
password?: string; // The password for the SSH connection
30+
privateKey?: string; // The private key for the SSH connection
31+
passphrase?: string; // The passphrase for the private key, if applicable
3132
}

src/utils/log.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import { ConnectionOptions, Inputs } from '../types';
22

3+
/** Logs a message with a timestamp */
34
export function log(message: string): void {
45
const timestamp = new Date().toISOString();
5-
console.log(`[${timestamp}] ${message}`);
6+
console.log(`[DEPLOYMENT][${timestamp}] ${message}`);
67
}
78

8-
export function logInputs(inputs: Inputs, connectionOptions: ConnectionOptions) {
9+
/** Logs input configurations */
10+
export function logInputs(inputs: Inputs, connectionOptions: ConnectionOptions): void {
911
log(`Host: ${connectionOptions.host}`);
10-
log(`Target: ${inputs.target}`);
11-
log(`SHA: ${inputs.sha}`);
12-
log(`GitHub Repo Owner: ${inputs.githubRepoOwner}`);
12+
log(`Target Directory: ${inputs.target}`);
13+
log(`Commit SHA: ${inputs.sha}`);
14+
log(`GitHub Repository: ${inputs.githubRepoOwner}/${inputs.githubRepo}`);
15+
log(`Branch: ${inputs.deploy_branch}`);
16+
}
17+
18+
/** Logs an error message with a timestamp */
19+
export function logError(message: string): void {
20+
const timestamp = new Date().toISOString();
21+
console.error(`[DEPLOYMENT ERROR][${timestamp}] ${message}`);
1322
}

0 commit comments

Comments
 (0)