DEV Community

Cover image for Building an Automated CI/CD Pipeline with GitHub Actions: From Testing to Continuous Deployment
Rahimah Sulayman
Rahimah Sulayman

Posted on

Building an Automated CI/CD Pipeline with GitHub Actions: From Testing to Continuous Deployment

Introduction

Modern software delivery requires more than writing code and pushing it to a repository. Applications need reliable processes for testing, building, and deploying changes with minimal manual intervention.

In this project, I built an automated CI/CD pipeline using GitHub Actions. What started as a basic workflow evolved into a multi-stage pipeline that validates repository changes, executes a custom reusable composite action, generates a dynamic status card, and deploys a website to GitHub Pages.

During implementation, I encountered and resolved several issues, including a Bash syntax error, an intentional CI validation failure, a temporary GitHub Actions server error, and a YAML configuration mistake.

This article documents the implementation, troubleshooting process, and final deployment.

Project Overview

The objective was to build a working CI/CD pipeline that automates repository validation, website generation, and deployment.

The project covered five major areas:

  1. Creating a GitHub repository and configuring a basic workflow.
  2. Implementing automated repository checks.
  3. Building and integrating a custom GitHub composite action.
  4. Creating a multi-stage CI/CD pipeline.
  5. Deploying the generated website to GitHub Pages.

Technologies used:

  • Git and GitHub
  • GitHub Actions
  • YAML
  • Bash
  • HTML and CSS
  • GitHub Pages

1. Repository Setup and the First GitHub Actions Workflow

I started by creating a public GitHub repository named pipeit-tutorial.

creating a public GitHub repository named  raw `pipeit-tutorial` endraw

The first task was to establish a basic GitHub Actions workflow and understand how workflow execution is triggered by repository events.

I created .github/workflows/hello.yml:

name: hello

on:
  push:
  workflow_dispatch:

jobs:
  say-hello:
    runs-on: ubuntu-latest
    steps:
      - name: Say hello
        run: echo "Hello! My first robot is working."

      - name: Show the date and time
        run: date
Enter fullscreen mode Exit fullscreen mode

created workflow

The workflow runs whenever code is pushed to the repository. I also included workflow_dispatch, which allows it to be started manually from the GitHub Actions interface.

After committing and pushing the file, GitHub Actions detected the workflow and executed its job on an Ubuntu runner.

job

hello.yml

The workflow successfully printed the greeting and displayed the execution date and time.

say hello

show time and date

This established the foundation for the automation that followed.

In addition to automatic execution on code pushes, I tested the pipeline using GitHub Actions' manual trigger (workflow_dispatch).

Steps followed:

  1. Opened the pipeit-tutorial repository on GitHub.
  2. Navigated to the Actions tab.
  3. Selected the hello workflow from the workflow list.
  4. Clicked Run workflow.
  5. Selected the main branch.
  6. Clicked Run workflow to initiate execution.

workflow dispatch

  1. Opened the resulting workflow run to inspect the execution logs.

workflow dispatch log

  1. Verified that all steps completed successfully, displaying the greeting message and execution date and time.

This confirmed that the workflow could be triggered on demand without requiring a new code push.

The workflow_dispatch event provides flexibility by allowing workflows to be executed manually whenever needed, alongside their automated triggers.

2. Implementing Continuous Integration Checks

The next objective was to introduce a basic validation process.

A CI pipeline should not simply execute commands, it should also verify that repository changes meet defined requirements.

For this project, I created a Bash script to check whether the README file contained a Markdown heading.

I created tests/check.sh:

#!/usr/bin/env bash

set -e

if grep -q '^#' README.md; then
    echo "✅ README has a title"
else
    echo "❌ README.md needs a title line that starts with #"
    exit 1
fi
Enter fullscreen mode Exit fullscreen mode

bash script

The script checks whether the README contains a Markdown heading. If the heading exists, the check passes. Otherwise, the script prints an error message and exits with a non-zero status.

I then created .github/workflows/ci.yml:

name: ci

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Get my code
        uses: actions/checkout@v4

      - name: Run my checks
        run: bash tests/check.sh
Enter fullscreen mode Exit fullscreen mode

ci.yml

This workflow runs on both pushes and pull requests.

The actions/checkout step retrieves the repository contents so that the runner can access the files. The next step executes the Bash validation script.

Testing the Failure Scenario

To verify that the validation was meaningful, I deliberately removed the Markdown heading from the README file and pushed the change.

deliberate fail

commit changes

The workflow failed as expected.

workflow failed

The log displayed:

❌ README.md needs a title line that starts with #
Enter fullscreen mode Exit fullscreen mode

This confirmed that the pipeline was enforcing the condition defined in the test script.

I restored the README heading and pushed the correction. The next CI execution passed successfully.

restored README heading

successful run

A useful CI pipeline must be capable of detecting invalid changes rather than simply reporting success.

3. Creating a Custom GitHub Composite Action

After establishing the CI checks, I moved on to reusable automation.

