Build a CI/CD Pipeline with GitHub Actions

Automate testing on every push with a GitHub Actions CI/CD pipeline.

Introduction

30 Second Summary

Imagine if every time you saved a document, someone instantly proofread it and flagged errors before anyone else could see them. That's what CI/CD does for code.

In this project, you will build a JavaScript utility library with automated tests and connect it to a GitHub Actions pipeline that runs those tests every time you push code.

What You'll Build

A string utility library with helper functions, automated tests using the Node.js built-in test runner, and a GitHub Actions workflow that triggers CI on every push.

By the end of this project, you'll have:

  • A public GitHub repository with a Node.js project containing string utility functions.
  • Automated tests using Node.js built-in test runner with zero external dependencies.
  • A GitHub Actions CI pipeline that automatically runs tests on every push and shows a green checkmark or red X on your commits.
  • Secret Mission: Add a linting step and a live status badge to your README.

Are there any prerequisites?

You'll need basic JavaScript knowledge and a GitHub account. No prior experience with CI/CD or automated testing is needed.

Set Up Your Project

Every CI/CD pipeline starts with a codebase. Before GitHub can automatically run your tests, you need a project folder with a properly configured package.json and a connected repository.

In this step, you'll create your Node.js project from scratch, configure it to use the built-in test runner, and push it to a public GitHub repository.

In this step, get ready to:
  • Create and initialize a Node.js project folder.
  • Configure your package.json with a test script.
  • Create a public GitHub repository and push your first commit.
Create your project folder
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and initialize it by running these commands:
mkdir string-utils-ci
cd string-utils-ci
npm init -y

What do these commands do?

  • mkdir string-utils-ci creates a new folder called string-utils-ci on your Desktop.
  • cd string-utils-ci moves your terminal into that folder.
  • npm init -y generates a default package.json file. The -y flag accepts all defaults so you don't have to answer prompts.
  • Confirm the project was initialized by running:
ls
  • You should see package.json in the output.

Don't see package.json?

  • Make sure you ran cd string-utils-ci before running npm init -y. You need to be inside the folder first.
  • If you see a "command not found" error for npm, your Node.js installation may not be in your PATH. Close and reopen your terminal, then try again.

Help me figure out why npm init isn't working in my terminal.

Configure your package.json

