Continuous Integration (CI) and Continuous Delivery (CD) are the backbone of modern software development, especially in projects that are continuously evolving. For my blog application, I decided to implement a CI/CD pipeline using GitHub Actions since it is free for public repositories and easy to set up without additional resources.

Workflow Trigger Events

I find trunk-based development to be the most effective branching strategy since it enables frequent delivery alongside CI/CD and minimizes the risk of major conflicts. This helps prevent unnecessary efforts caused by complex merges. To support this, I implemented a CI workflow that runs on pull_request events such as open, reopened, and synchronize. Additionally, I added a workflow_call and workflow_dispatch events, which allow me to reuse this CI workflow in my CD pipeline and trigger it manually from GitHub Actions if needed.

Continuous Integration GitHub Action Workflow

Since I’ve already split my unit and integration tests into different Maven profiles, as described in my previous post, I configured the CI workflow to run each test suite in separate steps. I've also added required permissions for workflow so results can be written into checks and pull-requests. Here's a breakdown of the steps that I have in the workflow:

Steps:

  • Checkout
  • Setup JDK
  • Test compile
  • Run unit tests
  • Run integration tests
  • Report test results
name: CI
on:
  workflow_call:
  workflow_dispatch:
  pull_request:
    types:
      - 'opened'
      - 'reopened'
      - 'synchronize'
permissions:
  checks: write
  pull-requests: write
jobs:
  test:
    name: 'Compile & Test'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Compile
        run: mvn clean compile test-compile
      - name: Run Unit Tests
        run: mvn test -P unit
      - name: Run Integration Tests
        run: mvn test -P integration
      - name: Report
        uses: mikepenz/action-junit-report@v4
        if: ${{ always() }}
        with:
          report_paths: '**/target/surefire-reports/*.xml'

Protect Main Branch and Require Status Checks on Pull Requests

To ensure that no one pushes directly to the main branch and that only pull requests (PRs) with passing CI checks are merged, I enabled branch protection rules. This requires that my CI workflow completes successfully before a PR can be merged. I did this by creating a ruleset in GitHub with the following steps:

1. Select Target Branch

I selected the default branch, which is the main branch, as the target of the ruleset.

GitHub Actions Ruleset Target Branch

2. Required Pull Requests and Selected Required Status Checks

I configured the ruleset to require PRs for all merges to the main branch and selected my CI action Compile & Test as a required status check on the PRs.

GitHub Actions Ruleset Status Check

With this configuration, whenever I create a PR, the CI workflow runs and ensures that the PR is safe to merge.

PR Status Check

Setting Up DockerHub Credentials in Secrets

I chose DockerHub as the image registry for my blog application because it’s free for my scale and provides a reliable, widely-used platform. To enable my CD workflow to push Docker images to DockerHub, I stored my DockerHub credentials in the repository secrets section under Settings > Secrets and Variables > Actions.

Repository Secrets for GitHub Actions

Continuous Delivery GitHub Action Workflow

For the CD workflow, I reused the CI workflow for testing by referencing it in the test job. The build job depends on the test job and is triggered whenever a PR is closed and merged into the main branch. Additionally, I included the workflow_dispatch event to allow manual triggering of the delivery process when needed.

In the build step, I set up the JDK, log in to DockerHub, and then build and push the Docker image. I also tag the image with latest and pushed that tag to DockerHub too.

Steps:

  • Checkout
  • Setup JDK
  • Login to DockerHub
  • Build Package
  • Calculate Version
  • Build Image, Push and Tag
name: CD

on:
  workflow_dispatch:
  pull_request:
    branches:
      - main
    types: [closed]
defaults:
  run:
    shell: bash
permissions:
  checks: write
  pull-requests: write
jobs:
  test:
    name: CI
    uses: ./.github/workflows/ci.yml
  build:
    needs: [test]
    if: ${{ github.event.pull_request.merged }} || ${{ github.event.workflow_dispatch }}
    name: 'Build & Publish'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Log in to Docker Hub
        uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      - name: Maven Install
        run: mvn install -Dmaven.test.skip=true
      - name: Calculate Version
        id: version
        run: echo "::set-output name=version::$(date +%Y.%-m.%-d-%H%M%S)"
      - name: Build & Push Release Image
        run: |
          docker build . --tag cbidici/site:${{ steps.version.outputs.version }}
          docker push cbidici/site:${{ steps.version.outputs.version }}
          docker tag cbidici/site:${{ steps.version.outputs.version }} cbidici/site:latest
          docker push cbidici/site:latest

After putting this workflow in place, I finally managed to publish an image to DockerHub.

GitHub Actions CD Workflow

DockerHub Tags

Conclusion

With this CI/CD pipeline in place, I no longer need to worry about manually running tests or delivering updates. The pipeline automates everything from testing to building and pushing Docker images. Best of all, my releases are safer now, as the CI ensures that only well-tested changes are released. As long as I continue to implement comprehensive tests, my CI/CD process will help maintain the integrity of my application.