Automate Commit Linting and Package Releases with Github Actions

Automate Commit Linting and Package Releases with Github Actions

LinkIconFeatures in this article

  1. Commitlint: Enforce commit message rules.

  2. Commitlint with GitHub Actions: Add a CI/CD workflow to lint commit messages on the remote (useful because developers can skip local checks).

  3. Git Hooks with Husky: Configure Git hooks to validate commit messages before completing a git commit.

  4. Commit Template Generator: Since remembering commit rules is annoying, a generator is ideal. Commitizen integrates well with Commitlint.

  5. Automated Versioning: changesets is excellent for monorepos. Automated with Github Actions

LinkIconCommitLint

We will enforce commit message using the Conventional Commit rules.

  1. Install v20
pnpm add -D @commitlint/cli @commitlint/config-conventional
  1. Create .commitlintrc.json
{
  "extends": ["@commitlint/config-conventional"]
}

Here is an extended example configuration:

{
  "extends": ["@commitlint/config-conventional"],
  "rules": {
    // 🆕 Define your own rules
    "type-enum": [2, "always", [
      "feat",
      "fix",
      "docs",
      "style",
      "refactor",
      "test",
      "build",
      "chore",
      "release",
      "revert",
      "perf",
      "enh",
      "ci",
      "security"
    ]],
    // 🆕 Define your own scopes for a commit
    "scope-enum": [2, "always", ["web", "cli"]],
    "type-case": [2, "always", "lower-case"],
    "type-empty": [2, "never"],
    "scope-case": [2, "always", "lower-case"],
    "subject-empty": [2, "never"],
    "subject-full-stop": [2, "never", "."],
    "header-max-length": [2, "always", 100]
  },
  // These config are set by default
  "prompt": {
    "settings": {},
    "messages": {
      "skip": "':skip",
      "max": "upper %d chars",
      "min": "%d chars at least",
      "emptyWarning": "can not be empty",
      "upperLimitWarning": "over limit",
      "lowerLimitWarning": "below limit"
    },
    "questions": {
      "type": {
        "description": "Select the type of change that you're committing:",
        "enum": {
          "feat": {
            "description": "Add a new feature.",
            "title": "Features"
          },
          "fix": {
            "description": "Submit a bug fix.",
            "title": "Bug Fixes"
          },
          "docs": {
            "description": "Documentation changes.",
            "title": "Documentation"
          },
          "style": {
            "description": "Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc).",
            "title": "Styles"
          },
          "refactor": {
            "description": "Neither fixes a bug nor adds a feature.",
            "title": "Code Refactoring"
          },
          "test": {
            "description": "Add or modify tests.",
            "title": "Tests"
          },
          "build": {
            "description": "Changes that affect build, bundling, deployment artifacts (Vite, Turborepo, Terraform).",
            "title": "Builds"
          },
          "chore": {
            "description": "Other changes that don't modify src, test files, build, or CI.",
            "title": "Chores"
          },
          "release": {
            "description": "Publish a new package version.",
            "title": "Package Release"
          },
          "revert": {
            "description": "Revert a previous commit.",
            "title": "Reverts"
          },
          "perf": {
            "description": "Improve performance without API changes (speed, render, response times, DB queries).",
            "title": "Performance"
          },
          "enh": {
            "description": "Improve an existing feature (UI, UX, outputs).",
            "title": "Enhancement"
          },
          "ci": {
            "description": "Changes to CI config files and scripts.",
            "title": "Continuous Integrations"
          },
          "security": {
            "description": "Changes that fix vulnerabilities (authentication, permissions).",
            "title": "Security Improvements"
          }
        }
      },
      "scope": {
        "description":
          "What is the scope of this change (e.g. component or file name)"
      },
      "subject": {
        "description": "Write a short, imperative tense description of the change"
      },
      "body": {
        "description": "Provide a longer description of the change"
      },
      "isBreaking": {
        "description": "Are there any breaking changes?"
      },
      "breakingBody": {
        "description":
          "A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself"
      },
      "breaking": {
        "description": "Describe the breaking changes"
      },
      "isIssueAffected": {
        "description": "Does this change affect any open issues?"
      },
      "issuesBody": {
        "description":
          "If issues are closed, the commit requires a body. Please enter a longer description of the commit itself"
      },
      "issues": {
        "description": "Add issue references (e.g. 'fix #123', 're #123'.)"
      }
    }
  }
}

