TidyURLs
Go to Tools
BLOG
The TidyURLsJournal

Practical guides, tutorials, and insights about URLs, SEO, web development, and online marketing. Learn, troubleshoot, and get the most of the web.

Back to the Journal
TidyURLs Journal

GitHub Actions Node.js CI Tutorial: Build and Test Your Project

Practical insights, clear explanations, and useful ideas for understanding the web.

October 25, 20267 min read6 topics
GitHub Actions Node.js CI Tutorial: Build and Test Your Project
Explore the details
#GitHub Actions#Node.js#CI#DevOps#npm#workflow security
October 25, 20267 min read

GitHub Actions Node.js CI Tutorial: Build and Test Your Project

Continuous integration (CI) runs checks automatically when code changes, helping a team catch problems before merging or deploying. For a Node.js project, a useful starting point is a workflow that installs the locked dependencies and runs the project’s test command on pushes and pull requests. This tutorial builds that workflow and explains how to handle triggers, caching, permissions, secrets, and untrusted pull requests safely.

Key takeaways

  • Store workflow files in .github/workflows/ as YAML.
  • Use npm ci with a committed lockfile for reproducible dependency installation.
  • Use actions/setup-node to select Node.js and cache npm’s download data.
  • Grant the workflow only the permissions it needs; a test job usually needs read access to repository contents.
  • Treat pull-request code as untrusted, especially when secrets or write permissions are available.

Create a basic Node.js CI workflow

Add a file such as .github/workflows/ci.yml to your repository:

name: Node.js CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

This example assumes the repository has a package-lock.json and a test script in package.json. Change the Node.js version to one supported by your project, and replace npm test if your project uses a different test command or package manager.

For example, a minimal package.json test script might look like this:

{
  "scripts": {
    "test": "node --test"
  }
}

The test runner and command must match your project. If you use a framework such as Jest or Vitest, define the appropriate script and make sure its dependencies are in the project manifest.

Understand the workflow’s parts

Triggers

The on section defines when GitHub Actions runs the workflow. In this example:

  • push runs when commits are pushed to main.
  • pull_request runs when a pull request targets main, including when new commits are added to that pull request.

Adjust the branch names to match your repository. You can also omit the branches filters if checks should run for all branches. More specific filters can reduce unnecessary runs, but make sure they do not accidentally skip checks required by your merge process.

Jobs and steps

A workflow contains one or more jobs. GitHub runs each job on a runner—in this example, an Ubuntu-hosted runner—and executes its steps in order. The job checks out the repository, selects Node.js, installs dependencies, and runs tests. If a step fails, the job fails, making the result visible in the Actions interface and usually on the pull request.

Dependency installation and caching

npm ci is designed for automated installs using a lockfile. It installs the dependency tree recorded in package-lock.json and fails if the lockfile does not match package.json. Commit the lockfile and update it when dependency declarations change. Avoid replacing npm ci with npm install in CI unless you have a specific reason to allow the lockfile to be updated.

The cache: npm setting asks setup-node to cache npm’s package data, which can reduce repeated downloads. It does not cache node_modules in this example. The lockfile is used to identify when the dependency cache should be refreshed, so keeping it accurate matters. Caching is a performance optimization, not a substitute for dependency installation or verification.

Add checks that reflect your project

Once the basic workflow passes, add the checks your project actually relies on. For example, if package.json defines lint and build scripts, you can run them as separate steps:

      - name: Run tests
        run: npm test

      - name: Check formatting and lint
        run: npm run lint

      - name: Build
        run: npm run build

Do not add commands for scripts that do not exist. A failing CI job should point to a real project check, not a missing script. If you support multiple Node.js versions, you can use a matrix, but start with the version or versions your project promises to support.

Permissions and secrets

The workflow sets permissions: contents: read, which is generally enough for a job that only needs to check out code and run tests. Avoid granting write permissions unless a job needs them. If a workflow specifies some token permissions, GitHub treats unspecified permissions as disabled for that token; review the permissions required by every action and job you add.

A test workflow usually does not need secrets. Do not add credentials “just in case.” If a separate job needs a secret—for example, to publish a release—scope it to that job or a protected environment where possible. Limit who can approve or access that environment, and consider short-lived identity mechanisms such as OpenID Connect when a supported deployment service offers them.

Never print secret values to logs, place them in source files, or pass them to steps that do not need them. Masking is not a guarantee against every way a value might be exposed, especially if untrusted code can run in a context where the secret is available.

Handle untrusted pull requests safely

Pull-request code can change tests, scripts, and package lifecycle hooks. Running those changes is a normal part of CI, but it means the workflow should treat the checked-out code as untrusted.

For ordinary validation, use the pull_request trigger and keep the job’s permissions minimal. For pull requests from forks, GitHub generally withholds repository secrets and restricts the token, but repository settings and workflow design matter. Do not rely on those defaults as a reason to expose credentials to code you do not trust.

Be especially careful with pull_request_target. It runs in the context of the base repository and can have access to more powerful permissions or secrets. Do not use it to check out and execute a pull request’s untrusted code with those privileges. Keep privileged tasks separate from validation, and only run them after appropriate review and with narrowly scoped credentials.

Common mistakes and troubleshooting

  • The install step fails because a lockfile is missing. Commit the lockfile for your package manager and use the matching install command. For npm, npm ci expects a compatible package-lock.json or supported shrinkwrap file.
  • The workflow cannot find the test command. Confirm the relevant script exists under scripts in package.json, and that its dependencies are declared.
  • The wrong Node.js version is selected. Set the workflow version to a version your application supports. If local development uses a version file, configure the workflow to use it consistently where appropriate.
  • The workflow runs more often than expected. Review the event and branch filters. Remember that a push and a pull-request update can each cause runs depending on the events and repository configuration.
  • A new action asks for broader permissions. Check its documentation and source, then grant only the access the workflow actually needs. For higher-assurance environments, consider pinning third-party actions to reviewed full commit SHAs rather than relying only on mutable version tags.
  • A cache does not make the job much faster. Check that the package manager and lockfile are detected as expected. Caching helps avoid repeated downloads; it does not remove the need to install dependencies.

A practical setup checklist

  1. Confirm the project’s supported Node.js version and package manager.
  2. Commit the package manifest and lockfile.
  3. Add the workflow under .github/workflows/.
  4. Run the workflow on a branch and fix any environment-specific test failures.
  5. Check that pull requests show the expected status check before merging.
  6. Keep validation jobs free of secrets and grant only necessary token permissions.
  7. Add build, lint, or deployment jobs only when they serve a clear purpose and have appropriate safeguards.

Conclusion

A small GitHub Actions workflow can provide reliable Node.js CI by checking out the code, selecting a supported runtime, installing locked dependencies, and running tests. Start with the least privilege and no secrets, then expand the workflow as your project’s checks and release process require. Treat pull-request code as untrusted and keep any privileged operations isolated from it.