Automated integrity checks with Husky and GitHub Actions

  • JavaScript

Automated integrity checks with Husky and GitHub Actions

Ensuring code quality and consistency is crucial in any front-end project. Tools like ESLint, TypeScript, Prettier, and testing frameworks such as Jest help maintain high standards, but their effectiveness depends on how and when they are executed. To maximize their impact, we can automate their execution both locally (before commits) and remotely (on pull requests).

In this article, we'll see how to configure Husky to run checks locally before each commit, and GitHub Actions to run checks when a pull request is opened. This ensures developers catch most issues before pushing code and that every pull request is validated consistently in CI, reducing the risk of bad code reaching the main branch.

We assume your project already has ESLint, TypeScript, Prettier, and Jest installed and configured, so your package.json should include scripts like this:

JSON

{
"scripts": {
"check:src": "eslint .",
"check:tsc": "tsc --noEmit",
"prettier": "prettier . --write --ignore-unknown",
"test": "jest"
}
}

Pre-commit checks with Husky

Running all checks on every commit is usually too slow and interrupts the developer flow. A lightweight local guardrail that lints and type-checks code, and formats only the files being committed, gives fast feedback without turning commits into a chore. We’ll let CI run heavyweight suites (integration tests, long unit suites) when opening a PR, where time is less critical and more resources can be dedicated to such tasks.

Setup

To get Husky into your project, we first need to add it as a dev dependency:

yarn add --dev husky

After Husky is installed, let's initialize it:

npx husky init

This command creates the .husky/ folder with a starter pre-commit hook file, and updates package.json so that husky install runs on prepare. That’s helpful, because hooks will then be registered automatically after running yarn install.

Configuration

Once Husky is initialized, we can edit the pre-commit configuration file inside the .husky/ directory to run our checks before each commit:

Bash

. "$(dirname -- "$0")/_/husky.sh"
 
yarn check:src
yarn check:tsc
yarn prettier $(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') --write --ignore-unknown
git update-index --again

Here, yarn check:src and yarn check:tsc execute ESLint and TypeScript checks. The yarn prettier command formats only the files staged for commit. If Prettier modifies any of those files, git update-index --again re-stages them so that the commit will include the formatted versions.

Continuous Integration with GitHub Actions

While Husky handles local checks, configuring GitHub Actions we ensure that no code is merged unless it passes even more checks. To tell GitHub how and when to run actions, we have to create a .yaml file under the .github/workflows/ directory. This file can have any name, so in this case we will call it project-integrity.yaml.

Let's start giving our workflow a name. GitHub shows this in the Actions UI and in the pull request checks list. This is just metadata and doesn't affect behavior, but makes the run easier to recognize in the UI.

Yaml

name: Project integrity checks

Now, we have to tell GitHub which events should start the workflow. In the following example, our workflow runs when a pull request is opened, reopened, when a label is added or removed to it or when a new commit is pushed.

Yaml

on:
pull_request:
types:
- synchronize
- opened
- reopened
- labeled
- unlabeled

Inside a workflow you can define one or more jobs. Each job runs on a runner (a virtual machine or a container). Jobs are composed of ordered steps that run commands or call other actions. Jobs can run in parallel or be chained with dependencies. In our case, since all our commands depends on the same dependencies, we will define a single job.

Yaml

jobs:
project-integrity-checks:
name: Project integrity checks
runs-on: ubuntu-22.04

Now let's define our steps. The first step in almost every workflow is to check out the pull request's branch.

Yaml

steps:
- name: Checkout branch
uses: actions/checkout@v4

After that, we have to set up Node.js and install all dependencies.

Yaml

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20.18.0
cache: yarn
 
- name: Install dependencies
run: yarn install --network-concurrency 1

Finally, we can execute all our checks and the test suite of the project.

Yaml

- name: Run ESLint
run: yarn check:src
 
- name: Run TypeScript checks
run: yarn check:tsc
 
- name: Run tests
run: yarn test

The final result should look like this:

Yaml

name: Project integrity checks
 
on:
pull_request:
types:
- synchronize
- opened
- reopened
- labeled
- unlabeled
 
jobs:
project-integrity-checks:
name: Project integrity checks
runs-on: ubuntu-22.04
 
steps:
- name: Checkout branch
uses: actions/checkout@v4
 
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20.18.0
cache: yarn
 
- name: Install dependencies
run: yarn install --network-concurrency 1
 
- name: Run ESLint
run: yarn check:src
 
- name: Run TypeScript checks
run: yarn check:tsc
 
- name: Run tests
run: yarn test

Now your project is ready to be automatically checked both locally and on every pull request, keeping your workflow smooth and your codebase consistent!