LinkIconCommitLint with Github Actions

Commit messages are linted locally, but they can be bypassed using the git commit --no-verify flag.

We can still double-check and lint the commit on remote using Github Actions.

We will use a pre-built Github Action: commit-lint-github-action

  1. First, ensure your code is pushed and published in Github

  2. Create .github/workflows/commitlint.yml and add the following config:

.github/workflwos/commitlint.yml
name: Lint Commit Messages
on: [pull_request, push]
 
permissions:
  contents: read
  pull-requests: read
 
jobs:
  commitlint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: wagoid/commitlint-github-action@v6

This workflow runs whenever code is pushed or a pull request is opened.

  1. Check the workflow under the Actions tab on GitHub.

LinkIconGit Hook: Husky with CommitLint

About Git Hooks

Avoid enforcing Git hooks in a corporate or contributor environment, as this may slow their workflow. Instead, prefer running Git hooks in a CI pipeline (Github Action) or use them locally at your discretion. Watch Theo take on Github Hooks

We want to perform commit message validation before creating the commit.

The git hook commit-msg is ideal for real.

Rather than writing the hook manually from scratch, we will use Husky, a handy git hook helper.

  1. Install Husky v9
pnpm add -D husky
 
# Active husky
pnpm exec husky install
 
# add hook (optionally use `pnpx` instead of `npx`)
echo "npx --no -- commitlint --edit \$1" > ./husky/commit-msg

Running above code must create the follwing file .husky/commit-msg

BashIcon.husky/commit-msg
npx --no -- commitlint --edit $1
  1. Try commiting test commit, it must fail because it is not a valid commit.

    Then try to commit chore: add commitlint and husky should pass.

LinkIconOptional: More Git Hooks

  1. Format, lint, and test code before commiting.
# Add pre-commit hook
echo "pnpm format && pnpm lint && git add ." > .husky/pre-commit
  1. Format, lint, and test before pushing to remote.
echo "pnpm check" > .husky/pre-push

Tips

  • Only run unit tests in Git hooks
  • E2E tests should run in CI pipeline (Github Actions)

LinkIconCommit Template Generator: Commitizen

Memorizing commit rules is tedious. Commitizen provides an interactive CLI for writing valid commits.

  1. Install Commitizen v4
pnpm add -D commitizen @commitlint/cz-commitlint inquirer@9
  • @commitlint/cz-commitlint is a commitizen adapter to adhere to the commit convention configured in commitlint.config.cjs. See Step 3 for configuration.

  • inquirer is a peer dependency needed

  1. Add script in package.json
{
	"scripts": {
		"cm": "cz"
	}
}
  1. Make your project Commitizen friendly by adding this config to package.json
{
	"config": {
    "commitizen": {
      "path": "@commitlint/cz-commitlint"
    }
  }
}

Now Commitizen uses rules from .commitlintrc.json.

  1. At this point running pnpm cm should starts Commitizen.

    Use this instead of git commit.

  2. (Optional) To trigger Commitizen on git commit, then create a prepare-commit-msg hook with Husky.

BashIcon.husky/prepare-commit-msg
exec < /dev/tty && npx cz --hook || true

And make the file executable

chmod +x .husky/prepare-commit-msg

Now running git commit opens the Commitizen prompt, ideal for contributors unfamiliar with pnpm cm.

LinkIconAutomate Versioning with Changesets

Changesets is a versioning and changelog tool designed for monorepos.

It stores version changes as markdown files called "changesets". This file is used to track the type of version bump with a description following semantic versioning.

Changesets follows semantic versioning: major.minor.patch.

A patch is a bug fix, a minor is a new feature, a major is a breaking change.

  1. Install Changesets v2
pnpm install -D @changesets/cli && pnpx changeset init

Running this command will generate a .changeset folder with a README.md and a config.json.

