Tools and Workflow for Modern Development

Tools and Workflow for Modern Development

9 minutes

This article explores the comprehensive toolchain that powers modern development workflows, focusing on automation, code quality, and streamlined releases. These tools form an integrated ecosystem that handles everything from initial project setup to production deployment and ongoing maintenance.

Across most of my projects, this standardized approach ensures consistency, reduces manual overhead, and maintains high code quality standards. The workflow has been refined through practical experience with various project types, from small utilities to complex monorepos.

Project Setup & Package Management

Bun

Bun serves as the primary package manager for JavaScript and TypeScript projects, offering superior performance and efficient dependency handling compared to traditional alternatives.

It is recommended to use Bun for all JavaScript/TypeScript projects due to its speed and efficiency. The note about keeping the bun.lock file in version control ensures consistent dependency versions across different environments. Bun is incredibly fast and can significantly reduce install times compared to npm or yarn.

TurboRepo

TurboRepo manages monorepos effectively, enabling multiple packages to be developed, tested, and deployed together while maintaining clear boundaries and shared configurations.

Monorepos are not always necessary, especially for small projects. However, I prefer to start with a monorepo structure even for single-package projects to allow for easy expansion in the future without major restructuring.

Here is an example of a basic TurboRepo configuration in a turbo.json file:

{
  "$schema": "https://turborepo.com/schema.json",
  "tasks": {
    "lint": {},
    "test": {},
    "dev": { "cache": false, "persistent": true },
    "update": { "cache": false }
  }
}

Makefile

Makefile provides a unified interface for project tasks including building, testing, and releasing. This approach creates consistency across different project types and simplifies onboarding for new team members.

There a lot of commands you can add to the Makefile, but here are the most common ones I usually include:

.PHONY: install lint test build update next
SHELL := /bin/bash
DEFAULT_GOAL := build

install:
	bun install

lint: install
	bunx eslint . --ext .ts,.json,.md --fix
	bunx prettier --write "**/*.md" "**/*.json" "**/*.ts" --log-level warn
	bunx tsc --noEmit

test: lint
	CI=CI bunx vitest --coverage

build: test
	rm -Rf dist
	bunx tsc --build

update:
	bunx npm-check-updates -u
	bun install

next:
	git pull origin main
	git checkout main
	git merge next
	git push -u origin main

Code Quality & Consistency

Prettier

Prettier handles automatic code formatting across all files, ensuring visual consistency regardless of individual developer preferences or editor configurations.

It’s a recommended practice to integrate Prettier with your code editor to enable automatic formatting on file save. This helps maintain a consistent code style throughout the development process.

The Prettier configuration is stored in a .prettierrc file at the root of the repository. Here is an example configuration:

{
  "$schema": "https://json.schemastore.org/prettierrc",
  "semi": false,
  "singleQuote": true,
  "printWidth": 140,
  "overrides": [{ "files": ".*rc", "options": { "parser": "json" } }]
}

ESLint

ESLint with specialized plugins for JSON and Markdown maintains code quality standards while catching potential issues during development rather than in production.

Here is an example of an ESLint configuration in a .eslintrc.config.mts file:

// @ts-check
import eslint from '@eslint/js'
import prettierPluginRecommended from 'eslint-plugin-prettier/recommended'
import * as tsPlugin from '@typescript-eslint/eslint-plugin'
import tsParser from '@typescript-eslint/parser'
import markdownPlugin from 'eslint-plugin-markdown'
import jsoncPlugin from 'eslint-plugin-jsonc'

