Sitemap

Understanding the CI/CD and Software development proccess as a QA

8 min readSep 10, 2024

In the software development lifecycle, Quality Assurance (QA) plays a crucial role in ensuring that the code delivered is reliable and meets the expected standards. In a CI/CD (Continuous Integration/Continuous Deployment) pipeline, QA helps to identify and address issues early, automate testing, and maintain high code quality.

My Journey with QA and CI/CD

I decided to create my own lab and purchased the domain serhatozdursun.com to build a personal website while experimenting with CI/CD. I’ve always believed that hands-on experience is the best teacher.

Initially, while developing my website using React with TypeScript, I didn’t prioritize test automation or functional testing. Like many early-stage projects, development took precedence over quality assurance. However, I quickly realized that without proper QA practices, I kept breaking features that were already working. To address this, I implemented unit tests using React’s testing libraries and integrated ESLint for static code analysis, ensuring code quality and preventing legacy issues.

I stored my code on GitHub, and you can check out the repository here. For hosting, I used a simple XCloud server. Though it was quite basic — accessible only through the provider’s console, with no copy-paste functionality — it was the most cost-effective solution. I installed Git, Node.js, and Yarn (my package manager for TypeScript) on the server. To publish the website, I chose Nginx for its simplicity and used Certbot to secure the site with a free SSL certificate.

Implementing Branch Protection and Automation

To safeguard my production application, I set up branch protection rules on the main branch of my GitHub repository. These rules prevent deletions, non-fast-forward merges, and ensure that code changes meet specific criteria before being merged. Here’s a sample of the branch protection configuration (with sensitive data removed):

{
"name": "protection",
"target": "main",
"source": "serhatozdursun/resume",
"rules": [
{
"type": "deletion"
},
{
"type": "non_fast_forward"
},
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 0
}
},
{
"type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": true,
"do_not_enforce_on_create": false,
"required_status_checks": [
{
"context": "Analyze (javascript)",
"integration_id": ****
},
{
"context": "Analyze (typescript)",
"integration_id": ****
},
{
"context": "build_and_test",
"integration_id": ****
}
]
}
}
]
}

Additionally, I configured GitHub Actions to ensure that code pushed to the main branch does not break the build or fail unit tests. I also integrated CodeQL for static code analysis to identify security vulnerabilities and maintain code quality.

Here is the yml file that I've used to have it

name: CodeQL Static Code Analysis

on:
pull_request:
branches: [main]

jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write

strategy:
matrix:
language: ['typescript', 'javascript']

steps:
- name: Checkout repository
uses: actions/checkout@v3
# This step checks out your repository code so that it can be analyzed.
# It ensures that the workflow has access to the code in the current pull request.

- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# This step initializes the CodeQL analysis tool.
# It sets up the CodeQL environment with the specified languages from the matrix (TypeScript and JavaScript in this case).

- name: Build code
uses: actions/setup-node@v3
with:
node-version: '18'
# This step sets up Node.js environment with the specified version (18).
# It's needed to run the build process and ensure that the correct version of Node.js is used.

- run: |
if [ -f package-lock.json ]; then
npm install
elif [ -f yarn.lock ]; then
yarn install
fi
# This step installs the project dependencies.
# It checks if `package-lock.json` or `yarn.lock` exists and runs `npm install` or `yarn install` accordingly to ensure all dependencies are installed.

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
# This step runs the CodeQL analysis on the checked-out code.
# It scans the code for security vulnerabilities and other issues according to the CodeQL rules.

Expanding QA Practices with End-to-End Testing

Realizing that unit tests alone weren’t sufficient, I aimed to implement end-to-end (E2E) testing to further ensure the quality of my application. Although I am more experienced with Java, I chose to use Python with pytest and Selenium for this project to broaden my skill set. The test automation project, which is available here, currently includes a few test cases, with plans to expand and add more QA gates.

To automate the E2E testing, I evaluated several options:

- Scheduled EOD runs, with alerts sent via email or Slack in case of any issues.
- Running tests after code merges, with alerts for any failures.

However, I found these approaches inefficient. Scheduled EOD runs could result in stale code not being tested, while running tests post-merge risks introducing untested code into the main branch.

Instead, I opted for a more effective approach by creating a GitHub Action that clones the project into a container, deploys the app locally at `http://localhost:3000`, and runs the tests in this environment. This ensures that the tests run in a production-like setting and accurately reflect the application’s state. The automation project is parameterized for different environments, allowing it to run tests against any specified base URL and browser.

Create GitHub Actions Workflow:

To automate the deployment process and run E2E tests, I created a GitHub Actions workflow. Here is the YAML configuration for the workflow:

name: UI Automation Test for Chrome

on:
pull_request:
branches:
- main

jobs:
build_and_test:
runs-on: ubuntu-latest

steps:
- name: Checkout resume repository
uses: actions/checkout@v3

- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'

- name: Install dependencies and build
run: |
yarn install
yarn build

- name: Start the application
run: |
yarn start &
env:
PORT: 3000

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'

- name: Install Chrome
run: |
sudo apt update
sudo apt install -y wget gnupg
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo apt install -y ./google-chrome-stable_current_amd64.deb

