Automating Quality Checks With GitHub Actions
Version control workflows typically revolve around a main branch holding the functional codebase, with developers working on separate branches for features or fixes. This separation makes it easier to trace errors before merging changes. But manually reviewing every line of code for bugs is impractical, even for smaller projects. Continuous integration (CI) solves this by automating the build and test process whenever code is pushed or a pull request is opened.
CI ensures incoming changes don't "break the build." A build compiles source code into an executable format; a successful build signals the changes are safe to integrate, while a failure flags issues that need re-evaluation. Catching defects early is far cheaper than fixing them later. Tools like Jenkins, TravisCI, CircleCI, and GitLab CI are available, but this tutorial uses GitHub Actions, a feature that lets you create workflows triggered by repository events such as pushes or pull requests.
Preparing a Node.js API for CI
The example project is a REST API called countries-info-api built with Node.js, Express, and PostgreSQL. It has no role-based authorization, so anyone can add, delete, or update country details. Each country record includes an auto-generated UUID id, name, capital, and population. The server is written in ES6 format and compiled with Babel, with Nodemon for automatic restarts during development.
Dependencies are installed via npm, with development-only packages (marked by the --save-dev flag) appearing under devDependencies in package.json. The server itself runs on a PORT variable defined in a .env file, loaded by the dotenv package to keep sensitive variables out of the codebase. Environment variables also include the PostgreSQL connection string in the format DATABASE_URL=postgres://user:password@host:port/database_name.
The Sequelize ORM handles database interactions, eliminating the need for raw SQL queries. The country model is defined using sequelize.define, and the PostgreSQL database is created with the CREATE DATABASE database_name command via the PSQL Shell.
Writing Tests and Measuring Coverage
Testing helps developers understand how their software behaves for end users and catches bugs early. For this project, unit tests and end-to-end tests are written using the Mocha test framework and the Chai assertion library. The sequelize-test-helpers package assists with testing Sequelize models.
Test coverage analysis shows whether test cases actually exercise the codebase and how much code is touched during runs. Istanbul, along with its CLI client nyc, instruments ES5 and ES2015+ JavaScript with line counters to track test coverage. The test script in package.json runs the tests and generates a report, producing a .nyc_output folder with raw coverage data and a coverage folder with formatted report files—both placed in .gitignore.
To report coverage results, the coveralls npm package is installed, and after signing in and adding the repository on Coveralls, a coveralls.yml file in the root directory holds the repo-token from the repo's settings. A dedicated coverage script runs the tests, generates the report, and sends it to Coveralls, which breaks down coverage on a file-by-file basis and highlights covered, missed, and changed lines.
Setting Up the Workflow File
GitHub provides starter templates for workflow files on the Actions page. The Node.js workflow template, saved as .github/workflows/node.js.yml in the root directory, contains basic commands with comments explaining their function. For this project, the file is modified to run both the standard build and coverage reporting.
The key elements of the workflow file are:
name: The displayed name of the workflow (e.g., "NodeJS CI") on the repository's Actions page.on: The triggering event—here, a push to the repository.jobs: A workflow can contain one or more jobs. In the initial sample, a single job named "build" runs in an environment specified byruns-on(e.g., a Windows environment). This can be split into separate build and coverage jobs.env: Environment variables available to jobs and steps. In the coverage job, sensitive variables are "hidden" and stored in the repo's secrets page under settings.steps: The list of actions executed for a job.
The build job uses a checkout action (version 2) to make the repository accessible, a setup-node action to configure the Node environment, and then runs the install, build, and test scripts from package.json. A separate coverage job uses a Coveralls action to post the test suite's LCOV coverage data to coveralls.io.
During initial testing, pushing to the feat-add-controllers-and-route branch without the repo_token in .coveralls.yml resulted in an error. After adding the token, the build ran successfully—demonstrating CI's value in catching configuration issues before they reach the main branch. The error and coverage summary were visible on the terminal thanks to the --verbose flag at the end of the coverage script.
What the Pipeline Achieves
With the workflow in place, every push to a pull request and to the main branch triggers a fresh test run and coverage report. The workflow file is stored in .github/workflows/ci.yml, making it version-controlled and visible to all contributors. This gives the team an immediate signal on whether new changes are safe to merge, without requiring anyone to run tests locally before pushing.
The two independent jobs — one for the test suite, one for coverage — keep responsibilities separate. If coverage fails because of a threshold not being met, that job fails on its own, while the core tests still complete and report their own status. This separation makes it easier to diagnose issues: you can tell at a glance whether the problem is a failing test or a coverage regression.
Reading the Results
Once a workflow run completes, GitHub’s Actions tab shows each job’s status. Clicking into a job reveals the step logs, including the exact command that failed and the output it produced. For the coverage job, you can inspect the lcov.info file that gets uploaded as an artifact. This file contains per-file coverage statistics. It’s also possible to use third-party services to post coverage summaries directly in pull requests, but GitHub’s built-in checks are sufficient for most small-to-medium projects.
Successfully completed runs produce a green checkmark on the pull request. That visibility is valuable in a collaborative setting — reviewers can quickly confirm that proposed changes do not break existing tests before they invest time in manual review. After all jobs pass, the branch is ready to merge with confidence.
Recommended Next Steps
- Adjust thresholds per project: The coverage minimum should reflect the project’s risk profile. A library with complex logic likely warrants a higher bar than a small utility repo.
- Evaluate each run’s logs: Even when the jobs succeed, scan the step output. Sometimes warnings appear in the logs that indicate configuration drift or deprecation notices that need attention.
- Expand the workflow over time: The same pattern used for
vitestapplies to build steps, type checks, or linting. Add another job with its ownoncondition and command set, and it runs alongside the existing ones.
Going Further
The example project is intentionally minimal, but the CI pattern scales well to larger codebases. As new features land, the workflow only grows linearly — one more job step, one more artifact if needed. The core benefit is constant: every proposed change runs against the full automated suite before it reaches the main branch.
Perhaps the most valuable outcome is the shift in workflow culture. Authoring a CI pipeline transforms testing from a pre-merge ritual into an automated gate that applies uniformly to every contributor. That consistency is what makes continuous integration genuinely continuous — the checks run the same way, every time, without relying on a developer remembering to run them locally.



