Set Up CI/CD with GitHub Actions in 10 Minutes

CI/CD stands for Continuous Integration and Continuous Delivery (or Deployment). In practice, it means your code gets tested — and optionally deployed — automatically every time you push a change, instead of you running npm test by hand and hoping you didn’t forget a step. The “CI” part runs your test suite and checks on every push or pull request. Meanwhile, the “CD” part takes over from there and ships the code somewhere once it passes.

It’s easy to assume this kind of automation is only worth the setup cost on large teams with complex release processes. However, that assumption is backwards. On a small project, you’re the only safety net — there’s no reviewer catching a broken build before it merges. As a result, a five-minute YAML file that runs your tests on every push catches regressions before they reach main, and it costs you nothing per run on public repos (and a generous free tier on private ones). For a broader primer on the concept, GitHub’s own introduction to continuous integration is a solid companion read.

By the end of this article, you’ll have a working GitHub Actions workflow that runs your test suite automatically on every push and pull request, and you’ll know how to check whether it passed. There’s also a short section on adding automatic deployment on top, for when you’re ready for that.

Prerequisites

Before starting, make sure you have:

  • A GitHub account and a repository already pushed to it
  • A project with a test command that runs locally (e.g., npm test, pytest, go test ./...)
  • Git installed locally, with the repo cloned to your machine

This tutorial uses a generic Node.js project as the example, but the same structure applies almost unchanged to Python, Go, Ruby, or anything else — only the setup and test-run steps differ. If your project doesn’t have tests configured yet, it’s worth pausing here and setting up a basic suite first, since a CI pipeline with nothing to run isn’t especially useful. Check out our guide to writing your first test suite if you need a starting point.

Step 1: Create the Workflow File

GitHub Actions looks for workflow definitions in a specific folder: .github/workflows/. Any YAML file placed there is picked up automatically — no registration step, no dashboard configuration.

From the root of your project, create the folder and a file inside it:

The filename itself doesn’t matter to GitHub — ci.yml, main.yml, tests.yml all work the same. What matters is that it lives inside .github/workflows/ and has a .yml or .yaml extension.

Step 2: Understand the Basic Structure

Every GitHub Actions workflow is built from three concepts, nested inside each other:

  • Triggers (on) — what event starts the workflow: a push, a pull request, a schedule, a manual click.
  • Jobs — a workflow can have one or more jobs. Each job runs on its own fresh virtual machine.
  • Steps — the individual commands or actions that run inside a job, in order, top to bottom.

Here’s the skeleton, with no real logic yet, just to see the shape:

name is just a label shown in the GitHub UI. on defines the triggers. runs-on picks the virtual machine image the job executes on — ubuntu-latest is the standard, fastest choice unless you specifically need macOS or Windows runners.

Step 3: Write a Real Workflow That Runs Your Tests

Now replace the placeholder with an actual working example for a Node.js project:

Here’s a quick breakdown of what each step does:

  • Check out repository — pulls your repo’s code onto the runner. Without this, the VM is empty; nothing to test.
  • Set up Node.js — installs the Node version you specify. Additionally, pin an exact version (or read it from an .nvmrc) instead of leaving it unspecified — this avoids surprises when GitHub updates its default images.
  • Install dependenciesnpm ci is preferred over npm install in CI, since it installs exactly what’s in package-lock.json and fails if the lockfile is out of sync. That’s exactly the strictness you want in an automated pipeline.
  • Run tests — replace npm test with whatever your project actually uses (npm run test:ci, yarn test, etc.).

If you’re on Python, the same three-step logic applies: check out the code, set up the language runtime with actions/setup-python@v7, install dependencies (pip install -r requirements.txt), then run tests (pytest). The trigger and job structure around it doesn’t change at all — only the middle steps are language-specific.

Once you’ve written the file, commit and push it:

Step 4: Verify the Workflow Ran

Go to your repository on GitHub and click the Actions tab. You should see your workflow listed, with a run corresponding to the commit you just pushed.

Here’s what to look for:

  • A yellow dot means the workflow is currently running.
  • A green check means every step succeeded.
  • Conversely, a red X means something failed — click into the run, then into the specific step, to see the exact error output. It’s the same log you’d get running the command locally, just captured from the runner.

If the workflow doesn’t show up at all, double-check that the file is actually inside .github/workflows/ (not .github/workflow/ or github/workflows/) and that the YAML indentation is valid — a single misaligned line can cause the whole file to be silently ignored.

Optional: Adding Automatic Deployment

Once tests pass reliably, you can extend the same file — or add a second job — to deploy automatically after a successful push to main. The pattern is usually:

The needs: test line makes this job wait for the test job to finish successfully first — no deploying broken code. What goes inside the actual deploy step depends entirely on where you’re deploying to (Vercel, a VPS via SSH, AWS, a container registry, etc.), so that part is intentionally left generic here. Most hosting providers publish their own official GitHub Action for this exact purpose — worth checking before writing a deploy script by hand.

Common Mistakes to Avoid

Forgetting to pin action versions. Using @main or leaving off the version tag on an action means your workflow’s behavior can change without warning when the action’s maintainers push an update. Pin to a specific major version (e.g., @v7 for actions/checkout and actions/setup-node as of mid-2026) at minimum.

Using npm install instead of npm ci in CI. npm install can silently update package-lock.json and mask dependency drift. npm ci is stricter and faster, and it’s what CI environments are meant to use.

Not scoping the trigger branches. Leaving on: push with no branch filter means the workflow fires on every branch, including short-lived feature branches, which burns Actions minutes for no benefit. Scope it to the branches you actually care about.

Hardcoding secrets directly in the YAML file. API keys, tokens, and credentials belong in the repository’s Settings → Secrets and variables → Actions, referenced in the workflow as ${{ secrets.YOUR_SECRET_NAME }} — never typed in plain text into a file that’s committed to version control.

Leave a Comment

Your email address will not be published. Required fields are marked *