Welcome back, future DevOps expert! In our previous chapters, we laid the groundwork by understanding the Linux command line and mastering version control with Git and GitHub. You now know how to manage code, track changes, and collaborate effectively. But what happens after you push your code to GitHub? How does it get built, tested, and eventually deployed to users?
This is where Continuous Integration (CI) and Continuous Delivery/Deployment (CD) come into play. This chapter will introduce you to the exciting world of CI/CD, where automation takes center stage, transforming manual, error-prone processes into fast, reliable, and repeatable workflows. We’ll explore the core principles of CI/CD, build your very first automated pipeline using GitHub Actions, and get a quick overview of Jenkins as another powerful tool in the CI/CD landscape. Get ready to make your code work for you!
Core Concepts: The Heartbeat of Modern Software Development
Before we dive into tools, let’s understand the fundamental ideas behind CI/CD. It’s a set of practices designed to deliver applications more frequently and reliably by automating the stages of software release.
What is Continuous Integration (CI)?
Imagine you’re working on a team project. Everyone is writing code, and occasionally, when you try to combine everyone’s work, things break. This is a common problem in software development.
Continuous Integration (CI) is a development practice where developers frequently merge their code changes into a central repository. Instead of merging once a week, they might merge several times a day. Each merge is then automatically verified by an automated build and test process.
Why is this important?
- Early Detection of Issues: By integrating and testing frequently, you catch bugs and integration problems much earlier, when they are easier and cheaper to fix.
- Reduced Integration Pain: No more “integration hell” at the end of a sprint! Small, frequent merges are less likely to conflict.
- Consistent Builds: Every time code is merged, a consistent build process runs, ensuring that the software can always be compiled and packaged correctly.
What is Continuous Delivery (CD) and Continuous Deployment (CD)?
Once your code has been integrated and tested, what’s next?
Continuous Delivery (CD) is an extension of CI. It ensures that code changes are automatically built, tested, and prepared for release to production. This means that at any point, your codebase is in a deployable state, and you can choose to release it to users with the push of a button. The decision to deploy is still manual.
Continuous Deployment (CD) takes this a step further. With Continuous Deployment, every change that passes all stages of your CI/CD pipeline is automatically deployed to production, without human intervention. This requires a high level of confidence in your automated tests and infrastructure.
Think of it this way:
- CI: You’ve built a delicious cake and tasted a small piece. It’s good!
- Continuous Delivery: The cake is fully baked, frosted, and ready to be served. You just need to decide when to put it on the table.
- Continuous Deployment: As soon as the cake is ready, it’s automatically placed on the table for everyone to enjoy!
The CI/CD Pipeline: Your Automated Assembly Line
The entire process, from code commit to deployment, is often visualized as a CI/CD Pipeline. This pipeline is a series of automated steps that your code goes through.
Here’s a common structure:
Explanation of Pipeline Stages:
- Code Commit: A developer pushes code changes to the version control system (e.g., GitHub). This action triggers the pipeline.
- Build Code: The source code is compiled into an executable application or packaged into a deployable artifact (e.g., a
.jarfile, a Docker image). - Run Unit Tests: Small, isolated tests verify individual components or functions of the code.
- Run Integration Tests: Tests that verify how different parts of the application or different systems interact.
- Create Artifact: The successful build and tested code is packaged into a deployable unit (e.g., a Docker image, a
.zipfile, a.debpackage). This is the “artifact” ready for deployment. - Deploy to Staging: The artifact is deployed to a staging environment, which closely mirrors the production environment.
- Run Acceptance Tests: More comprehensive tests are run in the staging environment to ensure the application meets business requirements. These might include end-to-end tests or user interface tests.
- Manual Approval?: For Continuous Delivery, a human might review the staging deployment and manually approve the release to production.
- Deploy to Production: If approved (or automatically in Continuous Deployment), the artifact is deployed to the live production environment.
- Monitor Production: After deployment, monitoring tools keep an eye on the application’s health and performance.
CI/CD Tooling: GitHub Actions vs. Jenkins
Many tools facilitate CI/CD. We’ll focus on two popular ones:
GitHub Actions:
- What it is: A CI/CD service directly integrated into GitHub.
- Pros: Seamless integration with your GitHub repositories, easy to get started with, vast marketplace of pre-built actions, serverless execution (no infrastructure to manage for the build agents).
- Cons: Tightly coupled with GitHub.
- When to use: Ideal for projects hosted on GitHub, open-source projects, and teams looking for a cloud-native, low-maintenance solution.
Jenkins:
- What it is: A powerful, open-source automation server.
- Pros: Highly flexible and extensible with thousands of plugins, can run on virtually any platform, self-hosted (giving you full control over your build environment).
- Cons: Requires setup and maintenance of your own Jenkins server and build agents, can have a steeper learning curve.
- When to use: For complex enterprise environments, specific compliance requirements, or when you need complete control over your CI/CD infrastructure.
In this chapter, we’ll start with GitHub Actions due to its ease of setup and direct integration with your Git workflow.
Step-by-Step Implementation: Your First GitHub Actions Workflow
Let’s get hands-on and create a simple CI pipeline using GitHub Actions. We’ll create a workflow that triggers whenever you push code to your repository, checks out the code, and runs a simple command.
Prerequisites:
- A GitHub account.
- A repository on GitHub (you can use one from Chapter 2 or create a new one).
- Some code in your repository. For this example, let’s assume you have a simple Node.js project with a
package.jsonfile and atestscript defined within it (e.g.,npm test). If not, you can create a simpleindex.jsandpackage.jsonwith a dummy test script.
Example Node.js Project (if you need one):
- Create a folder:
mkdir my-node-app && cd my-node-app - Initialize Node.js:
npm init -y - Create
index.js:// index.js console.log("Hello from Node.js!"); - Modify
package.jsonto include a test script: Openpackage.jsonand find the"scripts"section. Change it to:This"scripts": { "test": "echo \"Running dummy tests... All good!\" && exit 0", "start": "node index.js" },testscript will simply print a message and exit successfully. - Push to GitHub: Commit these files and push them to your GitHub repository.
Step 1: Create the Workflow Directory
GitHub Actions workflows are defined in YAML files located in a specific directory within your repository: .github/workflows/.
On your local machine, inside your my-node-app repository (or whatever your project is named), create this directory structure:
mkdir -p .github/workflows
The -p flag ensures that parent directories (.github) are created if they don’t already exist.
Step 2: Define Your First Workflow
Now, let’s create a YAML file for our workflow. You can name it anything descriptive, like node-ci.yml.
Create a new file: .github/workflows/node-ci.yml
Open this file in your text editor and add the following content:
# .github/workflows/node-ci.yml
# 1. Workflow Name: A descriptive name for your workflow.
name: Node.js CI Workflow
# 2. Trigger Event: When should this workflow run?
on:
push:
branches: [ main, develop ] # Run on pushes to 'main' or 'develop' branches
pull_request:
branches: [ main, develop ] # Run on pull requests targeting 'main' or 'develop'
# 3. Jobs: A workflow is made up of one or more jobs.
jobs:
# This is a single job named 'build-and-test'.
build-and-test:
# 4. Runner Environment: Specifies the type of machine to run the job on.
runs-on: ubuntu-latest # Use the latest Ubuntu Linux virtual machine
# 5. Steps: A sequence of tasks to be executed as part of the job.
steps:
# 6. Checkout Code: This action checks out your repository under $GITHUB_WORKSPACE.
- name: Checkout repository
uses: actions/checkout@v4 # Use the official 'checkout' action, version 4
# 7. Setup Node.js: This action sets up the Node.js environment.
- name: Setup Node.js environment
uses: actions/setup-node@v4 # Use the official 'setup-node' action, version 4
with:
node-version: '20.x' # Specify the Node.js version you want to use (e.g., 20.x stable)
# 8. Install Dependencies: Run 'npm install' to get project dependencies.
- name: Install dependencies
run: npm install
# 9. Run Tests: Execute the 'npm test' script.
- name: Run tests
run: npm test
Let’s break down each part of this YAML file, line by line:
name: Node.js CI Workflow: This is a human-readable name for your workflow. You’ll see this name in the GitHub Actions tab.on:: This section defines when the workflow should run.push:: The workflow triggers onpushevents.branches: [ main, develop ]: Specifically, it only triggers if the push is to themainordevelopbranch.pull_request:: The workflow also triggers onpull_requestevents.branches: [ main, develop ]: Again, only for pull requests targetingmainordevelop.
jobs:: A workflow can have multiple jobs that run in parallel or sequentially. Here, we have one job.build-and-test:: This is the ID of our single job.runs-on: ubuntu-latest: This tells GitHub Actions to run this job on a fresh virtual machine running the latest version of Ubuntu Linux. Other options includewindows-latestormacos-latest.steps:: This is a list of sequential tasks that thebuild-and-testjob will execute.- name: Checkout repository: A descriptive name for this step.uses: actions/checkout@v4: This is a pre-built GitHub Action.actions/checkout@v4fetches your repository’s code into the runner’s workspace.v4specifies the latest stable major version of this action as of 2026.- name: Setup Node.js environment: Another descriptive name.uses: actions/setup-node@v4: This action sets up a Node.js environment on the runner.with: node-version: '20.x': This is how you pass inputs to an action. Here, we’re tellingsetup-nodeto install a Node.js version from the 20.x series (e.g., 20.10.0, which is the latest LTS as of early 2026).- name: Install dependencies:run: npm install: This step executes thenpm installcommand in the shell on the runner, downloading all project dependencies.- name: Run tests:run: npm test: This step executes thenpm testcommand, which, in our example, will run the dummy test script we defined inpackage.json.
Step 3: Commit and Push Your Workflow
Save your node-ci.yml file. Now, commit these changes and push them to your GitHub repository:
git add .github/workflows/node-ci.yml
git commit -m "feat: Add initial Node.js CI workflow"
git push origin main # Or your default branch like 'master'
Step 4: Observe the Workflow in Action
Once you push, GitHub will automatically detect the new workflow file and trigger a run!
- Go to your repository on GitHub.
- Click on the “Actions” tab.
- You should see your “Node.js CI Workflow” listed, with the commit message you just pushed.
- Click on the workflow run to see its details. You’ll see each step (
Checkout repository,Setup Node.js environment,Install dependencies,Run tests) executing in real-time. - If everything is configured correctly, all steps should pass with green checkmarks!
Congratulations! You’ve just set up your first automated CI/CD pipeline.
Brief Introduction to Jenkins
While GitHub Actions is great for projects hosted on GitHub, Jenkins remains a powerhouse, especially for complex, on-premise, or multi-platform needs.
Jenkins Fundamentals
- Self-Hosted: You install and manage Jenkins on your own server (physical, VM, or Docker container).
- Jobs/Pipelines: Similar to GitHub Actions workflows, Jenkins uses “Jobs” or “Pipelines” to define automated tasks.
- Jenkinsfile: Modern Jenkins pipelines are often defined using a
Jenkinsfile– a Groovy script stored in your repository alongside your code. This allows for “Pipeline as Code.”
A Glimpse at a Jenkinsfile
Here’s a very simple declarative Jenkinsfile for a “Hello World” build. You wouldn’t run this directly in GitHub, but it shows the structure.
// Jenkinsfile
pipeline {
agent any // Tells Jenkins to run this pipeline on any available agent
stages {
stage('Build') { // Define a stage named 'Build'
steps {
echo 'Building the application...' // Print a message
// In a real scenario, this would be 'npm install' or 'mvn clean install'
}
}
stage('Test') { // Define a stage named 'Test'
steps {
echo 'Running tests...' // Print a message
// In a real scenario, this would be 'npm test' or 'mvn test'
}
}
}
}
Explanation:
pipeline { ... }: The root block for a declarative pipeline.agent any: Specifies where the pipeline should run.anymeans on any available Jenkins agent.stages { ... }: Contains one or morestageblocks.stage('Build') { ... }: A logical grouping of steps, representing a phase in the pipeline.steps { ... }: Contains the actual commands or actions to execute within a stage.echo '...': A simple command to print text to the console.
While setting up a full Jenkins server is beyond the scope of this introductory chapter, understanding its Jenkinsfile concept is crucial for grasping how CI/CD pipelines are defined across different tools. Many organizations use Jenkins for its flexibility and robust feature set.
Mini-Challenge: Enhance Your GitHub Actions Workflow
You’ve built a basic CI pipeline. Now, let’s make a small improvement!
Challenge:
Modify your node-ci.yml workflow to add an additional step before Install dependencies that simply prints a “Hello, CI/CD world!” message to the console.
Hint:
Remember the run: keyword for executing shell commands. You’ll need to add a new name: and run: pair in your steps list.
What to Observe/Learn: After pushing your changes, observe the new step executing in the GitHub Actions interface. This exercise reinforces how easy it is to add and modify steps in your pipeline, building it incrementally.
Common Pitfalls & Troubleshooting
Even with simple workflows, things can go wrong. Here are a few common issues and how to approach them:
YAML Syntax Errors:
- Problem: YAML is very sensitive to indentation. A single incorrect space can cause the workflow to fail to parse.
- Troubleshooting: GitHub Actions will usually report a “workflow syntax error” in the Actions tab. Use a YAML linter (many online tools available) or an IDE with YAML support to highlight errors. Double-check your spacing carefully.
- Example:
# WRONG INDENTATION jobs: build-and-test: # This line is incorrectly indented runs-on: ubuntu-latest
Incorrect
onTriggers:- Problem: Your workflow isn’t running when you expect it to (e.g., after a push).
- Troubleshooting: Verify the
on:section. Are you pushing to the correct branch? Is the event type (push,pull_request) correctly specified? If you’ve restricted branches, ensure your push matches.
Missing Dependencies or Commands:
- Problem: A
runstep fails with a “command not found” error, or a build fails because a package is missing. - Troubleshooting:
- Check logs: The GitHub Actions logs are your best friend. Expand the failing step and read the output carefully.
- Environment: Ensure your
uses:actions are setting up the correct environment (e.g.,setup-node@v4for Node.js). - Install steps: Did you include
npm install(orpip install,composer install, etc.) before trying to run your build or tests?
- Problem: A
Permissions Issues:
- Problem: Your workflow tries to access a resource (e.g., a private package, a cloud service) and gets an “access denied” error.
- Troubleshooting: This often involves environment variables or GitHub Secrets. We haven’t covered secrets yet, but know that sensitive information (like API keys) should never be hardcoded in your workflow file. GitHub Secrets are the secure way to manage these. For now, ensure your actions don’t implicitly require permissions you haven’t granted.
Summary
You’ve taken a massive leap forward in your DevOps journey! In this chapter, you’ve learned:
- Continuous Integration (CI): The practice of frequently merging code changes and automatically verifying them with builds and tests.
- Continuous Delivery (CD): Ensuring your software is always in a deployable state, ready for manual release.
- Continuous Deployment (CD): Automatically releasing every verified change to production.
- The CI/CD Pipeline: The automated sequence of steps (build, test, package, deploy) that your code goes through.
- GitHub Actions: How to define and implement a basic CI workflow using YAML, triggering on push events, checking out code, setting up environments, and running commands.
- Jenkins: A brief overview of Jenkins as a powerful, self-hosted alternative for CI/CD, and the concept of a
Jenkinsfile.
You’ve successfully built your first automated pipeline, which is a cornerstone of modern software development. In the next chapter, we’ll dive into containerization with Docker, a technology that works hand-in-hand with CI/CD to create consistent, isolated, and portable environments for your applications. Get ready to containerize your code!
References
- GitHub Actions Documentation
- GitHub Actions:
actions/checkoutv4 - GitHub Actions:
actions/setup-nodev4 - What is CI/CD? - Red Hat
- Jenkins Official Website
- Mermaid.js Flowchart Syntax
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.