Automated integrity checks with Husky and GitHub Actions
JavaScript

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!