Git and GitHub Best Practices at Necko Technologies

by Jérôme Dauge, Co-Founder

Git and GitHub Best Practices at Necko Technologies

In today's fast-paced development environment, maintaining a clean, understandable codebase history is crucial for team collaboration and project maintenance. At Necko Technologies, we've implemented a set of Git and GitHub practices that have significantly improved our development workflow. These practices help us maintain code quality, streamline releases, and make our Git history a valuable documentation resource rather than a confusing mess.

The Problem with Unstructured Commits

We've all seen (or created) Git histories that look like this:

Example of a messy Git history with unstructured commits

This kind of history provides little to no value when you need to understand what changed and why. It's difficult to generate meaningful changelogs, track features, or identify when bugs were introduced. To address this problem, we've standardized on several best practices.

Conventional Commits: Adding Meaning to Commit Messages

We follow the Conventional Commit specification, which provides a lightweight convention on top of commit messages. Using this standard gives our commit messages clear structure and intent, making our repository history readable and useful.

Each commit message follows this format:

<type>(<scope>): <subject>
 
<body>

Types

We use the following types to categorize our commits:

  • feat: A new feature
  • fix: A bug fix
  • build: Changes to build system or dependencies
  • ci: Changes to CI configuration files and scripts
  • docs: Documentation changes only
  • perf: Performance improvements
  • refactor: Code changes that neither fix bugs nor add features
  • chore: Maintenance tasks that don't modify source files
  • test: Adding or correcting tests
  • meta: Repository metadata changes

Scopes for Better Context

We emphasize using scopes to provide additional context. Different commit types typically use specific scope conventions:

  • For feat, fix, revert, and refactor: We use the package/module name where changes were made
  • For docs: Usually readme or the specific package with updated documentation
  • For deps: The programming language (e.g., node, python) or github-action
  • For ci: Typically the name of the CI job

Handling Formatting Commits

One particularly useful practice we've adopted is properly handling formatting commits. When you run tools like black or rome across the entire codebase, it can generate a commit that touches numerous files but only for formatting purposes. This can obscure the actual history of the code when using git blame.

We solve this with a .git-blame-ignore-revs file containing the SHA-1 hashes of formatting commits:

# Formatting with black v22.3.0
abd05e89c208c29e6d8afd9170b8326b540a1207
# Prettier reformatting
e7f9e82a0f6b5b17927639e9a77f882a9a57013f

This file is natively supported by Git and GitHub, though you need to enable it in your global Git config:

git config --global blame.ignoreRevsFile .git-blame-ignore-revs

Building a Clean, Linear History

While conventional commits provide structure, we also need to keep our repository history clean and focused. We achieve this through a disciplined approach to feature branches and pull requests.

Feature Branch Workflow

Our development process begins with feature branches:

  1. Create a new branch for each feature, fix, or improvement
  2. Make frequent commits in this branch (they don't need to follow conventions)
  3. Push regularly to back up your work
  4. When ready, create a pull request
Example of a feature branch

Structured Pull Requests

Pull request titles are crucial in our workflow. They must follow the conventional commit format since they'll become the commit message in the main branch. We enforce this using a GitHub action that validates PR titles.

Squash Merging: The Key to Clean History

When merging pull requests, we exclusively use squash merging. This condenses all the commits from the feature branch into a single, well-formatted commit on the main branch.

Without squash merging, our history would look messy:

Without squash merging

With squash merging, we get a clean, linear history:

With squash merging

Simplified Branch Strategy

We keep our branch strategy intentionally simple:

  • One long-lived branch: main
  • Short-lived feature branches that are deleted after merging

Every push to main triggers a non-production deployment, while releases trigger production deployments.

Automated Release Management

For release management, we leverage release-please, which automates version bumping and changelog generation based on conventional commits.

The process works like this:

  1. We commit changes following conventional commit standards
  2. Release-please creates a PR that includes version bumps and changelog updates
  3. When we're ready to release, we merge the PR
  4. GitHub Actions create a new release tag and trigger deployment

This results in a clean history with clear release points:

Clean linear history

Customizing Changelog Sections

We've customized our release-please configuration to include additional commit types in our changelogs:

[
  {
    "type": "feat",
    "section": "Features",
    "hidden": false
  },
  {
    "type": "fix",
    "section": "Bug Fixes",
    "hidden": false
  },
  {
    "type": "chore",
    "section": "Miscellaneous Chores",
    "hidden": true
  },
  {
    "type": "revert",
    "section": "Reverts",
    "hidden": false
  },
  {
    "type": "docs",
    "section": "Documentation",
    "hidden": false
  },
  {
    "type": "refactor",
    "section": "Code Refactoring",
    "hidden": false
  },
  {
    "type": "deps",
    "section": "Dependencies",
    "hidden": false
  },
  {
    "type": "ci",
    "section": "Continuous Integration",
    "hidden": false
  }
]

This configuration lives in release-please-config.json when using the manifest release type, or in the changelog-types field of the release-please job in your GitHub Actions workflow.

Benefits We've Seen

Since implementing these practices, we've experienced several significant improvements:

  1. Better collaboration - Team members can quickly understand what changed and why
  2. Automated versioning - Semantic versioning is automatically determined from our commit messages
  3. Comprehensive changelogs - Generated automatically with minimal effort
  4. Cleaner git history - Making it easier to track down when and why changes were introduced
  5. Streamlined releases - Less manual work and fewer errors in the release process

Getting Started in Your Team

If you're interested in adopting these practices for your team, we recommend starting with:

  1. Standardizing on conventional commits
  2. Setting up branch protection rules to enforce PR reviews
  3. Configuring squash merging as the default merge strategy
  4. Adding the release-please GitHub action to your workflow

The initial adjustment period takes a few weeks, but the long-term benefits to your development workflow are well worth the investment.

Conclusion

A disciplined approach to Git and GitHub workflows might seem like extra work initially, but it has proven invaluable for our team at Necko Technologies. By combining conventional commits, feature branches, squash merging, and automated releases, we've created a development process that scales well with team size and project complexity.

These practices have turned our Git history from a confusing jumble of meaningless commits into a valuable resource that helps us understand our codebase's evolution and automatically drives our release process.

Start Your AWS Project