export default [
  // Ignore built files and dependencies
  { ignores: ['dist/**', 'coverage/**', 'node_modules/**'] },

  // Prettier plugin configuration
  prettierPluginRecommended,

  // Base recommended configuration
  eslint.configs.recommended,

  // TypeScript ESLint plugin configuration
  {
    files: ['**/*.ts'],
    plugins: { '@typescript-eslint': tsPlugin },
    // merge rules from both presets (fall back to empty objects)
    rules: {
      ...(tsPlugin.configs.strict && tsPlugin.configs.strict.rules ? tsPlugin.configs.strict.rules : {}),
      ...(tsPlugin.configs.stylistic && tsPlugin.configs.stylistic.rules ? tsPlugin.configs.stylistic.rules : {}),
    },
    languageOptions: {
      parser: tsParser,
      ecmaVersion: 2022,
      sourceType: 'module',
      globals: {
        process: 'readonly',
        global: 'readonly',
        __dirname: 'readonly',
        Buffer: 'readonly',
        structuredClone: 'readonly',
      },
    },
  },

  // Markdown files: run ESLint over fenced code blocks
  {
    files: ['**/*.md', '**/*.md/*'],
    plugins: { markdown: markdownPlugin },
    processor: 'markdown/markdown',
  },

  // JSONC plugin - lint JSON files
  ...jsoncPlugin.configs['flat/recommended-with-jsonc'],
]

Husky

Husky manages Git hooks to enforce quality gates, ensuring that commit messages follow established conventions and code passes basic checks before being committed.

Husky is configured to run Commitlint on commit-msg hook to validate commit messages before they are created. This prevents invalid commit messages from being added to the repository.

Commitlint

Commitlint automatically validates commit message format, supporting the semantic release process by enforcing conventional commit standards.

Here is an example of a Commitlint configuration in a .commitlintrc file:

{
  "extends": ["@commitlint/config-conventional"],
  "rules": {
    "type-enum": [2, "always", ["feat", "fix", "docs", "style", "refactor", "perf", "test", "chore", "revert", "build", "ci"]],
    "type-case": [2, "always", "lower-case"],
    "type-empty": [2, "never"],
    "scope-case": [2, "always", "lower-case"],
    "subject-case": [2, "never", ["sentence-case", "start-case", "pascal-case", "upper-case"]],
    "subject-empty": [2, "never"],
    "subject-full-stop": [2, "never", "."],
    "header-max-length": [2, "always", 140],
    "body-leading-blank": [1, "always"],
    "body-max-line-length": [2, "always", 140],
    "footer-leading-blank": [1, "always"],
    "footer-max-line-length": [2, "always", 140]
  }
}

Git Workflow & Branch Management

GitHub Flow

The GitHub Flow provides a straightforward, branch-based workflow that balances simplicity with robust change management practices.

Nothing to add here, just read the official documentation if you are not familiar with it.

Commit Message Conventions

Conventional commits structure commit messages in a standardized format, enabling automated tooling to understand changes and make intelligent decisions about version bumps and release notes.

Next Branch

A dedicated next branch serves as a staging environment for integration testing before changes reach the main branch, allowing for final validation in a production-like environment.

There are nothing special about the next branch. It is just a regular branch that I use for integration testing before merging into main. You can name it differently or even skip it if your project is simple enough.

Usually, I create the next branch from main and when a few features or fixes are tested and verified in next, I open a pull request to merge next into main. This way, main always has stable and production-ready code.

Branch Protection Rules

Branch protection rules in GitHub ensure all changes undergo proper review and testing before integration, maintaining code quality and reducing the risk of introducing bugs.

There are many options you can enable in the branch protection rules, but at the very least, I usually require:

  • Restrict deletions
  • Require linear history
  • Require signed commits
  • Require a pull request before merging
  • Require status checks to pass and add Codacy Static Code Analysis as status check that is required
  • Block force pushes
  • Require code scanning results and add CodeQL as status check that is required

CodeQL requires additional setup in the repository’s Advanced Security settings, otherwise it won’t work. So don’t forget to configure it.

In the general settings of the repository, I also enable the following options:

  • Enable release immutability
  • Allow rebase merging and disable squash merging and merge commits
  • Automatically delete head branches
  • Auto-close issues with merged linked pull requests

Of course you can adjust these settings based on your team’s workflow and preferences.

Testing & Quality Assurance

Unit Tests & Coverage Reports

Coverage reports provide visibility into test comprehensiveness, helping identify areas that need additional testing attention.

You are free to use any testing framework you prefer, such as Jest or Mocha, but I often use vitest for its speed and simplicity, especially in projects using Bun.