- name: Install ChromeDriver
run: |
CHROMEDRIVER_VERSION=$(curl -sS https://chromedriver.storage.googleapis.com/LATEST_RELEASE)
wget https://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip
unzip chromedriver_linux64.zip
sudo mv chromedriver /usr/local/bin/chromedriver
sudo chmod +x /usr/local/bin/chromedriver

- name: Clone the test repository
run: |
git clone https://github.com/serhatozdursun/serhatozdursun-ui-tests.git
env:
GITHUB_TOKEN: ${{ secrets.UI_TEST_TOKEN }}

- name: Install Python dependencies
working-directory: serhatozdursun-ui-tests
run: |
pip install -r requirements.txt

- name: Run tests
working-directory: serhatozdursun-ui-tests
id: run_tests
run: |
pytest --browser chrome --base_url http://localhost:3000 --html=reports/html/report.html --junitxml=reports/report.xml

- name: Upload HTML report
if: always()
uses: actions/upload-artifact@v4
with:
name: html-report
path: serhatozdursun-ui-tests/reports/html
continue-on-error: true

- name: Upload XML report
if: always()
uses: actions/upload-artifact@v4
with:
name: xml-report
path: serhatozdursun-ui-tests/reports/report.xml
continue-on-error: true

Addressing Automation Challenges

Despite setting up automated testing, one issue remained: updating the production server was not fully automated. The process required manually logging into the server, stopping the React application, pulling the latest changes, installing new packages if any, rebuilding the React app with `yarn build`, and then restarting the server with `yarn start`. This was far from an ideal CI/CD process.

To address this, I explored using SSH within GitHub Actions for a more comprehensive automation solution. Here’s how I approached it:

1. Generate an SSH Key Pair (On Your Local Machine or Server)

I generated an SSH key pair for connecting to the server.

ssh-keygen -t rsa -b 4096 -C "user@example.com"

This generated two files:

  • Private Key: ~/.ssh/id_rsa (keep this private and secure)
  • Public Key: ~/.ssh/id_rsa.pub (this will be added to your server)

2. Add the Public SSH Key to The Server

Once I have the id_rsa.pub file (the public key), I added this to the server’s authorized_keys to allow GitHub Actions to connect.

  1. Copy the Public Key to the Server:
  • Run the following command to copy the public key to your server:
ssh-copy-id user@the.server.ip.address
  • This automatically added the contents of id_rsa.pub to the server’s ~/.ssh/authorized_keys file.

3. Add the Private SSH Key to GitHub Secrets

Now, you need to add the private SSH key (id_rsa) to GitHub so that GitHub Actions can use it to connect to your server.

Copy the contents of the private SSH key (id_rsa):

cat ~/.ssh/id_rsa

In your GitHub repository:

  • Go to Settings > Secrets and variables > Actions.
  • Click New repository secret.
  • Add a new secret:
  • Name: SSH_PRIVATE_KEY
  • Value: Paste the contents of your id_rsa file (the private key).

Add a secret for your server IP:

  • Add a secret for your server IP:
  • Name: SERVER_HOST
  • Value: the.server.ip.address

Add a secret for your SSH username:

  • Name: SERVER_USER
  • Value: the-username

4. Create GitHub Actions Workflow:

Create a YAML file for GitHub Actions to use the correct SSH secrets and configuration to deploy to your server.

name: Deploy Website

on:
push:
branches:
- main

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Deploy to server via SSH
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
# Define the target directory
TARGET_DIR="repo/resume"

# Print the current directory
echo "Current directory: $(pwd)"

# Check if we are in the correct directory
CURRENT_DIR=$(basename $(pwd))
if [ "$CURRENT_DIR" != "$(basename $TARGET_DIR)" ]; then
# Navigate to the parent directory and then to the target directory
cd ~ || exit 1
cd $TARGET_DIR || exit 1
else
echo "Already in the correct directory."
fi

# Export correct node binary and pm2 path
export PATH=/root/.nvm/versions/node/v21.6.2/bin:$PATH

# set nvm defult
nvm alias default 21

# Stop the app managed by pm2, if running
if pm2 stop serhatozdursun_website; then
echo "PM2 process 'serhatozdursun_website' stopped successfully."
else
echo "Failed to stop the pm2 process 'serhatozdursun_website'. Exiting."
exit 1
fi

# Ensure no other process is running on port 3000
PID=$(sudo netstat -tulnp | grep :3000 | awk '{print $7}' | cut -d'/' -f1)
if [ -n "$PID" ]; then
echo "Stopping process with PID $PID on port 3000."
sudo kill -9 $PID
echo "Process stopped."
else
echo "No process found on port 3000."
fi

# Remove .next directory
rm -rf .next || exit 1

# Pull the latest code from the main branch
if git pull origin main; then
echo "Code pulled successfully."
else
echo "Failed to pull code from the repository. Exiting."
exit 1
fi

# Install any new dependencies
if yarn install; then
echo "Dependencies installed successfully."
else
echo "Failed to install dependencies. Exiting."
exit 1
fi

# Build the app
if yarn build; then
echo "App built successfully."
else
echo "Failed to build the app. Exiting."
exit 1
fi

# Start the app with pm2
if pm2 start yarn --name "serhatozdursun_website" -- start; then
echo "PM2 process 'serhatozdursun_website' started successfully."
else
echo "Failed to start the pm2 process 'serhatozdursun_website'. Exiting."
exit 1
fi

# Save the pm2 process list
if pm2 save; then
echo "PM2 process list saved successfully."
else
echo "Failed to save the pm2 process list. Exiting."
exit 1
fi

Through this journey, I deepened my appreciation for the critical role QA plays in the development lifecycle. I gained hands-on experience in web hosting, server setup, and deployment automation. The process of troubleshooting, breaking, and fixing my own code while establishing robust testing and deployment pipelines highlighted the indispensable role of QA in ensuring that applications are not only functional but also production-ready. This experience underscored the value of a meticulous approach to quality assurance and its impact on delivering high-quality, reliable software.

For more insights and to explore the implementation details, check out the following resources:

--

--

Serhat Ozdursun
Serhat Ozdursun

Written by Serhat Ozdursun

QA Automation Engineer at Index

No responses yet