The default package.json from npm init -y has placeholder values. You need to update it with your project's actual details and add the test script.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the string-utils-ci folder, and click Open.
  • In the Explorer sidebar on the left, click package.json to open it.
  • Select all the content in the file with Cmd+A (macOS) or Ctrl+A (Windows), then replace it with the following:
{
  "name": "string-utils-ci",
  "version": "1.0.0",
  "description": "A string utility library with CI/CD pipeline",
  "main": "src/stringUtils.js",
  "scripts": {
    "test": "node --test"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

What does this configuration do?

  • "test": "node --test" tells npm to use Node.js's built-in test runner when you run npm test. No external testing library needed.
  • "main": "src/stringUtils.js" points to where your utility library will live. You'll create this file in the next step.
  • Without a "type" field, Node.js defaults to CommonJS modules. This lets you use the familiar require() and module.exports syntax.
  • Save the file with Cmd+S (macOS) or Ctrl+S (Windows).

✔️ Awesome, I've got everything!

Great. Make sure you've saved package.json before moving on.

ⓧ I'd like to double check the full code

Here's what your package.json should look like:

{
  "name": "string-utils-ci",
  "version": "1.0.0",
  "description": "A string utility library with CI/CD pipeline",
  "main": "src/stringUtils.js",
  "scripts": {
    "test": "node --test"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Seeing red squiggly lines in VS Code?

  • Make sure you don't have a trailing comma after the last item in any list or object. JSON doesn't allow trailing commas.
  • Check that all your opening braces and brackets have matching closing ones.

Help me fix the JSON errors in my package.json file.

Create a GitHub repository and push your code

Your project needs to live on GitHub so that GitHub Actions can run your CI pipeline. You'll create a public repository and push your local code to it.

Why a public repository?

GitHub Actions is free and unlimited for public repositories. Private repos get 2,000 minutes per month on the Free plan, which is plenty, but a public repo avoids any billing confusion.

  • Open your browser and go to github.com.
  • Click the + icon in the top-right corner of the page.
  • Click New repository.
  • Enter string-utils-ci in the Repository name field.
  • Select Public for the repository visibility.
  • Leave all other options unchecked (no README, no .gitignore, no license).
  • Click Create repository.

GitHub shows a quick setup page with commands to connect your local project. You'll use VS Code's built-in terminal to run them.

  • Back in VS Code, open the integrated terminal by pressing Ctrl+` (backtick key, same on macOS and Windows).
  • Initialize your Git repository, commit your code, and push it to GitHub by running these commands:
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin [[YOUR_REPO_URL="https://github.com/your-username/string-utils-ci.git"]]
git push -u origin main

What do these commands do?

  • git init turns your folder into a Git repository that can track changes.
  • git add . stages all files in the folder for your first commit.
  • git commit -m "Initial commit" saves a snapshot of your project with a message describing the change.
  • git branch -M main ensures your default branch is named main.
  • git remote add origin connects your local folder to the GitHub repository you just created.
  • git push -u origin main uploads your code to GitHub.

Find your repository URL

Copy the URL from the GitHub quick setup page that appeared after you created your repository. It looks like https://github.com/your-username/string-utils-ci.git with your actual GitHub username.

  • Refresh your GitHub repository page in your browser.
  • You should see package.json listed in the file view with your "Initial commit" message next to it.

Push failed or authentication error?

  • If you see a permission or authentication error, make sure your Git credentials are configured. Try running the push command again. Your browser may open a GitHub sign-in window.
  • Double check that the URL after git remote add origin matches your repository exactly. You can verify with git remote -v.
  • If you accidentally initialized the repo with a README on GitHub, delete the repository on GitHub and recreate it with no files.

Help me troubleshoot my git push to GitHub.

Your project is set up and live on GitHub. Next up, you'll write the string utility functions and automated tests that your CI pipeline will run.

Write Your Utility Library

Your GitHub repository is connected and your package.json already has a test script configured. Now you need actual code for that test script to run against.

In this step, you'll write a small string utility library with three helper functions, then write automated tests to verify they work. These are the exact tests your CI pipeline will run automatically in the next step.

In this step, get ready to:
  • Create three string utility functions in a src/ directory.
  • Write automated tests using the Node.js built-in test runner.
  • Run tests locally and experience what a test failure looks like.
Create the string utility functions

Your utility library will have three functions that manipulate strings. Each function handles edge cases like empty input, which makes them perfect for testing.

  • Create a src folder inside your string-utils-ci project by running this command in your terminal:
mkdir src
  • In VS Code, create a new file called stringUtils.js inside the src folder.
  • Right-click the src folder in the Explorer sidebar.
  • Select New File... and name it stringUtils.js.
  • Add the following code to src/stringUtils.js:
// Capitalizes the first letter of a string
function capitalize(str) {
  if (!str) return '';
  return str.charAt(0).toUpperCase() + str.slice(1);
}

// Reverses a string
function reverse(str) {
  if (!str) return '';
  return str.split('').reverse().join('');
}

// Shortens a string to maxLength, adding ellipsis if truncated
function truncate(str, maxLength) {
  if (!str) return '';
  if (str.length <= maxLength) return str;
  return str.slice(0, maxLength - 3) + '...';
}

// Export all three functions for use in other files
module.exports = { capitalize, reverse, truncate };

What does this code do?

Each function takes a string as input and transforms it in a different way:

  • capitalize('hello') returns 'Hello' by uppercasing the first character.
  • reverse('hello') returns 'olleh' by splitting the string into characters, reversing the array, and joining it back.
  • truncate('Hello World', 8) returns 'Hello...' by slicing to the max length minus 3, then appending ....

The if (!str) return '' guard at the top of each function handles empty or undefined input gracefully. The module.exports line makes all three functions available to other files that require() this module.

  • Save src/stringUtils.js.
  • Confirm the file exists by running:
ls src

You should see stringUtils.js in the output.

Don't see the file?

  • Make sure you created the src directory first with mkdir src. If you saved the file outside of src/, delete it and recreate it inside the folder.
  • Check that your terminal is in the string-utils-ci directory. Run pwd to verify your current location.

Help me figure out why my src/stringUtils.js file isn't showing up.

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file before moving on.

ⓧ I'd like to double check the full code

function capitalize(str) {
  if (!str) return '';
  return str.charAt(0).toUpperCase() + str.slice(1);
}

function reverse(str) {
  if (!str) return '';
  return str.split('').reverse().join('');
}

function truncate(str, maxLength) {
  if (!str) return '';
  if (str.length <= maxLength) return str;
  return str.slice(0, maxLength - 3) + '...';
}

module.exports = { capitalize, reverse, truncate };
Write automated tests for each function

Node.js has a built-in test runner that you can use without installing any extra packages. It provides describe and it blocks to organize your tests, and assert to check that values match what you expect.

  • Create a tests folder by running:
mkdir tests
  • In VS Code, right-click the tests folder in the Explorer sidebar.
  • Select New File... and name it stringUtils.test.js.
  • Add the following code to tests/stringUtils.test.js:
const { describe, it } = require('node:test');
const assert = require('node:assert');
const { capitalize, reverse, truncate } = require('../src/stringUtils');

// Tests for the capitalize function
describe('capitalize', () => {
  it('capitalizes the first letter', () => {
    assert.strictEqual(capitalize('hello'), 'Hello');
  });

  it('returns empty string for empty input', () => {
    assert.strictEqual(capitalize(''), '');
  });

  it('handles single character', () => {
    assert.strictEqual(capitalize('a'), 'A');
  });
});

// Tests for the reverse function
describe('reverse', () => {
  it('reverses a string', () => {
    assert.strictEqual(reverse('hello'), 'olleh');
  });

  it('returns empty string for empty input', () => {
    assert.strictEqual(reverse(''), '');
  });

  it('handles palindromes', () => {
    assert.strictEqual(reverse('racecar'), 'racecar');
  });
});

// Tests for the truncate function
describe('truncate', () => {
  it('truncates long strings with ellipsis', () => {
    assert.strictEqual(truncate('Hello World', 8), 'Hello...');
  });

  it('does not truncate short strings', () => {
    assert.strictEqual(truncate('Hi', 10), 'Hi');
  });

  it('returns empty string for empty input', () => {
    assert.strictEqual(truncate('', 5), '');
  });
});

What does this test file do?

The first two lines import Node.js's built-in testing tools. describe groups related tests together, and it defines an individual test case.

Each test calls a function with a known input and uses assert.strictEqual to check that the output matches exactly what you expect. If the actual value does not match, the test fails and tells you what went wrong.

Each describe block covers one function and tests normal cases plus edge cases (empty strings, single characters, palindromes). This pattern of testing both happy paths and edge cases is standard practice in professional codebases.

  • Save tests/stringUtils.test.js.
  • You won't see a visible change yet. The next substep runs the tests so you can see them in action.

Seeing a syntax error when you save?

  • Check that every describe and it block has matching opening and closing parentheses and curly braces. Each block ends with });.
  • Make sure the require('../src/stringUtils') path uses ../ to go up one directory from tests/ into the project root, then down into src/.

Help me fix a syntax error in my test file.

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file before moving on.

ⓧ I'd like to double check the full code

const { describe, it } = require('node:test');
const assert = require('node:assert');
const { capitalize, reverse, truncate } = require('../src/stringUtils');

describe('capitalize', () => {
  it('capitalizes the first letter', () => {
    assert.strictEqual(capitalize('hello'), 'Hello');
  });

  it('returns empty string for empty input', () => {
    assert.strictEqual(capitalize(''), '');
  });

  it('handles single character', () => {
    assert.strictEqual(capitalize('a'), 'A');
  });
});

describe('reverse', () => {
  it('reverses a string', () => {
    assert.strictEqual(reverse('hello'), 'olleh');
  });

  it('returns empty string for empty input', () => {
    assert.strictEqual(reverse(''), '');
  });

  it('handles palindromes', () => {
    assert.strictEqual(reverse('racecar'), 'racecar');
  });
});

describe('truncate', () => {
  it('truncates long strings with ellipsis', () => {
    assert.strictEqual(truncate('Hello World', 8), 'Hello...');
  });

  it('does not truncate short strings', () => {
    assert.strictEqual(truncate('Hi', 10), 'Hi');
  });

  it('returns empty string for empty input', () => {
    assert.strictEqual(truncate('', 5), '');
  });
});
Run your tests and experience a failure

Your package.json already has a test script set to node --test. This command auto-discovers any file matching *.test.js and runs it.

  • Run your tests by executing:
npm test

You should see output showing each test passing with a checkmark, followed by a summary that says all 9 tests passed and 0 failed.

What is npm test doing?

The npm test command runs whatever is defined in the "test" script in your package.json. In your case that is node --test, which invokes Node.js's built-in test runner.

The test runner automatically finds files matching patterns like **/*.test.js and runs them. You did not need to install any testing framework because node:test ships with Node.js itself.

Tests failing or command not found?

  • If you see a module not found error mentioning ../src/stringUtils, check that your src/stringUtils.js file is saved and in the correct location.
  • If you see 0 tests found, make sure your test file is named stringUtils.test.js (with .test.js at the end) so the runner can discover it.
  • If npm is not recognized, make sure Node.js is installed (run node --version to check).

Help me figure out why npm test isn't working.

Now you know what passing tests look like. Next you will intentionally break one to see what failure looks like. This is important because your CI pipeline will show you the same failure output when a bug slips in.

  • In tests/stringUtils.test.js, find this line inside the first describe('capitalize') block:
    assert.strictEqual(capitalize('hello'), 'Hello');
  • Change 'Hello' to 'HELLO', so the line now looks like this:
    assert.strictEqual(capitalize('hello'), 'HELLO');
  • Save the file and run the tests again:
npm test

You should see one test fail. The output shows you the expected value versus the actual value, so you can pinpoint exactly what went wrong.

Why intentionally break a test?

In a real project, you will not break tests on purpose. But seeing what failure looks like now prepares you for when your CI pipeline catches a real bug. The red X and error details in the output are the same feedback your team would see on a pull request.

  • Fix the test by changing 'HELLO' back to 'Hello':
    assert.strictEqual(capitalize('hello'), 'Hello');
  • Save the file and confirm the tests pass again:
npm test

All 9 tests should pass. Now commit and push your work.

  • Stage, commit, and push your changes by running:
git add .
git commit -m "Add string utility library and tests"
git push
  • You should see your commit pushed to GitHub successfully.

Git push rejected?

  • If you see a rejection about the remote containing work you don't have, run git pull --rebase first, then try git push again.
  • If you see an authentication error, make sure your Git credentials are configured correctly for your GitHub account.

Help me fix a git push issue.

Your utility library has code and tests, all pushed to GitHub. Next up, you'll create a CI pipeline that runs these tests automatically every time you push.

Create Your CI Pipeline

You have a utility library with tests that pass locally. But what happens when you push code and forget to run the tests first?

This is where Continuous Integration comes in. In this step, you'll create a GitHub Actions workflow that automatically runs your tests every time you push code to GitHub.

In this step, get ready to:
  • Create a GitHub Actions workflow file with CI triggers.
  • Define the steps that check out code, set up Node.js, and run tests.
  • Push to GitHub and watch your pipeline run automatically.
Create the workflow directory and file

GitHub Actions looks for workflow files in a specific location: the .github/workflows directory at the root of your repository.

  • Create the workflow directory by running this command in your VS Code terminal:
mkdir -p .github/workflows
  • Confirm the directory was created by running:
ls .github

You should see workflows in the output.

Seeing an error with mkdir -p?

If you're using PowerShell on Windows, the -p flag isn't supported. Use this command instead:

New-Item -ItemType Directory -Path .github/workflows -Force

Alternatively, switch your VS Code terminal to Git Bash by clicking the dropdown arrow next to the + icon in the terminal panel and selecting Git Bash.

  • Create a new file called ci.yml inside the .github/workflows/ directory. In VS Code's Explorer sidebar, right-click the workflows folder and select New File.
  • Name the file ci.yml and press Enter.
  • Add the workflow name, triggers, and job definition by pasting this code into ci.yml:
name: CI

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest

    steps:

What does this code do?

  • name: CI gives the workflow a human-readable name that shows up in the GitHub Actions tab.
  • The on: section defines when the workflow runs. It triggers on every push to main and on every pull request targeting main.
  • jobs: defines a job called test that runs on a fresh Ubuntu Linux virtual machine provided by GitHub.
  • steps: is where you'll define the individual actions your job performs. You'll fill this in next.

What is a YAML file?

YAML is a configuration format that uses indentation to represent structure. GitHub Actions uses YAML files to define your pipeline's instructions. Indentation matters in YAML, so make sure your spacing matches exactly.

Add the workflow steps

The steps: section tells GitHub exactly what to do when the workflow triggers. Your pipeline needs three actions: grab the code, set up Node.js, and run the tests.

  • In ci.yml, add the following lines directly below steps::
      - name: Checkout code
        uses: actions/checkout@v6

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

      - name: Run tests
        run: npm test

What does this code do?

  • actions/checkout@v6 clones your repository onto the runner machine so the pipeline can access your code.
  • actions/setup-node@v6 installs Node.js version 22 on the runner. Without this, the machine wouldn't have the right Node version to run your tests.
  • run: npm test executes the same test command you run locally. If any test fails, the workflow fails too.
  • Save ci.yml.

Indentation looks off?

YAML is very sensitive to spacing. Each step item should be indented with 6 spaces (to sit under steps: which is indented 4 spaces). If VS Code shows yellow or red squiggly lines, your indentation is likely off.

help me fix my ci.yml indentation

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

Here's the complete ci.yml file. Make sure yours matches exactly, including indentation:

name: CI

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest

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

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

      - name: Run tests
        run: npm test
Push and watch the pipeline run

Your workflow file is ready. Now you'll commit it and push to GitHub so that Actions picks it up and runs your pipeline for the first time.

  • Commit and push the workflow file by running these commands:
git add .github/workflows/ci.yml
git commit -m "Add CI workflow"
git push

What does this do?

Pushing the workflow file to the main branch triggers the pipeline immediately. GitHub detects the new file in .github/workflows/ and starts running it.

  • Open your repository on GitHub in your browser.
  • Click the Actions tab in the top navigation bar of your repository.

You should see your workflow running (indicated by a yellow spinning icon) or already completed with a green checkmark.

  • Click on the workflow run (it will be named after your commit message, Add CI workflow) to see the details.
  • Click the test job to expand the step-by-step logs.

You should see each step (Checkout code, Set up Node.js, Run tests) completed with green checkmarks. The Run tests step shows the same test output you saw locally.

Don't see the Actions tab?

The Actions tab only appears on repositories that have workflow files pushed to them. Make sure your push succeeded (no errors in the terminal) and refresh the GitHub page.

If your pipeline failed with a red X, click into the failed run and expand the failing step to read the error message. The most common issue is YAML indentation.

help me debug my failing GitHub Actions workflow

Your CI pipeline is live. Every time you push code to this repository, GitHub will automatically check out your code, set up Node.js, and run your tests. Next up, you'll experience the full development lifecycle loop by adding new features and watching the pipeline catch bugs.

Experience the SDLC Loop

Your CI pipeline is live and passing on GitHub. Now it's time to experience the real power of that green checkmark.

You'll extend your utility library with a new function, push it through the pipeline, and then intentionally break something. Watching your pipeline catch a bug before it reaches production is the core value of CI/CD.

In this step, get ready to:
  • Add a new string utility function and its tests.
  • Push, break, and fix code to see the full SDLC loop.
  • Observe the green-red-green commit pattern that tells your project's story.
Add a slugify function to your utility library

URL-friendly slugs turn readable titles into clean URL paths. Your new slugify function will lowercase a string, strip special characters, and replace spaces with hyphens.

  • In VS Code, open src/stringUtils.js.
  • Find the closing brace of the truncate function and the module.exports line at the bottom.
  • Replace the module.exports line with the following code, which adds the slugify function and updates the exports:
function slugify(str) {
  if (!str) return '';
  return str
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9\s-]/g, '')
    .replace(/[\s]+/g, '-');
}

module.exports = { capitalize, reverse, truncate, slugify };

What does this code do?

The slugify function transforms any string into a URL-safe slug through a chain of operations:

  • .toLowerCase() converts everything to lowercase.
  • .trim() removes leading and trailing whitespace.
  • The first .replace() strips anything that is not a letter, number, space, or hyphen.
  • The second .replace() swaps spaces for hyphens.
  • Save src/stringUtils.js.

You won't see a visible change yet. The next chunk adds tests that exercise this function.

Write tests for slugify
  • Open tests/stringUtils.test.js.
  • Find the import line at the top of the file:
const { capitalize, reverse, truncate } = require('../src/stringUtils');
  • Update it to also import slugify, so the line now reads:
const { capitalize, reverse, truncate, slugify } = require('../src/stringUtils');
  • Scroll to the bottom of the file, after the closing }); of the truncate describe block.
  • Add the following test suite for slugify:
describe('slugify', () => {
  it('converts a string to a slug', () => {
    assert.strictEqual(slugify('Hello World'), 'hello-world');
  });

  it('removes special characters', () => {
    assert.strictEqual(slugify('Hello! World?'), 'hello-world');
  });

  it('returns empty string for empty input', () => {
    assert.strictEqual(slugify(''), '');
  });
});

What does this test suite cover?

Three scenarios test slugify from different angles:

  • Normal input with spaces becomes a hyphenated slug.
  • Special characters (exclamation marks, question marks) are stripped out.
  • An empty string stays empty instead of crashing.
  • Save tests/stringUtils.test.js.

Seeing a syntax error when you save?

Make sure the new describe block starts after the previous one closes. Each describe block must have matching opening ( and closing }); pairs.

Also confirm you updated the require line at the top to include slugify in the destructured import.

Help me fix a syntax error in my stringUtils.test.js file.

  • Run the tests locally to verify everything passes by running this command:
npm test

You should see all 12 tests passing (3 for each of your 4 functions).

  • Commit and push your new feature to GitHub by running these commands:
git add .
git commit -m "Add slugify function with tests"
git push
  • Switch to your browser and open the **Actions** tab in your GitHub repository.
  • Click the latest workflow run to watch it execute in real time.

After the run completes, you should see a green checkmark next to your commit. Your pipeline automatically tested your new function.

Pipeline still running after a few minutes?

GitHub Actions runners typically start within 10-30 seconds for public repositories. If your run is queued for longer, check that your repository is set to public. Private repositories on the Free plan share a limited runner pool.

Help me troubleshoot why my GitHub Actions workflow is stuck in the queue.

✔️ Awesome, I've got everything!

Great. Double check you've saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the full src/stringUtils.js at this point:

function capitalize(str) {
  if (!str) return '';
  return str.charAt(0).toUpperCase() + str.slice(1);
}

function reverse(str) {
  if (!str) return '';
  return str.split('').reverse().join('');
}

function truncate(str, maxLength) {
  if (!str) return '';
  if (str.length <= maxLength) return str;
  return str.slice(0, maxLength - 3) + '...';
}

function slugify(str) {
  if (!str) return '';
  return str
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9\s-]/g, '')
    .replace(/[\s]+/g, '-');
}

module.exports = { capitalize, reverse, truncate, slugify };

And here is the full tests/stringUtils.test.js:

const { describe, it } = require('node:test');
const assert = require('node:assert');
const { capitalize, reverse, truncate, slugify } = require('../src/stringUtils');

describe('capitalize', () => {
  it('capitalizes the first letter', () => {
    assert.strictEqual(capitalize('hello'), 'Hello');
  });

  it('returns empty string for empty input', () => {
    assert.strictEqual(capitalize(''), '');
  });

  it('handles single character', () => {
    assert.strictEqual(capitalize('a'), 'A');
  });
});

describe('reverse', () => {
  it('reverses a string', () => {
    assert.strictEqual(reverse('hello'), 'olleh');
  });

  it('returns empty string for empty input', () => {
    assert.strictEqual(reverse(''), '');
  });

  it('handles palindromes', () => {
    assert.strictEqual(reverse('racecar'), 'racecar');
  });
});

describe('truncate', () => {
  it('truncates long strings with ellipsis', () => {
    assert.strictEqual(truncate('Hello World', 8), 'Hello...');
  });

  it('does not truncate short strings', () => {
    assert.strictEqual(truncate('Hi', 10), 'Hi');
  });

  it('returns empty string for empty input', () => {
    assert.strictEqual(truncate('', 5), '');
  });
});

describe('slugify', () => {
  it('converts a string to a slug', () => {
    assert.strictEqual(slugify('Hello World'), 'hello-world');
  });

  it('removes special characters', () => {
    assert.strictEqual(slugify('Hello! World?'), 'hello-world');
  });

  it('returns empty string for empty input', () => {
    assert.strictEqual(slugify(''), '');
  });
});
Intentionally introduce a bug

A pipeline that only ever passes does not teach you much. The real value of CI/CD shows up when something breaks and the pipeline catches it before anyone else sees the damage.

  • In src/stringUtils.js, scroll to the slugify function.
  • Find this line:
    .replace(/[\s]+/g, '-');
  • Change the '-' to '_', so the line now looks like this:
    .replace(/[\s]+/g, '_');

This makes slugify produce underscores instead of hyphens. Your tests expect hyphens, so they will fail.

  • Save src/stringUtils.js.
  • Commit and push the broken code by running these commands:
git add .
git commit -m "Introduce bug in slugify"
git push
  • Switch back to the **Actions** tab in your GitHub repository.
  • Click the latest workflow run (it will have an orange dot while running).

After the run completes, you should see a red X next to the commit. The pipeline caught your bug.

  • Under **Jobs**, click the **test** job.
  • Click the **Run tests** step to expand its log output.

The failed step is automatically expanded showing which assertion failed. You can see the expected value versus what was actually returned.

Why does CI catch this faster than manual testing?

In a team setting, this bug would have been caught the moment the developer pushed. No teammate needs to review or run the tests manually. The pipeline acts as an automated quality gate that runs every single time code changes.

This is the core promise of Continuous Integration. Integrate early, test automatically, catch problems before they spread.

Fix the bug and restore the green checkmark
  • Return to src/stringUtils.js in VS Code.
  • Find this line in the slugify function:
    .replace(/[\s]+/g, '_');
  • Change the '_' back to '-', so it reads:
    .replace(/[\s]+/g, '-');
  • Save src/stringUtils.js.
  • Verify the fix locally by running:
npm test

All 12 tests should pass again.

  • Commit and push the fix by running these commands:
git add .
git commit -m "Fix slugify bug"
git push
  • Switch back to the **Actions** tab one last time.
  • Wait for the workflow run to complete.

You should see a green checkmark on your latest commit. Your pipeline is passing again.

  • Click the **Code** tab in your repository to view your commit history.

You should see three recent commits: a green checkmark, a red X, and another green checkmark. This pattern tells the story of your SDLC loop. You wrote a feature, broke it, and fixed it. The pipeline tracked every step.

What is the SDLC loop?

The Software Development Lifecycle (SDLC) is the repeating cycle of writing code, testing it, and shipping it. Your commit history now shows a real SDLC loop: code, commit, automated test, feedback, fix, repeat.

Professional teams run this loop dozens of times per day. The CI pipeline makes it safe by catching failures instantly instead of days later when a customer reports a bug.

Still seeing a red X after your fix?

Make sure you changed the underscore back to a hyphen in the slugify function. Run npm test locally to confirm all 12 tests pass before pushing.

If tests pass locally but fail on GitHub, double check that you committed and pushed the fix. Run git status to confirm there are no uncommitted changes.

Help me figure out why my pipeline is still failing after I fixed the bug.

Secret mission

Add Linting and a Status Badge

Your pipeline runs tests, but professional teams also enforce code style automatically. Can you add ESLint as a linting step that runs before your tests, then show off your pipeline status with a badge in your README?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project uses a public GitHub repository, so GitHub Actions minutes are completely free with no ongoing costs.

Resources you used:

  • Local string-utils-ci project folder on your Desktop.
  • Public string-utils-ci repository on GitHub.

Keep everything running

No action needed. Choose this if you want to keep experimenting with your CI/CD pipeline or add more utility functions.

  • Your local project folder stays on your Desktop.
  • Your GitHub repository remains public and your Actions workflow continues to run on every push.
  • Node.js stays installed on your machine for future projects.

Pause - I'll come back to this later

Since there are no running processes or paid resources, your project is already paused by default.

  • Your local files remain on your Desktop until you need them again.
  • Your GitHub repository stays intact. The Actions workflow only triggers when you push new code, so nothing runs while you are away.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Delete the local project folder by running this command in your terminal:

macOS

cd ~/Desktop
rm -rf string-utils-ci

Windows

cd ~\Desktop
rmdir /s /q string-utils-ci
  • Delete the GitHub repository:
  • Open your string-utils-ci repository on GitHub.
  • Click Settings in the top navigation bar of the repository.
  • Scroll to the bottom of the page to the Danger Zone section.
  • Click Delete this repository and follow the confirmation prompts.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully automated CI/CD pipeline that tests your code every time you push to GitHub.

You've learned how to:

  • Build a JavaScript utility library with automated tests using Node.js built-in test runner and zero external dependencies.
  • Create a GitHub Actions CI pipeline that automatically checks out code, sets up the environment, and runs tests on every push.
  • Experience the full SDLC loop firsthand: write code, test locally, push, watch automated CI run, catch failures, and fix them.
  • Secret Mission: Added a linting stage to your pipeline and a live status badge to your README.

Ready to quiz yourself?