After running tests, I generate coverage reports in lcov format using vitest and upload them to Codacy for analysis.

I hold test files in the separate tests directory at the root of the repository and usually ignore them in the Codacy analysis to focus on production code quality.

Here is my usual vitest configuration in the vite.config.ts file:

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    exclude: ['node_modules', 'dist', 'example', 'scripts'],
    coverage: {
      reporter: ['lcovonly', 'text'],
      include: ['src/**/*.ts'],
      exclude: ['**/types.ts'],
      all: true,
    },
  },
})

Codacy Static Code Analysis

Codacy offers continuous code quality and security analysis, providing automated feedback on code health and potential vulnerabilities.

So you are welcome to open a free account on Codacy and connect your GitHub repository to get automated code reviews and maintain high standards.

Here is an example of a Codacy configuration in a .codacy.yml file:

engines:
  duplication:
    enabled: true
    exclude_paths:
      - 'tests/**'
  metric:
    enabled: true
    exclude_paths:
      - 'tests/**'

Test Github Actions

GitHub Actions test workflows execute automatically on pull requests and branch pushes, running the complete test suite defined in the project’s Makefile to catch issues early.

Here is an example of a GitHub Actions workflow for tests defined in .github/workflows/test.yml:

name: Test

on:
  push:
    branches: [next]
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Validate commit messages
        uses: wagoid/commitlint-github-action@v6
        with:
          configFile: .commitlintrc

      - uses: actions/setup-node@v5
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: 'bun.lock'
          registry-url: 'https://registry.npmjs.org/'

      - uses: oven-sh/[email protected]

      - run: make test

      - uses: codacy/[email protected]
        with:
          project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
          coverage-reports: coverage/lcov.info

Release Automation & Deployment

Semantic Release

Semantic Release automates the entire release process by analyzing commit messages to determine appropriate version bumps (major, minor, patch) and generating comprehensive release notes automatically.

I don’t hold the CHANGELOG.md file in the repository. Instead, I rely on the release notes generated by semantic-release and published. Here is an example of Semantic Release configuration in a .releaserc file:

{
  "branches": ["main"],
  "plugins": [
    ["@semantic-release/commit-analyzer", { "preset": "conventionalcommits" }],
    ["@semantic-release/release-notes-generator", { "preset": "conventionalcommits" }],
    "@semantic-release/git",
    "@semantic-release/github",
    "@semantic-release/npm"
  ]
}

Release Github Actions

GitHub Actions release workflows trigger on main branch updates, executing the semantic-release process to publish new versions without manual intervention.

Here’s an example of a GitHub Actions workflow for releases defined in .github/workflows/release.yml:

name: Release

on:
  push:
    branches: [main]

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      issues: write
      pull-requests: write
      id-token: write
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v5
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: 'bun.lock'
          registry-url: 'https://registry.npmjs.org/'

      - uses: oven-sh/[email protected]

      - run: make build

      - run: bunx semantic-release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

      - uses: codacy/[email protected]
        with:
          project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
          coverage-reports: coverage/lcov.info

Maintenance & Dependencies

Dependabot

Dependabot maintains project health by automatically creating pull requests for dependency updates, ensuring projects stay current with security patches and feature improvements.

Here’s an example configuration for Dependabot in a .github/dependabot.yml file:

version: 2
updates:
  # Enable version updates for npm
  - package-ecosystem: 'npm'
    directory: '/'
    schedule:
      interval: 'weekly'
    open-pull-requests-limit: 5

  # Enable version updates for GitHub Actions
  - package-ecosystem: 'github-actions'
    directory: '/'
    schedule:
      interval: 'weekly'
    open-pull-requests-limit: 5

Conclusion

This toolchain provides a robust framework for managing modern software development projects, emphasizing automation, code quality, and streamlined workflows. By adopting these tools and practices, teams can enhance productivity, reduce errors, and maintain high standards throughout the development lifecycle.

Feel free to adapt and customize this setup to fit your specific project needs and team preferences.