GitHub Actions CI/CD Tutorial Series — Part 4
In Part 3 of this tutorial series, we covered the following steps:
*) Created the Dockerfile
*) Created folder and YAML file for the CI/CD part
*) Configured Slack Webhooks to add as a job on our YAML file
If you missed Part 3, you can find it here: Part 3
Continue expanding the YAML file — Yalla | يلا
Our current cicd.yml
file looks like this:
name: Build & Deployment
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
name: Build Project
runs-on: ubuntu-22.04
steps:
- name: Slack Notification Ci/Cd started
uses: 8398a7/action-slack@v3.15.1
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
text: 'CI/CD started :eyes: In the Name of God the Merciful, the Compassionate / Bismillahir Rahmanir Raheem /بسم الله الرحمن الرحيم'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Next, add the steps to check out the repository and validate Gradle Wrapper:
- name: Checkout Repository
uses: actions/checkout@v3.5.1
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1.0.6
By the way you can find the steps after actions/
like checkout@v3
& v3wrapper-validation-action@v1
here at GitHub Marketplace .
Then add the step to running the Super-Linter, which is a popular linting tool that helps identify and report code issues across multiple languages and file formats.
- name: Run Super-Linter
uses: github/super-linter@v4.10.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VALIDATE_MARKDOWN: false
VALIDATE_SQLFLUFF: false
env
: This section defines the environment variables for the action:
GITHUB_TOKEN
: The GitHub token is automatically generated by GitHub Actions and provides the action with a token to interact with the GitHub API. The token is dynamically set using${{ secrets.GITHUB_TOKEN }}
.VALIDATE_MARKDOWN
: By setting this variable tofalse
, the Super-Linter will not lint Markdown files during the workflow.VALIDATE_SQLFLUFF
: By setting this variable tofalse
, the Super-Linter will not run SQLFluff, a SQL linter, during the workflow.
After that add again a step to post something in the Slack channel so other people will also know that the Super-Linter check is finished:
- name: Super Linter Slack Notification
uses: 8398a7/action-slack@v3.15.1
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
text: 'Super Linter finished... :heavy_check_mark: nice / lateef / لطيف'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
The next step would be to log in on Docker Hub:
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
Here we add again secrets to GitHub, so basically you need to have an account on https://hub.docker.com/ and add your username to GitHub secrets.
Add again a step for Slack to post the current status on the channel:
- name: Docker Hub Slack Notification
uses: 8398a7/action-slack@v3.15.1
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
text: 'Docker Hub Login... :lock: okay / tayyib / طيب'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Now, we need a step for building and pushing a Docker image to a Docker registry (in this case, Docker Hub):
- name: Build and push Docker image
uses: docker/build-push-action@v4.0.0
with:
push: true
tags: habibicoding/task-app-api:latest
context: .
with
: This section defines the input parameters for the action:
push
: Setting this totrue
enables pushing the built Docker image to the specified registry. If set tofalse
, the image would only be built but not pushed.tags
: Specifies the image tags that will be assigned to the built image. In this case, the image is tagged ashabibicoding/task-app-api:latest
, wherehabibicoding
is the Docker Hub username,task-app-api
is the repository name, andlatest
is the tag for the most recent version of the image.context
: This parameter specifies the build context, which is the path to the directory containing the Dockerfile and other files required for building the image. In this case, the.
(dot) represents the current working directory.
Time to add again a step for a Slack channel notification:
- name: Notify Docker push results
uses: 8398a7/action-slack@v3.15.1
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
text: ':white_check_mark: pushed habibicoding/task-app-api:latest to Docker Hub... https://hub.docker.com/repository/docker/habibicoding/task-app-api'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Add now an line break and start the second job at the very left of the file like build
.
Adding our second deploy job
Now we are finished with the build
job so we can add our second and last job the deploy
:
deploy:
name: Deploy Project
needs: build
if: github.event_name == 'push'
runs-on: ubuntu-22.04
steps:
needs
: Specifies that this job depends on the successful completion of the build
job. The deploy
job will only be executed if the build
job has successfully completed.
if
: This condition specifies that the deploy
job will only be executed if the triggering event is a 'push' to the repository. If the workflow is triggered by any other event (e.g., pull request), the deploy
job will be skipped.
runs-on
: Specifies the type of runner on which the job will be executed. In this case, the job will run on an Ubuntu 22.04 runner.
This job configuration ensures that the deployment process only occurs when the build job completes successfully, and the triggering event is a push to the repository. The job will run on an Ubuntu 22.04 runner, providing a consistent environment for executing the deployment steps.
Next, add again a Slack step for notifying our channel:
- name: Notify deployment started
uses: 8398a7/action-slack@v3.15.1
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
text: 'Linode deployment started... :crossed_fingers: so God will / in shaAllah / ان شاء الله خير'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
After that set up your SSH agent with webfactory
:
- name: Setup SSH agent
uses: webfactory/ssh-agent@v0.7.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
When the workflow reaches this step, it will set up an SSH agent and load the provided private key into it. This allows the workflow to authenticate with the Linode remote server using the corresponding public key, which will be added to the authorized_keys
file on the Linode Ubuntu instance. This step is particularly useful for securely deploying code, managing infrastructure, or performing other tasks that require SSH authentication in a GitHub Actions workflow.
Then create Ed25519 SSH key pair and store them in a specific file on your locale computer. You will use the private key on the GitHub Actions pipeline and the public key on the Linode Ubuntu instance.
Just create first a new folder:
mkdir pipeline-linode
Enter the command to generate the Ed25519 SSH inside the pipeline-linode
folder:
ssh-keygen -t ed25519 -f ~/pipeline-linode/pipeline-linode -C "habibicoding@aol.com"
VERY VERY IMPORTANT DON’T ENTER A PASSPHRASE FOR THIS KEY GENERATION!!!
If you type a passphrase the GitHub Actions pipeline will not be able to connect to the Linode Ubuntu instance, because it can’t give at connection time the passphrase to our server.
JUST PRESS ENTER AT “Enter passphrase (empty for no passphrase):” & “Enter same passphrase again:”
Time to navigate to your pipeline-linode
folder and list all files:
cd pipeline-linode && ls -la
First copy the pipeline-linode.pub
file inside your authorized_keys
file of your Linode Ubuntu instance:
pbcopy < pipeline-linode.pub
Second use SSH again to connect to your server:
ssh {your-user}@{your-linode-ip} -p 1022
Navigate to your .ssh/
folder and open with sudo
vim/nano the authorized_keys
file:
cd .ssh/
sudo vim authorized_keys
After that add in the file a new line and paste your public key:
Save and close the file, then check if it is there:
cat authorized_keys
Logout from your server and you should be again in your locale pipeline-linode
folder and list all files again:
exit
ls -la
Now, we need to copy the private key, which has only the name pipeline-linode
:
pbcopy < pipeline-linode
Open in your browser again GitHub and go again to the settings section of your repository and add another secret:
IMPORTANT the name of the secret needs exactly to be SSH_PRIVATE_KEY because we named it so in the pipeline:
After pressing “Add secret” you should see the new secret in your list:
Jump back again now to the cicd.yml file and add the step to deploy our application to the server:
- name: Deploy to Droplet
run: |
ssh -o StrictHostKeyChecking=no -p ${{ secrets.DROPLET_PORT }} ${{ secrets.DEPLOYMENT_USER }}@${{ secrets.DEPLOYMENT_HOST }} "\
docker-compose pull && \
docker-compose up -d && \
docker system prune -af"
run
: This section contains a series of shell commands that will be executed in the runner's environment. The commands are executed using a single SSH command, connecting to the remote server with specified options and credentials.
Here’s an explanation of the SSH command and its components:
ssh
: The SSH command, which establishes a secure connection to the remote server.-o StrictHostKeyChecking=no
: This option disables strict host key checking, allowing the connection even if the remote server's public key is not in the local known_hosts file. This can be useful for automation but may pose a security risk as it exposes the connection to potential man-in-the-middle attacks.-p ${{ secrets.DROPLET_PORT }}
: Specifies the port to use when connecting to the remote server. The port number is stored as a secret in the repository settings.${{ secrets.DEPLOYMENT_USER }}@${{ secrets.DEPLOYMENT_HOST }}
: Specifies the username and hostname (or IP address) of the remote server. Both values are stored as secrets in the repository settings.
The SSH command is followed by a series of commands enclosed in double quotes and separated by backslashes. These commands will be executed on the remote server:
docker-compose pull
: Pulls the latest version of the Docker images specified in thedocker-compose.yml
file.docker-compose up -d
: Starts or updates the services defined in thedocker-compose.yml
file in detached mode (running in the background).docker system prune -af
: Cleans up unused Docker objects, such as stopped containers, unused networks, and dangling images. The-a
flag removes all unused images, not just dangling ones, and the-f
flag forces the cleanup without prompting for confirmation.
With that, we conclude the fourth part of this tutorial series. If you found it useful and informative, give it a clap. Here is Part 5.
Don’t forget to check out the video version of this article on our YouTube channel at https://www.youtube.com/@habibicoding.