• Skip to main content

JS

Git Hooks for Mortals

July 18, 2022 by Jason Short

Continuing my series of opinionated local development environment implementations, today I will document my git hooks configuration.

The two main contenders are pre-commit and git-hooks. I’ve settled on git-hooks, mostly because I prefer to assemble my own wrappers.

Installation

brew install git-hooks-go

Configuration

Hook scripts should be installed into each workspace that requires hooks:

git-hooks install

Templates can also be set for future clone or init use:

git config --global init.templatedir '<path>'

Where <path> is a copy of the git-hooks scripts. ~/.git-templates is a popular location. Existing repositories (without hooks) can have these template hooks applied via git init.

Hooks

Hooks can exist at several scopes:

  • Local hooks are executed from $REPO/githooks/<hook>
  • Global hooks are executed from ~/.githooks/<hook>

where <hook> is a git action, such as pre-commit

Entrypoints in these paths must be executable. Here’s an example git diff-index whitespace check from my global pre-commit hooks:

$ cat ~/.githooks/pre-commit/whitespace.sh
#!/bin/sh

if git rev-parse --verify HEAD >/dev/null 2>&1
then
  against=HEAD
else
  # Initial commit: diff against an empty tree object
  against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

git diff-index --check --cached $against --

Here’s a similarly convoluted example for wrapping tools that are not git-aware:

$ cat githooks/pre-commit/shellcheck.sh
#!/usr/bin/env bash

set -efu -o pipefail

if git rev-parse --verify HEAD &> /dev/null
then
  against=HEAD
else
  against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

( git diff-index --name-only $against | grep '\.sh$' || true ) | while read -r file ; do
  if [[ -f "${file}" ]] ; then
    shellcheck "${file}"
  fi
done

Or something simpler, like golangci-lint which is significantly more context-aware out of the box than shellcheck:

$ cat githooks/pre-commit/go
#!/bin/sh
golangci-lint run

In both of the above cases, the paths referenced are symlinks to a common hook storage path, rather than copies.

Context-specific K8s cluster and namespace

May 24, 2022 by Jason Short

In the last post, I shared my context-specific direnv+asdf config for managing interpreters and tools. Since writing that, I attempted to add kubernetes contexts to the mix, and found only dubious advice from other folks advocating wrapper scripts and other oddities.

It’s poorly documented and somewhat counterintuitive, but kubectl supports multiple configuration files in the KUBECONFIG environment variable. Unlike most other unix-like applications however, it treats those configuration files almost exactly backwards – the first definition wins. But it’s enough to get the job done.

First, we create an overlay configuration file, with the minimum amount duplicated from ~/.kube/config.

Here’s the contents of ~/.kube/overlay/project-cluster-namespace. Name and path are completely arbitrary, but I do like to be organized.

contexts:
- context:
    cluster: gke_cluster_name
    namespace: my-application-namespace
    user: gke_authentication_bits
  name: my-favorite-context
current-context: my-favorite-context

From there, one need only add a KUBECONFIG environment variable to .envrc as follows:

export KUBECONFIG="${HOME}/.kube/overlay/project-cluster-namespace:${HOME}/.kube/config"

Entering the directory with direnv hooks active will yield the following:

direnv: loading ~/projects/my-application
direnv: export +KUBECONFIG

The result is a context-specific cluster and namespace without having to deal with clunky kubectl wrapper scripts or extra files in the workspace, and no side effects for other sessions.

Context-specific tool management with direnv

May 4, 2022 by Jason Short

I’ve tried juggling virtualenvs by hand. Who has the time to manually activate and deactivate when jumping between projects?

I’ve tried language-specific version managers. Sometimes they work. Sometimes you have to wait double-digit seconds for re-shimming.

I’ve tried using tool-specific wrappers like tfenv. The cognitive burden increases exponentially with each one added to the pile.

But none of them solve the entire problem. Maybe this project needs a specific version of Python. Maybe that one is stuck on a weird version of Terraform. Yet another is using a deprecated Helm version, but the package manager only has the latest patch in that series.

Direnv does a lot out of the box, but its strength is configuring projects to use an available version of a tool, it does not provide those versions itself. Some languages have competent version manager tools that are worth using, but operational tools like terraform and kubectl generally lack version managers.

That’s where asdf comes in. Not only is it a joy to type, when combined with direnv it’s actually fast. Putting tool paths into a direnv include means no more waiting for slow re-shimming, running commands manually, or re-hashing your shell cache.

It’s as simple as:

brew install direnv asdf
direnv hook
asdf plugin add direnv
asdf direnv setup --version system

You can then add use asdf to your .envrc file, and the plugin will modify PATH based on .tool-versions, e.g.:

$ echo "use asdf" >> project/.envrc
$ direnv allow project
$ cat << EOF > project/.tool-versions
helm 2.16.3
kubectl 1.20.15
terraform 0.14.11
EOF

Now, not only is the python virtualenv automatically activated and deactivated, so are those tools:

> $ cd project
direnv: loading ~/project/.envrc
direnv: using asdf
direnv: loading ~/.cache/asdf-direnv/env/2471675625-973451084-2702573808-1004245725
direnv: using asdf terraform 0.14.11
direnv: using asdf kubectl 1.20.15
direnv: using asdf helm 2.16.3
direnv: export ~PATH

But what about virtual environments?

asdf is great for managing tool versions, but some tools have baggage. Like python virtual environments. For that, we’ll need something a bit more specialized: pyenv.

$ brew install pyenv
$ echo _PYENV_DIR="$(pyenv prefix)" >> ~/.zshrc
$ source ~/.zshrc

direnv “layout” mode places files (such as virtualenvs) into the ~/.direnv folder within the project:

$ echo "layout pyenv 3.10.10" >> project/.envrc
$ direnv allow project
$ cd project
direnv: loading ~/project/.envrc
direnv: export +PYENV_VERSION +VIRTUAL_ENV ~PATH

$ which python
/redacted/project/.direnv/python-3.10.10/bin/python

Python packages can be managed normally from here via pip. Similar plugins exist for node, ruby, etc, and can be found in the direnv stdlib man page.