diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..94e90dd --- /dev/null +++ b/.env.example @@ -0,0 +1,81 @@ +NODE_ENV=local + +HOST=127.0.0.1 +REMOTE_USERNAME=website-dev +PORT=22 +# PASSWORD=wLsC20XvBjDOPxMt4lJ0 +SSH_KEY='-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABB++JZyp +-----END OPENSSH PRIVATE KEY-----' +SSH_PASSPHRASE=password +TARGET=/home/website-dev/htdocs/dev.website.dev +SHA=9f17d06 +ENV_FILE='APP_NAME="laravel" +APP_ENV=local +APP_KEY=base64:pz/7scAOjZebfvvtU1TK5JxccE9tZ6VYMFXYWnozSXY= +APP_DEBUG=true +APP_URL="https://dev.laravel.com" + +LOG_CHANNEL=daily +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST="localhost" +DB_PORT=3306 +DB_DATABASE="laravel-dev" +DB_USERNAME="laravel-dev" +DB_PASSWORD="password" + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +FILESYSTEM_DISK=local +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +MEMCACHED_HOST=127.0.0.1 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_HOST=mailpit +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +PUSHER_APP_ID= +PUSHER_APP_KEY= +PUSHER_APP_SECRET= +PUSHER_HOST= +PUSHER_PORT=443 +PUSHER_SCHEME=https +PUSHER_APP_CLUSTER=mt1 + +VITE_APP_NAME="${APP_NAME}" +VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +VITE_PUSHER_HOST="${PUSHER_HOST}" +VITE_PUSHER_PORT="${PUSHER_PORT}" +VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" +VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" +APP_TIMEZONE="Europe/Andorra"' +# COMMAND_SCRIPT_BEFORE_CHECK_FOLDERS=your-command +# COMMAND_SCRIPT_AFTER_CHECK_FOLDERS=your-command +# COMMAND_SCRIPT_BEFORE_DOWNLOAD=your-command +# COMMAND_SCRIPT_AFTER_DOWNLOAD=your-command +# COMMAND_SCRIPT_BEFORE_ACTIVATE=your-command +# COMMAND_SCRIPT_AFTER_ACTIVATE=your-command +GITHUB_REPO_OWNER=Laravel +GITHUB_REPO=Laravel +GITHUB_DEPLOY_BRANCH=main diff --git a/.gitignore b/.gitignore index 362e32c..7982b5a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules/ coverage docs -.vscode \ No newline at end of file +.vscode +.env \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..92f97e7 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/action.yml b/action.yml index 7706f74..c5c188b 100644 --- a/action.yml +++ b/action.yml @@ -17,18 +17,21 @@ inputs: ssh_key: description: 'SSH private key to connect server instead use password' required: false + ssh_passphrase: + description: 'SSH private passphrase' + required: false target: description: 'Remote server target path ' required: true sha: description: 'Git commit sha need to deploy (github.sha)' required: true - github_token: - description: 'Github token' - required: true env_file: description: 'Environment file content to sync with .env file' required: false + deploy_branch: + description: 'Branch will deploy from' + required: true command_script_before_check_folders: diff --git a/package-lock.json b/package-lock.json index 51f8110..86a949d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,11 @@ "node-ssh": "^13.2.0" }, "devDependencies": { + "@vercel/ncc": "^0.38.1", "dotenv": "^16.4.5", "jest": "^29.7.0", - "nock": "^13.5.4" + "nock": "^13.5.4", + "typescript": "^5.5.4" } }, "node_modules/@actions/core": { @@ -1213,6 +1215,15 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/@vercel/ncc": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.1.tgz", + "integrity": "sha512-IBBb+iI2NLu4VQn3Vwldyi2QwaXt5+hTyh58ggAMoCGE6DJmPvwL3KPBWcJl1m9LYPChBLE980Jw+CS4Wokqxw==", + "dev": true, + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -3710,6 +3721,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici": { "version": "5.28.4", "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", diff --git a/package.json b/package.json index 853dba7..2a010f9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "laravel-zero-time", "version": "1.0.0", "description": "Deploy project to server by ssh with zero downtime deployment.", - "main": "src/index.js", + "main": "src/index.ts", "directories": { "test": "test" }, @@ -14,13 +14,16 @@ "node-ssh": "^13.2.0" }, "devDependencies": { + "@vercel/ncc": "^0.38.1", "dotenv": "^16.4.5", "jest": "^29.7.0", - "nock": "^13.5.4" + "nock": "^13.5.4", + "typescript": "^5.5.4" }, "scripts": { "test": "jest", - "build": "ncc build src/index.js -m" + "start": "npm run build && node dist/index.js", + "build": "ncc build src/index.ts -o dist --source-map --minify" }, "repository": { "type": "git", diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 708c943..0000000 --- a/src/index.js +++ /dev/null @@ -1,247 +0,0 @@ -const core = require('@actions/core'); -const github = require('@actions/github'); -const { NodeSSH } = require('node-ssh'); -const axios = require('axios'); -const dotenv = require('dotenv'); - -const ssh = new NodeSSH(); - -// Load environment variables from .env file in development mode -if (process.env.NODE_ENV !== 'production') { - dotenv.config(); -} - -async function deploy() { - try { - const inputs = getInputs(); - validateInputs(inputs); - logInputs(inputs); - - await checkSponsorship(inputs.githubRepoOwner); - - await sshOperations.connect(inputs); - - await prepareDeployment(inputs); - - await activateRelease(inputs); - - log("Deployment completed successfully!"); - } catch (error) { - console.error(`Error: ${error.message}`); - core.setFailed(error.message); - } finally { - ssh.dispose(); - } -} - -function getInputs() { - return { - host: process.env.HOST || core.getInput('host'), - username: process.env.REMOTE_USERNAME || core.getInput('username'), - port: process.env.PORT || core.getInput('port'), - password: process.env.PASSWORD || core.getInput('password'), - sshKey: (process.env.SSH_KEY || core.getInput('ssh_key')).replace(/\\n/g, '\n'), // Replace escaped newlines with actual newlines - passphrase: process.env.SSH_PASSPHRASE || core.getInput('ssh_passphrase'), // Add passphrase - target: process.env.TARGET || core.getInput('target'), - sha: process.env.SHA || core.getInput('sha'), - deploy_branch: process.env.GITHUB_DEPLOY_BRANCH || core.getInput('deploy_branch'), - githubToken: process.env.GITHUB_TOKEN || core.getInput('github_token'), - envFile: process.env.ENV_FILE || core.getInput('env_file'), - commandScriptBeforeCheckFolders: process.env.COMMAND_SCRIPT_BEFORE_CHECK_FOLDERS || core.getInput('command_script_before_check_folders'), - commandScriptAfterCheckFolders: process.env.COMMAND_SCRIPT_AFTER_CHECK_FOLDERS || core.getInput('command_script_after_check_folders'), - commandScriptBeforeDownload: process.env.COMMAND_SCRIPT_BEFORE_DOWNLOAD || core.getInput('command_script_before_download'), - commandScriptAfterDownload: process.env.COMMAND_SCRIPT_AFTER_DOWNLOAD || core.getInput('command_script_after_download'), - commandScriptBeforeActivate: process.env.COMMAND_SCRIPT_BEFORE_ACTIVATE || core.getInput('command_script_before_activate'), - commandScriptAfterActivate: process.env.COMMAND_SCRIPT_AFTER_ACTIVATE || core.getInput('command_script_after_activate'), - githubRepoOwner: process.env.GITHUB_REPO_OWNER || github.context.payload.repository.owner.login, - githubRepo: process.env.GITHUB_REPO || github.context.payload.repository.name - }; -} - -function validateInputs(inputs) { - if (!inputs.host) throw new Error("Host is required."); - if (!inputs.username) throw new Error("Username is required."); - if (inputs.port && (isNaN(inputs.port) || inputs.port < 1 || inputs.port > 65535)) { - throw new Error("Port must be a valid number between 1 and 65535."); - } - if (!inputs.target) throw new Error("Target directory is required."); - if (!inputs.sha) throw new Error("SHA is required."); - if (!inputs.githubToken) throw new Error("GitHub token is required."); -} - -function log(message) { - const timestamp = new Date().toISOString(); - console.log(`[${timestamp}] ${message}`); -} - -function logInputs(inputs) { - log(`Host: ${inputs.host}`); - log(`Target: ${inputs.target}`); - log(`SHA: ${inputs.sha}`); - log(`GitHub Repo Owner: ${inputs.githubRepoOwner}`); - // Omitting sensitive information like GitHub token and SSH key -} - -async function checkSponsorship(githubRepoOwner) { - try { - const response = await axios.post('https://deployer.flowsahl.com/api/check-github-sponsorship', { - github_username: githubRepoOwner - }); - log('Thanks for sponsoring us :)'); - } catch (error) { - handleSponsorshipError(error); - } -} - -function handleSponsorshipError(error) { - if (error.response) { - if (error.response.status === 403) { - throw new Error("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]."); - } else if (error.response.status === 500) { - log("An internal server error occurred while checking sponsorship, but the deployment will continue."); - } else { - log(`Sponsorship check failed with status ${error.response.status}: ${error.response.data}`); - throw new Error("Sponsorship check failed. Please try again later."); - } - } else { - log("An unknown error occurred during the sponsorship check."); - // throw error; - } -} - -const sshOperations = { - async connect({ host, username, port, password, sshKey, passphrase }) { - log("Connecting to the server..."); - const connectionOptions = { - host, - username, - port: port ? parseInt(port) : undefined, - privateKey: sshKey, - passphrase: passphrase - }; - - try { - await ssh.connect(connectionOptions); - log("Successfully connected using SSH key."); - } catch (keyError) { - if (password) { - log("SSH key authentication failed, attempting password authentication..."); - try { - await ssh.connect({ host, username, port: port ? parseInt(port) : undefined, password }); - log("Successfully connected using password."); - } catch (passwordError) { - log(`Failed to connect with password: ${passwordError.message}`); - throw passwordError; - } - } else { - log(`Failed to connect with SSH key: ${keyError.message}`); - throw keyError; - } - } - }, - - async execute(command) { - try { - const result = await ssh.execCommand(command); - if (result.stdout) log(result.stdout); - if (result.stderr) console.error(result.stderr); - if (result.code !== 0) throw new Error(`Command failed: ${command} - ${result.stderr}`); - } catch (error) { - throw new Error(`Failed to execute command: ${command} - ${error.message}`); - } - } -}; - -async function prepareDeployment(inputs) { - const paths = getPaths(inputs.target, inputs.sha); - - await runOptionalScript(inputs.commandScriptBeforeCheckFolders, "before check folders"); - - await checkAndPrepareFolders(paths); - - await runOptionalScript(inputs.commandScriptAfterCheckFolders, "after check folders"); - - await cloneAndPrepareRepository(inputs, paths); - - await syncEnvironmentFile(inputs.envFile, paths); - - await linkStorage(paths); - - await runOptionalScript(inputs.commandScriptAfterDownload, "after download"); -} - -function getPaths(target, sha) { - return { - releasePath: `${target}/releases/${sha}`, - activeReleasePath: `${target}/current` - }; -} - -async function runOptionalScript(script, description) { - if (script !== 'false') { - log(`Running script ${description}: ${script}`); - await sshOperations.execute(script); - } -} - -async function checkAndPrepareFolders(paths) { - log("Checking the folders..."); - const folders = [ - `${paths.target}/releases`, - `${paths.target}/storage`, - `${paths.target}/storage/app`, - `${paths.target}/storage/app/public`, - `${paths.target}/storage/logs`, - `${paths.target}/storage/framework`, - `${paths.target}/storage/framework/cache`, - `${paths.target}/storage/framework/sessions`, - `${paths.target}/storage/framework/views` - ]; - await sshOperations.execute(`mkdir -p ${folders.join(' ')}`); - await sshOperations.execute(`rm -rf ${paths.target}/_temp_${paths.sha}`); - await sshOperations.execute(`rm -rf ${paths.target}/releases/${paths.sha}`); - await sshOperations.execute(`rm -rf ${paths.target}/${paths.sha}.zip`); -} - -async function cloneAndPrepareRepository(inputs, paths) { - await runOptionalScript(inputs.commandScriptBeforeDownload, "before clone"); - - const repoUrl = `git@github.com:${inputs.githubRepoOwner}/${inputs.githubRepo}.git`; - log(`Cloning Repo: ${repoUrl}`); - - await sshOperations.execute(` - cd ${inputs.target} && - rm -rf ${paths.releasePath} && - git clone ${repoUrl} ${paths.releasePath} && - cd ${paths.releasePath} && - git checkout ${inputs.deploy_branch} - `); -} - -async function syncEnvironmentFile(envFile, paths) { - if (envFile) { - log("Syncing .env file"); - await sshOperations.execute(`echo '${envFile}' > ${paths.target}/.env`); - await sshOperations.execute(`ln -sfn ${paths.target}/.env ${paths.releasePath}/.env`); - } -} - -async function linkStorage(paths) { - log("Linking the current release with storage"); - await sshOperations.execute(`ln -sfn ${paths.target}/storage ${paths.releasePath}/storage`); -} - -async function activateRelease(inputs) { - const paths = getPaths(inputs.target, inputs.sha); - - await runOptionalScript(inputs.commandScriptBeforeActivate, "before activate"); - - log("Activating the release"); - await sshOperations.execute(`ln -sfn ${paths.releasePath} ${paths.activeReleasePath} && ls -1dt ${inputs.target}/releases/*/ | tail -n +4 | xargs rm -rf`); - - await runOptionalScript(inputs.commandScriptAfterActivate, "after activate"); -} -module.exports = { deploy }; - -// Automatically run the deploy function when the script is executed -deploy(); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..177bd4d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,348 @@ +import * as core from '@actions/core'; +import * as github from '@actions/github'; +import { NodeSSH } from 'node-ssh'; +import axios from 'axios'; +import dotenv from 'dotenv'; + +const ssh = new NodeSSH(); + +// Load environment variables from .env file in development mode +if (process.env.NODE_ENV !== 'production') { + dotenv.config(); +} + +interface Inputs { + target: string; + sha: string; + deploy_branch: string; + envFile?: string; + commandScriptBeforeCheckFolders?: string; + commandScriptAfterCheckFolders?: string; + commandScriptBeforeDownload?: string; + commandScriptAfterDownload?: string; + commandScriptBeforeActivate?: string; + commandScriptAfterActivate?: string; + githubRepoOwner: string; + githubRepo: string; +} + +interface Paths { + target: string; + sha: string; + releasePath: string; + activeReleasePath: string; +} + +interface ConnectionOptions { + host: string; + username: string; + port?: number | 22; + password?: string; + privateKey?: string; + passphrase?: string; +} + +async function deploy() { + try { + const inputs = getInputs(); + + const connectionOptions = getConnectionOptions(); + + validateConfig(inputs); + + validateConnectionOptions(connectionOptions); + + logInputs(inputs, connectionOptions); + + await checkSponsorship(inputs.githubRepoOwner); + + await sshOperations.connect(connectionOptions); + + await prepareDeployment(inputs); + + await activateRelease(inputs); + + log('Deployment completed successfully!'); + } catch (error: any) { + console.error(`Error: ${error.message}`); + core.setFailed(error.message); + } finally { + ssh.dispose(); + } +} + +function getConnectionOptions(): ConnectionOptions { + return { + host: process.env.HOST || core.getInput('host'), + username: process.env.REMOTE_USERNAME || core.getInput('username'), + port: parseInt(process.env.PORT || core.getInput('port') || '22'), + password: process.env.PASSWORD || core.getInput('password'), + privateKey: (process.env.SSH_KEY || core.getInput('ssh_key')).replace( + /\\n/g, + '\n' + ), // Replace escaped newlines with actual newlines + passphrase: process.env.SSH_PASSPHRASE || core.getInput('ssh_passphrase'), // Add passphrase + }; +} + +function getInputs(): Inputs { + return { + target: process.env.TARGET || core.getInput('target'), + sha: process.env.SHA || core.getInput('sha'), + deploy_branch: + process.env.GITHUB_DEPLOY_BRANCH || core.getInput('deploy_branch'), + envFile: process.env.ENV_FILE || core.getInput('env_file'), + commandScriptBeforeCheckFolders: + process.env.COMMAND_SCRIPT_BEFORE_CHECK_FOLDERS || + core.getInput('command_script_before_check_folders'), + commandScriptAfterCheckFolders: + process.env.COMMAND_SCRIPT_AFTER_CHECK_FOLDERS || + core.getInput('command_script_after_check_folders'), + commandScriptBeforeDownload: + process.env.COMMAND_SCRIPT_BEFORE_DOWNLOAD || + core.getInput('command_script_before_download'), + commandScriptAfterDownload: + process.env.COMMAND_SCRIPT_AFTER_DOWNLOAD || + core.getInput('command_script_after_download'), + commandScriptBeforeActivate: + process.env.COMMAND_SCRIPT_BEFORE_ACTIVATE || + core.getInput('command_script_before_activate'), + commandScriptAfterActivate: + process.env.COMMAND_SCRIPT_AFTER_ACTIVATE || + core.getInput('command_script_after_activate'), + githubRepoOwner: + process.env.GITHUB_REPO_OWNER || + github.context.payload.repository?.owner?.login || + '', + githubRepo: + process.env.GITHUB_REPO || github.context.payload.repository?.name || '', + }; +} + +function validateConfig(inputs: Inputs) { + if (!inputs.target) throw new Error('Target directory is required.'); + if (!inputs.sha) throw new Error('SHA is required.'); +} + +function validateConnectionOptions(connectionOptions: ConnectionOptions) { + if (!connectionOptions.host) throw new Error('Host is required.'); + if (!connectionOptions.username) throw new Error('Username is required.'); + if ( + connectionOptions.port && + (isNaN(connectionOptions.port) || + connectionOptions.port < 1 || + connectionOptions.port > 65535) + ) { + throw new Error('Port must be a valid number between 1 and 65535.'); + } +} + +function log(message: string) { + const timestamp = new Date().toISOString(); + console.log(`[${timestamp}] ${message}`); +} + +function logInputs(inputs: Inputs, connectionOptions: ConnectionOptions) { + log(`Host: ${connectionOptions.host}`); + log(`Target: ${inputs.target}`); + log(`SHA: ${inputs.sha}`); + log(`GitHub Repo Owner: ${inputs.githubRepoOwner}`); + // Omitting sensitive information like GitHub token and SSH key +} + +async function checkSponsorship(githubRepoOwner: string) { + try { + const response = await axios.post( + 'https://deployer.flowsahl.com/api/check-github-sponsorship', + { + github_username: githubRepoOwner, + } + ); + log('Thanks for sponsoring us :)'); + } catch (error: any) { + handleSponsorshipError(error); + } +} + +function handleSponsorshipError(error: any) { + if (error.response) { + if (error.response.status === 403) { + throw new Error( + '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].' + ); + } else if (error.response.status === 500) { + log( + 'An internal server error occurred while checking sponsorship, but the deployment will continue.' + ); + } else { + log( + `Sponsorship check failed with status ${error.response.status}: ${error.response.data}` + ); + throw new Error('Sponsorship check failed. Please try again later.'); + } + } else { + log('An unknown error occurred during the sponsorship check.'); + // throw error; + } +} + +const sshOperations = { + async connect({ + host, + username, + port, + password, + privateKey, + passphrase, + }: ConnectionOptions) { + log('Connecting to the server...'); + + const connectionOptions: ConnectionOptions = { + host, + username, + port: port, + privateKey: privateKey, + passphrase: passphrase, + password: password ? password : undefined, + }; + + try { + if (password) { + log('SSH key authentication password set Successfully'); + } + + await ssh.connect(connectionOptions); + } catch (keyError: any) { + log(`Failed to connect with SSH key: ${keyError.message}`); + throw keyError; + } + }, + + async execute(command: string) { + try { + const result = await ssh.execCommand(command); + if (result.stdout) log(result.stdout); + if (result.stderr) console.error(result.stderr); + if (result.code !== 0) + throw new Error(`Command failed: ${command} - ${result.stderr}`); + } catch (error: any) { + throw new Error( + `Failed to execute command: ${command} - ${error.message}` + ); + } + }, +}; + +async function prepareDeployment(inputs: Inputs) { + const paths = getPaths(inputs.target, inputs.sha); + + await runOptionalScript( + inputs.commandScriptBeforeCheckFolders, + 'before check folders' + ); + + await checkAndPrepareFolders(paths); + + await runOptionalScript( + inputs.commandScriptAfterCheckFolders, + 'after check folders' + ); + + await cloneAndPrepareRepository(inputs, paths); + + await syncEnvironmentFile(inputs.envFile, paths); + + await linkStorage(paths); + + await runOptionalScript(inputs.commandScriptAfterDownload, 'after download'); +} + +function getPaths(target: string, sha: string): Paths { + return { + target: target, + sha: sha, + releasePath: `${target}/releases/${sha}-${new Date().toISOString()}`, + activeReleasePath: `${target}/current`, + }; +} + +async function runOptionalScript( + script: string | undefined, + description: string +) { + if (script && script !== 'false') { + log(`Running script ${description}: ${script}`); + await sshOperations.execute(script); + } +} + +async function checkAndPrepareFolders(paths: any) { + log('Checking the folders...'); + const folders = [ + `${paths.target}/releases`, + `${paths.target}/storage`, + `${paths.target}/storage/app`, + `${paths.target}/storage/app/public`, + `${paths.target}/storage/logs`, + `${paths.target}/storage/framework`, + `${paths.target}/storage/framework/cache`, + `${paths.target}/storage/framework/sessions`, + `${paths.target}/storage/framework/views`, + ]; + await sshOperations.execute(`mkdir -p ${folders.join(' ')}`); + await sshOperations.execute(`rm -rf ${paths.target}/_temp_${paths.sha}`); + await sshOperations.execute(`rm -rf ${paths.target}/releases/${paths.sha}`); + await sshOperations.execute(`rm -rf ${paths.target}/${paths.sha}.zip`); +} + +async function cloneAndPrepareRepository(inputs: Inputs, paths: Paths) { + await runOptionalScript(inputs.commandScriptBeforeDownload, 'before clone'); + + const repoUrl = `git@github.com:${inputs.githubRepoOwner}/${inputs.githubRepo}.git`; + log(`Cloning Repo: ${repoUrl}`); + + await sshOperations.execute(` + cd ${inputs.target} && + rm -rf ${paths.releasePath} && + git clone -b ${inputs.deploy_branch} ${repoUrl} ${paths.releasePath} && + cd ${paths.releasePath} + `); +} + +async function syncEnvironmentFile(envFile: string | undefined, paths: Paths) { + if (envFile) { + log('Syncing .env file'); + await sshOperations.execute(`echo '${envFile}' > ${paths.target}/.env`); + await sshOperations.execute( + `ln -sfn ${paths.target}/.env ${paths.releasePath}/.env` + ); + } +} + +async function linkStorage(paths: Paths) { + log('Linking the current release with storage'); + await sshOperations.execute( + `ln -sfn ${paths.target}/storage ${paths.releasePath}/storage` + ); +} + +async function activateRelease(inputs: Inputs) { + const paths = getPaths(inputs.target, inputs.sha); + + await runOptionalScript( + inputs.commandScriptBeforeActivate, + 'before activate' + ); + + log('Activating the release'); + await sshOperations.execute( + `ln -sfn ${paths.releasePath} ${paths.activeReleasePath} && ls -1dt ${inputs.target}/releases/*/ | tail -n +4 | xargs rm -rf` + ); + + await runOptionalScript(inputs.commandScriptAfterActivate, 'after activate'); +} + +export { deploy }; + +// Automatically run the deploy function when the script is executed +deploy(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c36b5ce --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "ES2020", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} \ No newline at end of file