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.
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.
With this configuration, whenever I create a PR, the CI workflow runs and ensures that the PR is safe to merge.
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.
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.
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.