GitHub Actions supports custom actions, allowing teams to package repeated workflow steps into reusable components.

For this project, I created a custom composite action that generates a dynamic HTML status card.

The action was created in .github/actions/build-card/action.yml.

custom composite action

commit

successful run

It accepts a name and an optional message, generates a website inside the site directory, and writes a summary to the GitHub Actions run page.

The generated page includes information about the build, such as the commit reference, workflow run number, triggering event, and build time.

status card

The action uses a Bash script and an HTML template to generate the page dynamically. One important implementation detail was the use of an unquoted heredoc (<<EOF) so that shell variables could be expanded while generating the HTML.

I also configured the action to expose the generated page path as an output.

Integrating the Custom Action into the CI Workflow

Once the composite action was created, I integrated it into the existing CI workflow.

The additional step was:

- name: Try my own action
  uses: ./.github/actions/build-card
  with:
    name: Rahimah Sulayman
Enter fullscreen mode Exit fullscreen mode

This allowed the workflow to invoke the action directly from the repository.

After pushing the changes, the CI workflow executed successfully. The custom action generated the HTML status card and displayed its summary in the workflow run.

I also corrected the generated page footer to:

<p>Built with GitHub Actions</p>
Enter fullscreen mode Exit fullscreen mode

This demonstrated how reusable composite actions can simplify workflows and package related operations into a single component.

4. Building the Complete CI/CD Pipeline

With the validation script and custom action working, I moved on to the main pipeline.

The goal was to combine testing, website generation, and deployment into a single automated workflow.

Before configuring deployment, I enabled GitHub Pages in the repository settings and selected GitHub Actions as the deployment source.

github pages

I then created .github/workflows/pipeline.yml.

The workflow was configured with three jobs:

  • test: checks the repository.
  • build: generates the website and packages it for deployment.
  • deploy: publishes the website to GitHub Pages.

The pipeline also included a scheduled trigger and a manual trigger.

Pipeline Configuration

The workflow uses the following structure:

name: pipeline

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 8 * * *'
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - name: Run my checks
        run: bash tests/check.sh

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - name: Build my status card with my own action
        uses: ./.github/actions/build-card
        with:
          name: Rahimah Sulayman
          message: Updated automatically through continuous deployment

      - name: Package the website
        uses: actions/upload-pages-artifact@v5
        with:
          path: site

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Publish to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v5
Enter fullscreen mode Exit fullscreen mode

pipeline.yml

commit

Understanding the Job Dependencies

One important feature of this pipeline is the use of the needs keyword.

The build job depends on the successful completion of the test job:

build:
  needs: test
Enter fullscreen mode Exit fullscreen mode

Similarly, deployment depends on the build job:

deploy:
  needs: build
Enter fullscreen mode Exit fullscreen mode

This establishes the execution order:

Test → Build → Deploy

The deployment process cannot proceed unless the preceding stages complete successfully.

The workflow also defines permissions required for GitHub Pages deployment:

permissions:
  contents: read
  pages: write
  id-token: write
Enter fullscreen mode Exit fullscreen mode

The concurrency configuration ensures that only the relevant Pages deployment workflow runs at a time, cancelling an older in-progress run when a newer one takes its place.

The scheduled trigger runs the pipeline daily at 08:00 UTC, while workflow_dispatch allows manual execution.

5. Deploying the Website to GitHub Pages

After configuring the pipeline, I pushed the workflow to the repository.

GitHub Actions executed the three jobs in sequence.

The test job validated the repository, the build job generated the website and packaged the site directory, and the deploy job published the artifact to GitHub Pages.

test, build, deploy

The workflow completed successfully.

The deployed website was available at:

https://rahimahisah17.github.io/pipeit-tutorial/

deployed website

At this point, the project had moved beyond basic CI automation. It now had a workflow that could validate changes, generate a website, and deploy the result.

6. Verifying Automatic Deployment

A successful initial deployment was not enough. I also needed to confirm that subsequent changes would automatically reach the live website.

To test this, I updated the custom action's message input in the pipeline:

message: Updated automatically through continuous deployment
Enter fullscreen mode Exit fullscreen mode

commit updated .yml

I committed and pushed the change to the main branch.

This triggered the pipeline automatically.

GitHub Actions executed the test, build, and deployment jobs again. After the deployment completed, I refreshed the live website and confirmed that the updated content was visible.

successful run

website

This verified the continuous deployment process.

The final workflow supported both initial publication and automatic updates whenever changes were pushed to the configured branch.

7. Troubleshooting and Lessons Learned

The implementation was not entirely error-free. Several issues occurred during development, and resolving them was an important part of completing the project.

Issue 1: Bash Syntax Error

During the initial implementation of tests/check.sh, the conditional statement contained a syntax error.

The script did not execute as expected because the if statement was not structured correctly.

I corrected the conditional block and ensured that the then, else, and fi keywords were properly placed.

The corrected script subsequently executed successfully.