JSONIcon.changeset/config.json
{
	"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
	"changelog": "@changesets/cli/changelog",
	"commit": false,
	"fixed": [],
	"linked": [],
	"access": "public",
	"baseBranch": "main",
	"updateInternalDependencies": "patch",
	"ignore": []
 }
  1. After completing your PR commits, create a "changeset" to version a package
pnpx changeset

or add to package.json to run with pnpm changeset

{
	"scripts": {
		"changeset": "changeset"
	}
}

You’ll be asked which packages to bump and for a description.

Changeset reads all packages defined in the root package.json

If workspaces is not defined, add it:

JSONIconpackage.json
{
  "workspaces": ["apps/*", "packages/*"]
}
  1. (Optional) Install changeset bot for contributors. This bot will remind all contributors to include a changeset whenever they create a PR.

  2. Manual release commands. (This command is not needed, explained in the next section)

# bumps version based on markdown files
pnpx changeset version
# releases to npm
pnpx changeset publish

LinkIconAutomate Changesets Releases with Github Actions

While we can do this manually, we can automate releases with Github actions.

  1. Create .github/workflows/release.yml
.github/workflows/release.yml
name: Release
 
on:
  push:
    branches:
      - main
 
concurrency: ${{ github.workflow }}-${{ github.ref }}
 
jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repo
        uses: actions/checkout@v3
 
      - name: Setup Node.js 20.x
        uses: actions/setup-node@v3
        with:
          node-version: 20.x
 
      - name: Install Dependencies
        run: npm install
 
      - name: Create Release Pull Request or Publish to npm
        id: changesets
        uses: changesets/action@v1
        with:
          publish: npx changeset publish
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
  1. This workflow needs a NPM Token in order to do releases in NPM.
  • Create an NPM_TOKEN at npmjs.com with automation permissions.
  • Store it in Github Actions secrets as NPM_TOKEN at Settings -> Secrets and variables -> Actions
  • By default Github Actions cannot create PRs. Enable read/write Actions permissions under Settings -> Actions -> General -> Enable "Read and write permissions"
  • GitHub provides GITHUB_TOKEN automatically (no need to manually create one). You must create manually if you need:
    • accces across multiple repos
    • permissions beyond GITHUB_TOKEN
    • Github Apps or user PAT integrations

How the workflow work

  • Runs on every push to main.

  • If "changeset" markdown file exists, Github Action will create/update a "Release Preview PR". This "Release Preview PR" has an overview of all changes currently on the main branch but not yet in the latest release

  • This "Release Preview PR" includes:

    • Removal of all "changeset" markdown files
    • All Packages version are bumped
    • Changelog entry is added for every "changeset" file
    • Uses pnpx changeset version
  • When ready to release, merge the "Release Preview PR" into main.

    The Github Action handles publishing to npm (pnpx changeset publish), updating tags and changelogs

  1. Bonus: Include PR links and GitHub usernames in the changelog automatically.
pnpm add -D @changesets/changelog-github

Then add the formatter adapter in .changeset/config.json

JSONIcon
{
	"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
	"changelog": ["@changesets/changelog-github", {
	...
}

Now when the "release PR" is merge to main, the changelog will include links to PR as well as authors.

Here is a potential workflow:

  • You put up a PR to add a new Alert component.
  • When PR is done (done with commits), run pnpx changeset and select a minor version change, since we are adding new functionality.
  • You'll be prompted for a summary of the changes. Write something nice about our new Alert component, how to import it, when to use it, and how to use it.
  • It'll then ask you for confirmation of the changeset.
  • A changeset is added to your changes as a markdown file.
  • You commit this markdown file and push it to your branch. Let markdown merge in with your PR.
  • Your PR merges with main, but it's not yet released to consumers. It's simply in main.
  • Changesets Github Action creates or updates the "Release Preview PR"
  • This "Release Preview PR" keeps track of all changes that'll go out in the next release. It looks at the "largest change" to determine which versioning path it should take. So if you have 2 minor updates and one patch, the next release will be a minor update. If you have a major update and 500 patch changes, it'll bump it up to major.
  • Merging the "Release Preview PR" to main versions your package(s), creates a tag, and publishes to npm. Using the changeset markdown files you created earlier, the changelog file is automatically updated with this information. If you're on GitHub, it creates a nice release log for you too using the same changeset files.

LinkIconReferences