CI/CD (Continuous Integration/Continuous Deployment) is a software development methodology that aims to simplify and speed up the process of delivering changes and new features to the user.
CI, or Continuous Integration, is the process of automatically merging code from different developers into one common branch. This process usually includes automated tests, which helps detect and fix bugs faster.
CD, or continuous delivery, is the next step after CI. It is the process of automatically deploying code to production. This means that once your code is ready and all tests are passed, it automatically moves to the production stage.
Advantages of CI/CD
1- Faster time to market
With CI/CD, you can automate various stages of development, thereby speeding up the software delivery process.
2. higher product quality
CI/CD involves frequent code testing, which helps in detecting and fixing bugs early in the development process.
3. Increased team productivity
By automating routine processes, developers can focus on writing code rather than integrating and delivering it.
4. Quick rollback
If something goes wrong, you can quickly roll back to a previous version of code, reducing risk to your users.
5. Accountability
With CI/CD, every code change can be traced back to a specific commit and developer, improving transparency and accountability.
CI/CD is a powerful tool for development teams that can greatly improve the development process and the delivery of high quality software.
Customize CI/CD with GitHub Actions
GitHub Actions is a GitHub functionality that allows you to automate, customize, and execute software without intermediaries. Today, we'll look at how to customize continuous integration and delivery (CI/CD) using GitHub Actions.
Creating a workflow file
Create a new file in the .github/workflows directory in your repository. Name it dev.yml, for example. This file will contain all the settings for your CI/CD workflow. I usually name the file by the name of the branch it is attached to.
An example of a file for Laravel + Vue.js that I usually use in my projects
name: Continuous Integration and Deployment
on:
push:
branches: [ dev ]
pull_request:
branches: [ dev ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
- name: Install Dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Setup Laravel
run: |
cp .env.example .env
php artisan config:clear
php artisan cache:clear
php artisan key:generate
- name: Composer Lint
run: ./vendor/bin/phplint --no-cache
- name: Composer Audit
run: composer audit
- name: Composer Validate
run: composer validate --no-check-all --strict
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install Node.js Dependencies
run: npm install
- name: Build Vue.js
run: npm run build
- name: Run PHPUnit Tests
run: ./vendor/bin/phpunit --colors=always
- name: Lint PHP Code
run: ./vendor/bin/pint --test
- name: PHPStan
run: ./vendor/bin/phpstan analyse
deploy:
needs: [build]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Setup Environment File
run: |
cp .env.example .env
sed -i 's/DB_USERNAME=root/DB_USERNAME=${{ secrets.DB_USERNAME }}/g' .env
sed -i 's/DB_PASSWORD=/DB_PASSWORD=${{ secrets.DB_PASSWORD }}/g' .env
sed -i 's|VTIGER_URL=http://localhost:8080|VTIGER_URL=${{ secrets.VTIGER_URL }}|g' .env
sed -i 's/VTIGER_USERNAME=admin/VTIGER_USERNAME=${{ secrets.VTIGER_USERNAME }}/g' .env
sed -i 's/VTIGER_ACCESS_KEY=1234567890/VTIGER_ACCESS_KEY=${{ secrets.VTIGER_ACCESS_KEY }}/g' .env
- name: Setup Laravel
run: |
php artisan config:clear
php artisan cache:clear
php artisan key:generate
php artisan storage:link
- name: Permission setup
run: |
chmod -R 777 storage bootstrap/cache
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install Node.js Dependencies
run: npm install
- name: Build Vue.js
run: npm run build
- name: Clean Up
run: rm -rf node_modules
- name: copy file via ssh key
uses: appleboy/scp-action@v0.1.4
with:
host: ${{ secrets.SERVER_IP }}
username: ${{ secrets.SERVER_USERNAME }}
password: ${{ secrets.PASSWORD }}
port: 22
key: ${{ secrets.DEPLOY_KEY }}
source: "./"
target: "~/path_to_project"
- name: Run laravel commands
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_IP }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.DEPLOY_KEY }}
port: 22
script:
chown -R www-root ~/path_to_project && cd ~/path_to_project && php artisan cache:clear && php artisan config:clear && php artisan view:clear && php artisan migrate
Example you can find also in github: https://github.com/semelyanov86/realty-objects/blob/dev/.github/workflows/laravel.yml
The dev.yml file specifies the events that should trigger the CI/CD process. In our example, it is configured so that it is triggered whenever a push or pull request is made to the dev branch:
name: Continuous Integration and Deployment
on:
push:
branches: [ dev ]
pull_request:
branches: [ dev ]
Jobs are individual steps in your CI/CD process. They can be run in parallel or sequentially, depending on your requirements. Below is an example of a task for building a project:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
# ...
Here we basically start building the project - get the sources from github, install php.
Then we need to install dependencies and run various checks:
- name: Install Dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Composer Lint
run: ./vendor/bin/phplint --no-cache
- name: Composer Audit
run: composer audit
- name: Composer Validate
run: composer validate --no-check-all --strict
- name: Run PHPUnit Tests
run: ./vendor/bin/phpunit --colors=always
# ...
Before submitting changes to the server, we need to verify that our code meets all the required standards, namely:
- automatic code refactoring and updating (Rector)
- codestyle - Pint
- static analysis - Larastan
- vulnerability check (composer audit command)
- validity of composer.json (the composer validate command)
- Absence of unused packages. If a package is not used, no need to keep it on the system (composer-unused).
- control of dependency cohesion/chaining and direction (Deptrac).
- Linter (phplint).
- And of course tests (PHPUnit).
Deploy to the server
When all the tests are passed, we can move on to the server deploy:
deploy:
needs: [build]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
# ...
- name: copy file via ssh key
uses: appleboy/scp-action@v0.1.4
with:
host: ${{ secrets.SERVER_IP }}
username: ${{ secrets.SERVER_USERNAME }}
password: ${{ secrets.PASSWORD }}
port: 22
key: ${{ secrets.DEPLOY_KEY }}
source: "./"
target: "~/path_to_project"
- name: Run laravel commands
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_IP }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.DEPLOY_KEY }}
port: 22
script:
chown -R www-root ~/path_to_project && cd ~/path_to_project && php artisan cache:clear && php artisan config:clear && php artisan view:clear && php artisan migrate
There are a number of strategies for deployment. You can build on the server itself, accept changes from the git, install dependencies, build the frontend. You can do the opposite, build everything on gitHub, send the files via ftp. In our example, we go with the second option, since we may not always have node, git or composer installed on the server. After sending the changes, we still need to connect to the server via ssh, clear the cache, and run the migrations.
To securely store sensitive data such as SSH keys and database access parameters, use GitHub secrets. You can add them in the "Secrets" section of your repository settings. For example, if you look at this command - ssh ${{ secrets.SERVER_USERNAME }}@${{ secrets.SERVER_IP }}} , then here we connect to the server using the variables from the github - SERVER_USERNAME and SERVER_IP.
To authorize on the server, we need to add a private key as secrets on the github, and on the server we need to add a public key to the authorized_keys file.
You will probably need to fill the .env file with the necessary keys. In our example, we copy the .env.example file to .env and then replace the strings with the github secrets:
- name: Setup Environment File
run: |
cp .env.example .env
sed -i 's/DB_USERNAME=root/DB_USERNAME=${{ secrets.DB_USERNAME }}/g' .env
sed -i 's/DB_PASSWORD=/DB_PASSWORD=${{ secrets.DB_PASSWORD }}/g' .env
sed -i 's|VTIGER_URL=http://localhost:8080|VTIGER_URL=${{ secrets.VTIGER_URL }}|g' .env
sed -i 's/VTIGER_USERNAME=admin/VTIGER_USERNAME=${{ secrets.VTIGER_USERNAME }}/g' .env
sed -i 's/VTIGER_ACCESS_KEY=1234567890/VTIGER_ACCESS_KEY=${{ secrets.VTIGER_ACCESS_KEY }}/g' .env
This is a basic CI/CD setup using GitHub Actions. Remember that every project is unique, and you can always customize this example to meet your specific needs. Happy coding!