Lesson: Shell scripts require careful attention to syntax. A small structural mistake can cause an entire CI job to fail.

Issue 2: Deliberate README Validation Failure

I intentionally removed the heading from the README to test whether the CI workflow could detect an invalid repository change.

The workflow failed and displayed the expected validation message.

This was not an unexpected pipeline defect. It was a controlled test to confirm that the validation script was working.

After restoring the heading, the workflow passed.

Lesson: Testing failure conditions is just as important as testing successful execution. A pipeline should fail when its defined requirements are not met.

Issue 3: GitHub Actions Internal Server Error

During the execution of the initial workflow, GitHub Actions returned a temporary internal server error.

The issue was not caused by the repository's workflow configuration.

I reran the failed jobs, and the workflow completed successfully.

Lesson: Not every CI/CD failure originates from application code or configuration. External platform issues can also affect workflow execution, so it is important to inspect the error details before changing working code.

Issue 4: Duplicate YAML Key

While updating the message passed to the custom action, I accidentally introduced a duplicate key:

message: message: Updated automatically through continuous deployment
Enter fullscreen mode Exit fullscreen mode

This resulted in a YAML configuration error.

I corrected the line to:

message: Updated automatically through continuous deployment
Enter fullscreen mode Exit fullscreen mode

After committing the correction, the pipeline executed successfully.

Lesson: YAML configuration must be structured carefully. Duplicate or incorrectly formatted keys can prevent a workflow from being parsed and executed.

8. Repository Structure

The completed repository is organized as follows:

pipeit-tutorial/
│
├── .github/
│   ├── actions/
│   │   └── build-card/
│   │       └── action.yml
│   │
│   └── workflows/
│       ├── hello.yml
│       ├── ci.yml
│       └── pipeline.yml
│
├── tests/
│   └── check.sh
│
├── README.md
└── site/
    └── index.html
Enter fullscreen mode Exit fullscreen mode

The site/index.html file is generated by the custom action during the build process.

The repository structure separates workflow definitions, reusable actions, validation scripts, documentation, and generated website content.

9. Project Release and Documentation

After completing the implementation, I updated the repository documentation to explain the project, its architecture, technologies, and deployment process.

I also published a GitHub release:

Release: v1.0.0

This release marks the completed version of the project and provides a reference point for the implementation.

The repository includes the workflow definitions, custom composite action, validation script, and documentation required to understand how the pipeline works.

10. What This Project Demonstrates

The project provided practical experience with several important DevOps and CI/CD concepts:

  • Continuous Integration: Automatically validating repository changes through GitHub Actions.
  • Workflow Triggers: Running automation in response to pushes, scheduled events, and manual requests.
  • Reusable Automation: Packaging repeated operations into a custom composite action.
  • Job Dependencies: Controlling pipeline execution using needs.
  • Artifact Management: Packaging generated website content for deployment.
  • Continuous Deployment: Automatically publishing updates to GitHub Pages.
  • Troubleshooting: Diagnosing shell script errors, YAML configuration issues, validation failures, and external platform errors.

It also reinforced the importance of designing pipelines that provide clear feedback and stop execution when validation requirements are not satisfied.

Conclusion

Building this CI/CD pipeline was a practical exercise in bringing together the essential stages of automated software delivery.

What started as a simple GitHub Actions workflow evolved into a multi-stage pipeline capable of validating repository changes, executing reusable automation, generating dynamic website content, and deploying updates automatically.

One of the most valuable aspects of the project was troubleshooting the issues encountered during implementation. Understanding why a workflow fails, identifying whether the problem comes from the script, configuration, or external platform, and applying the appropriate correction are important parts of working with automation.

The completed project demonstrates how GitHub Actions can be used to build structured and reusable CI/CD workflows without requiring a complex deployment environment.

It also provides a foundation for future improvements, such as introducing more comprehensive automated tests, adding security and dependency scanning, and extending the deployment process to a containerized application.

The pipeline is built, tested, and deployed.

Project Links

Summary

This project demonstrates how GitHub Actions can automate a complete software delivery workflow, from code validation to deployment.

By the end of the implementation, I had:

  • Created GitHub Actions workflows for basic automation and continuous integration.
  • Implemented a Bash script to validate repository documentation.
  • Built a reusable custom composite action to generate a dynamic HTML status card.
  • Configured a multi-job pipeline with testing, building, and deployment stages.
  • Enabled automated deployment to GitHub Pages.
  • Verified that changes pushed to the main branch triggered a new deployment.
  • Documented troubleshooting experiences and published the project as a GitHub release.

The result is a working CI/CD implementation that demonstrates workflow automation, reusable actions, pipeline dependencies, and continuous deployment using GitHub Actions.

Top comments (1)

Collapse
 
rahimah_dev profile image
Rahimah Sulayman •

A pipeline is only as reliable as the process behind it. This project reinforced the importance of automated validation, reusable workflows, and controlled deployments. Every successful run and every failure provided an opportunity to improve the delivery process.