├── .gitignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── compile_code_and_modules.yml ├── dist ├── pagent.exe ├── build │ └── Release │ │ └── cpufeatures.node └── lib │ └── protocol │ └── crypto │ └── build │ └── Release │ └── sshcrypto.node ├── package.json ├── LICENSE.md ├── action.yml ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: DeployRepository 4 | -------------------------------------------------------------------------------- /dist/pagent.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeployRepository/zero-downtime-deployment/HEAD/dist/pagent.exe -------------------------------------------------------------------------------- /dist/build/Release/cpufeatures.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeployRepository/zero-downtime-deployment/HEAD/dist/build/Release/cpufeatures.node -------------------------------------------------------------------------------- /dist/lib/protocol/crypto/build/Release/sshcrypto.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeployRepository/zero-downtime-deployment/HEAD/dist/lib/protocol/crypto/build/Release/sshcrypto.node -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-deployer-src", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "ncc build index.js -m" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/DeployRepository/laravel-deployer-src.git" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/DeployRepository/laravel-deployer-src/issues" 19 | }, 20 | "homepage": "https://github.com/DeployRepository/laravel-deployer-src#readme", 21 | "dependencies": { 22 | "@actions/core": "^1.10.1", 23 | "@actions/exec": "^1.1.1", 24 | "@actions/github": "^6.0.0", 25 | "axios": "^1.7.4", 26 | "node-ssh": "^13.2.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/workflows/compile_code_and_modules.yml: -------------------------------------------------------------------------------- 1 | name: compile code and modules 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | paths-ignore: 9 | - 'dist/**' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | 19 | - name: Install dependencies 20 | run: npm install 21 | 22 | - name: Install ncc 23 | run: npm install -g @vercel/ncc 24 | 25 | - name: Build 26 | run: npm run build 27 | 28 | - name: Check for changes 29 | id: git-check 30 | run: | 31 | git diff --quiet || echo "::set-output name=changes::true" 32 | 33 | - name: Commit changes 34 | if: steps.git-check.outputs.changes == 'true' 35 | run: | 36 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 37 | git config --global user.name "github-actions[bot]" 38 | git add . 39 | git commit -m "build: ${{ github.sha }}" 40 | git push -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Proprietary License 2 | 3 | This repository and the code within are proprietary and are licensed under the following terms: 4 | 5 | 1. **License Grant**: Upon payment of the subscription fee, you are granted a non-exclusive, non-transferable, revocable license to use the code contained in this repository for the duration of your subscription. 6 | 7 | 2. **Restrictions**: You may not: 8 | - Redistribute, sublicense, or otherwise transfer the code or any portion thereof. 9 | - Use the code for any unlawful purpose or in violation of any local, state, federal, or international law. 10 | 11 | 3. **Termination**: This license is effective until terminated. Your rights under this license will terminate automatically without notice from the author if you fail to comply with any term(s) of this license. 12 | 13 | 4. **Disclaimer of Warranty**: The code is provided "as is" without warranty of any kind, either express or implied. 14 | 15 | 5. **Limitation of Liability**: In no event shall the author be liable for any special, incidental, indirect, or consequential damages whatsoever arising out of the use of or inability to use the code. 16 | 17 | For more details, please contact contact@deployrepository.com 18 | 19 | ## Student Tier 20 | 21 | We offer a special discounted rate for students to support learning and education. If you are a student, you can subscribe to our Student Tier for $1 a month. This tier grants you access to DeployRepository/zero-downtime-deployment with unlimited projects, deployments, and team members. 22 | 23 | To qualify for the Student Tier, please ensure you provide valid student credentials during the subscription process. -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Zero Downtime Deployment' 2 | description: 'Deploy project to server by ssh with zero downtime deployment.' 3 | 4 | inputs: 5 | host: 6 | description: 'Remote server host' 7 | required: true 8 | username: 9 | description: 'Remote server username' 10 | required: true 11 | port: 12 | description: 'Remote server port' 13 | required: true 14 | password: 15 | description: 'Remote server password' 16 | required: true 17 | target: 18 | description: 'Remote server target path' 19 | required: true 20 | sha: 21 | description: 'Git commit sha need to deploy (github.sha)' 22 | required: true 23 | github_token: 24 | description: 'Github token' 25 | required: true 26 | env_file: 27 | description: 'Environment file content to sync with .env file' 28 | required: false 29 | 30 | run_script_before_check_folders: 31 | description: 'Run script before checking folders' 32 | required: false 33 | default: 'false' 34 | run_script_after_check_folders: 35 | description: 'Run script after checking folders' 36 | required: false 37 | default: 'false' 38 | run_script_before_download: 39 | description: 'Run script before download release' 40 | required: false 41 | default: 'false' 42 | run_script_after_download: 43 | description: 'Run script after download release' 44 | required: false 45 | default: 'false' 46 | run_script_before_activate: 47 | description: 'Run script before activate release' 48 | required: false 49 | default: 'false' 50 | run_script_after_activate: 51 | description: 'Run script after activate release' 52 | required: false 53 | default: 'false' 54 | 55 | runs: 56 | using: 'node20' 57 | main: 'dist/index.js' 58 | 59 | branding: 60 | icon: "upload-cloud" 61 | color: "black" -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core'); 2 | const github = require('@actions/github'); 3 | const { NodeSSH } = require('node-ssh'); 4 | const axios = require('axios'); 5 | 6 | const ssh = new NodeSSH(); 7 | 8 | async function run() { 9 | try { 10 | const host = core.getInput('host'); 11 | const username = core.getInput('username'); 12 | const port = core.getInput('port'); 13 | const password = core.getInput('password'); 14 | const target = core.getInput('target'); 15 | const sha = core.getInput('sha'); 16 | const githubToken = core.getInput('github_token'); 17 | const envFile = core.getInput('env_file'); 18 | const runScriptBeforeCheckFolders = core.getInput('run_script_before_check_folders'); 19 | const runScriptAfterCheckFolders = core.getInput('run_script_after_check_folders'); 20 | const runScriptBeforeDownload = core.getInput('run_script_before_download'); 21 | const runScriptAfterDownload = core.getInput('run_script_after_download'); 22 | const runScriptBeforeActivate = core.getInput('run_script_before_activate'); 23 | const runScriptAfterActivate = core.getInput('run_script_after_activate'); 24 | 25 | // Log inputs for debugging 26 | await console.log(`Host: ${host}`); 27 | await console.log(`Target: ${target}`); 28 | await console.log(`SHA: ${sha}`); 29 | 30 | let githubRepoOwner = github.context.payload.repository.owner.login; 31 | await console.log('Checking if the user is a sponsor [' + githubRepoOwner + ']'); 32 | 33 | try { 34 | // Check if the user is a sponsor 35 | const isSponsor = await axios.post('https://deployrepository.com/api/check-github-sponsorship', { 36 | github_username: githubRepoOwner 37 | }); 38 | 39 | console.log('thanks for sponsoring us :)'); 40 | } catch (error) { 41 | if (error.response && error.response.status === 403) { 42 | throw new Error("You are not a sponsor, Please consider sponsoring us to use this action, https://github.com/sponsors/DeployRepository , Start sponsoring us and try again [1$ or more]"); 43 | } else if (error.response && error.response.status === 500) { 44 | console.error("An error occurred while checking sponsorship, but the deployment will continue."); 45 | } else { 46 | throw error; 47 | } 48 | } 49 | 50 | console.log("Connecting to the server..."); 51 | 52 | await ssh.connect({ 53 | host, 54 | username, 55 | port: port ? parseInt(port) : undefined, 56 | password 57 | }); 58 | 59 | const THIS_RELEASE_PATH = `${target}/releases/${sha}`; 60 | const ACTIVE_RELEASE_PATH = `${target}/current`; 61 | 62 | async function executeCommand(command) { 63 | command = command.replace(/\$THIS_RELEASE_PATH/g, THIS_RELEASE_PATH).replace(/\$ACTIVE_RELEASE_PATH/g, ACTIVE_RELEASE_PATH); 64 | 65 | const result = await ssh.execCommand(command); 66 | if (result.stdout) { 67 | await console.log(result.stdout); 68 | } 69 | if (result.stderr) { 70 | await console.error(result.stderr); 71 | } 72 | if (result.code !== 0) { 73 | throw new Error(`Command failed: ${command} - ${result.stderr}`); 74 | } 75 | } 76 | 77 | if (runScriptBeforeCheckFolders !== 'false') { 78 | await console.log(`Running script before check folders: ${runScriptBeforeCheckFolders}`); 79 | await executeCommand(runScriptBeforeCheckFolders); 80 | } 81 | 82 | await console.log("Checking the folders..."); 83 | await executeCommand(`mkdir -p ${target}/releases ${target}/storage ${target}/storage/app ${target}/storage/app/public ${target}/storage/logs ${target}/storage/framework ${target}/storage/framework/cache ${target}/storage/framework/sessions ${target}/storage/framework/views`); 84 | 85 | await executeCommand(`rm -rf ${target}/_temp_${sha}`); 86 | await executeCommand(`rm -rf ${target}/releases/${sha}`); 87 | await executeCommand(`rm -rf ${target}/${sha}.zip`); 88 | 89 | if (runScriptAfterCheckFolders !== 'false') { 90 | await console.log(`Running script after check folders: ${runScriptAfterCheckFolders}`); 91 | await executeCommand(runScriptAfterCheckFolders); 92 | } 93 | 94 | if (runScriptBeforeDownload !== 'false') { 95 | await console.log(`Running script before download: ${runScriptBeforeDownload}`); 96 | await executeCommand(runScriptBeforeDownload); 97 | } 98 | 99 | const repoUrl = `https://github.com/${github.context.repo.owner}/${github.context.repo.repo}`; 100 | await console.log(`Repo URL: ${repoUrl}`); 101 | await executeCommand(`cd ${target} && curl -sS -u ${github.context.repo.owner}:${githubToken} -L -o ${sha}.zip ${repoUrl}/archive/${sha}.zip && unzip ${sha}.zip -d _temp_${sha} && mkdir -p releases/${sha} && mv _temp_${sha}/${github.context.repo.repo}-${sha}/* ${target}/releases/${sha} && rm -rf _temp_${sha} ${sha}.zip`); 102 | 103 | if (envFile) { 104 | await console.log("Syncing .env file"); 105 | await executeCommand(`echo '${envFile}' > ${target}/.env`); 106 | await executeCommand(`ln -sfn ${target}/.env ${target}/releases/${sha}/.env`); 107 | } 108 | 109 | await executeCommand(`rm -rf ${target}/releases/${sha}/storage`); 110 | 111 | await console.log("Linking the current release with storage"); 112 | await executeCommand(`ln -sfn ${target}/storage ${target}/releases/${sha}/storage`); 113 | 114 | if (runScriptAfterDownload !== 'false') { 115 | await console.log(`Running script after download: ${runScriptAfterDownload}`); 116 | await executeCommand(runScriptAfterDownload); 117 | } 118 | 119 | if (runScriptBeforeActivate !== 'false') { 120 | await console.log(`Running script before activate: ${runScriptBeforeActivate}`); 121 | await executeCommand(runScriptBeforeActivate); 122 | } 123 | 124 | await console.log("Activating the release"); 125 | await executeCommand(`ln -sfn ${target}/releases/${sha} ${target}/current && ls -1dt ${target}/releases/*/ | tail -n +4 | xargs rm -rf`); 126 | 127 | if (runScriptAfterActivate !== 'false') { 128 | await console.log(`Running script after activate: ${runScriptAfterActivate}`); 129 | await executeCommand(runScriptAfterActivate); 130 | } 131 | 132 | } catch (error) { 133 | await console.log(`Error: ${error.message}`); 134 | core.setFailed(error.message); 135 | } finally { 136 | ssh.dispose(); 137 | } 138 | } 139 | 140 | run(); 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zero downtime deployment 2 | 3 | ## 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. 5 | 6 | ## Features 7 | - **Zero Downtime Deployment**: Ensure uninterrupted service during deployments. 8 | - **Multi-Technology Support**: Currently supporting Laravel, with more technologies being added. 9 | - **Easy Integration**: Simple setup and integration into your existing workflow. 10 | - **Flexible Deployment**: Suitable for projects of all sizes, from personal projects to enterprise applications. 11 | - **Custom Scripts**: Run custom scripts before and after key deployment steps. 12 | - **Secure**: Uses GitHub Secrets for sensitive data like server credentials and GitHub tokens. 13 | - **Environment File Sync**: Sync environment variables with the remote server. 14 | 15 | ## Example Usage 16 | You can use our [Setuper](https://deployrepository.com/setuper) by filing this form well be do all work, create workflow and secrets. 17 | or you can follow an example workflow configuration that uses the `DeployRepository/zero-downtime-deployment@v1.0.0` action: 18 | 19 | > [!IMPORTANT] 20 | > You need to add the following secrets to your repository: 21 | 22 | - **GH_TOKEN**: You'll need to provide the action with a **Personal Access Token (PAT)**, Permission we need is `repo` 23 | [How To create](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) 24 | - **REMOTE_HOST**: the host of the server 25 | - **REMOTE_USER**: the username of the server 26 | - **REMOTE_PASSWORD**: the password of the server 27 | - **REMOTE_PORT**: the port of the server 28 | - **ENV_FILE**: the content of the environment file to sync with `.env` 29 | 30 | ### Notes 31 | - The `target` input should be the path to the directory where the project will be deployed on the server (ex: /home/www/test.com). 32 | - The `target/current/public` directory should be the root of the domain. 33 | - Use the `${{ github.sha }}` variable to pass the commit SHA to the `sha` input. 34 | 35 | ### Start Use with Laravel 36 | To use this action in your workflow, you need to add the following step in your GitHub Actions workflow file (`.github/workflows/your-workflow.yml`): 37 | 38 | ```yaml 39 | name: deploy to server 40 | 41 | concurrency: 42 | group: production 43 | cancel-in-progress: true 44 | 45 | on: 46 | release: 47 | types: [released] 48 | 49 | jobs: 50 | deployment: 51 | runs-on: ubuntu-latest 52 | environment: production 53 | 54 | steps: 55 | - name: Deploy Laravel project 56 | uses: DeployRepository/zero-downtime-deployment@v1.0.0 57 | with: 58 | host: ${{ secrets.REMOTE_HOST }} 59 | username: ${{ secrets.REMOTE_USER }} 60 | password: ${{ secrets.REMOTE_PASSWORD }} 61 | port: ${{ secrets.REMOTE_PORT }} 62 | target: '/home/www/test.com' # Path to the directory where the project will be deployed 63 | sha: ${{ github.sha }} 64 | github_token: ${{ secrets.GH_TOKEN }} 65 | env_file: ${{ secrets.ENV_FILE }} 66 | # run_script_before_check_folders: ls -la 67 | # run_script_after_check_folders: ls -la 68 | # run_script_before_download: ls -la 69 | run_script_after_download: | 70 | cd $THIS_RELEASE_PATH 71 | composer install --prefer-dist 72 | php artisan migrate --force 73 | php artisan storage:link 74 | # run_script_before_activate: ls -la 75 | run_script_after_activate: | 76 | cd $ACTIVE_RELEASE_PATH 77 | php artisan optimize 78 | ``` 79 | For test, go and create your first release, you can see process of deploy in tab of actions in your repo 80 | 81 | ## Support 82 | If you need help or have any questions, feel free to reach out to me at [Discussion](https://github.com/DeployRepository/zero-downtime-deployment/discussions). 83 | 84 | ## This GitHub Action Is Sponsorware 💰💰💰 85 | Originally, this github action was only available to my sponsors on GitHub Sponsors until I reached 100 sponsors. 86 | 87 | 88 | 89 | Enjoy, and thanks for the support! ❤️ [Become a sponsor](https://github.com/sponsors/DeployRepository) 90 | 91 | Learn more about Sponsorware at github.com/sponsorware/docs 💰. 92 | 93 | ## Custom Scripts 94 | You can provide custom scripts to run at various stages of the deployment. Below are the supported stages where you can run your scripts: 95 | 96 | - **Before Checking Folders**: `run_script_before_check_folders` 97 | - **After Checking Folders**: `run_script_after_check_folders` 98 | - **Before Downloading Release**: `run_script_before_download` 99 | - **After Downloading Release**: `run_script_after_download` 100 | - **Before Activating Release**: `run_script_before_activate` 101 | - **After Activating Release**: `run_script_after_activate` 102 | 103 | ## Troubleshooting 104 | If you encounter issues, check the GitHub Actions logs for detailed error messages. Ensure that: 105 | - SSH credentials are correct. 106 | - The target directory on the remote server has the necessary permissions. 107 | - The specified Git commit SHA exists in your repository. 108 | 109 | ## Inputs 110 | 111 | | Name | Description | Required | Default | 112 | |------------------------------|------------------------------------------------|----------|---------| 113 | | `host` | Remote server host | Yes | | 114 | | `username` | Remote server username | Yes | | 115 | | `password` | Remote server password | Yes | | 116 | | `port` | Remote server port | Yes | `22` | 117 | | `target` | Remote server target path | Yes | | 118 | | `sha` | Git commit SHA to deploy (use `${{ github.sha }}`) | Yes | | 119 | | `github_token` | GitHub token | Yes | | 120 | | `env_file` | Content of the environment file to sync with `.env` | No | | 121 | | `run_script_before_check_folders` | Script to run before checking folders | No | `false` | 122 | | `run_script_after_check_folders` | Script to run after checking folders | No | `false` | 123 | | `run_script_before_download` | Script to run before downloading release | No | `false` | 124 | | `run_script_after_download` | Script to run after downloading release | No | `false` | 125 | | `run_script_before_activate` | Script to run before activating release | No | `false` | 126 | | `run_script_after_activate` | Script to run after activating release | No | `false` | 127 | 128 | ## Security 129 | Ensure that sensitive data such as server credentials and GitHub tokens are stored securely using GitHub Secrets. 130 | 131 | ## License 132 | 133 | This repository and the code within are proprietary. Access is granted to sponsors only. Please see the [LICENSE](LICENSE.md) file for more information. 134 | 135 | ## Contribution and Issues 136 | If you encounter any issues or have suggestions for improvements, please open an issue or submit a pull request on the [GitHub repository](https://github.com/DeployRepository/zero-downtime-deployment). 137 | 138 | This documentation should provide a clear and comprehensive guide for users to get started with your GitHub Action. 139 | --------------------------------------------------------------------------------