GitHub Actions accumulates workflow run history quickly. After a few months of active development, a busy repository can have thousands of runs: every push, every pull request, every scheduled job. Most of those runs are no longer useful, but the GitHub web interface only lets you delete one run at a time. If you want to clean up a large backlog, you need a different approach.
This post covers a one-liner that bulk-deletes all workflow runs for a repository using the GitHub CLI (gh) and jq.
Most projects have a moment when someone merges a change to a critical file without the right people ever seeing it. Not because anyone meant to skip the review, but because nothing in the workflow made it obvious who should have been asked. A security-sensitive configuration file, a shared library that dozens of services depend on, a public API contract: all of them can drift in the wrong direction when ownership is implied rather than explicit.
A CODEOWNERS file solves that problem. It maps paths and patterns in your repository to the people and teams responsible for reviewing them. When someone opens a pull request or merge request that touches those paths, the platform automatically requests a review from the designated owners. No manual assignment required, no institutional knowledge needed, and no way to merge without the right sign-off if you enforce it with branch protection.
This post covers what a CODEOWNERS file is, how to write one, and the permission details that quietly break it.
Git doesn't have a rename command for tags. There's no git tag --move or git tag --rename. Tags are immutable references; once created, you can't change the name of one without creating a new one and removing the old one.
That's awkward when a tag has the wrong name. You tagged v2.1.7 as release-2.1.7. The project switched from a release-X.Y.Z convention to vX.Y.Z. A typo slipped through before the push. Whatever the reason, the tag exists in both local and remote repositories, and it needs to be renamed.
When you're building a Terraform provider, the default installation mechanism works against you. Every time you want to test a change, Terraform looks up the provider in a registry. That means you either publish a pre-release to the Terraform Registry on every iteration, configure a private local mirror, or wire up a complex network mirror configuration just to try out a two-line fix. None of those options belong in a tight edit-compile-test loop.
The dev_overrides block in the Terraform CLI configuration file solves this. It tells Terraform to skip the registry entirely for a named provider and load the binary from a local path instead. On macOS and Linux, this file is ~/.terraformrc. On Windows, it's %APPDATA%\terraform.rc.
You open a pull request. The CI checks pass. A reviewer leaves a comment:
"Please squash your commits and rebase onto the latest main."
If you're new to contributing to open source or working on a team with a structured workflow, that request can feel like an obstacle between you and getting your work merged. It isn't. It means the project cares about its history, and it's worth learning why.
This post covers the contributor side: how to rebase, squash, and sign a branch before you open a pull request (or merge request, as GitLab calls it).
The maintainer side (the three GitHub merge strategies and why squash and merge is the right default) lives in Squash and Merge: A Better Default.
Anyone can commit code to a repository pretending to be you. Git's author fields (user.name and user.email) are free-form text that any client can set to anything. Signing a commit ties it to a key that only you hold. Once your commits show a verified badge on GitHub or GitLab, reviewers know the code actually came from you.
This post walks through the full picture: why signing matters, how to configure your git client correctly, how to generate and publish a GPG key, how to use your platform's no-reply address so you never expose your real email, and how to automate Signed-off-by trailers with git hooks, complete with copy-paste examples for every step.
If you browse the commit history of a long-running project, you'll find one of two things. Either a history that reads like a record, something you can actually use to understand why the code is the way it is, or something like this:
fix stuff
wip
update
changes
more fixes
actually fix it this time
I've written commits like that. Most developers have. It happens when you treat the commit message as a formality, when the only audience you're writing for is the CI system that just needs to see something in the message field.
Compare that to this:
feat(datastore): add support for datastore clusters
fix(ssh): prevent IPv6 addresses from being double-wrapped in brackets
refactor(firmware): set firmware configuration during create
chore(deps): bump govmomi from 0.51.0 to 0.52.0
docs: update README with plugin installation instructions
feat(datasource): add virtual machine datasource
Which would you rather read?
The second log follows Conventional Commits, a lightweight specification that gives every commit a predictable, parseable shape. The difference isn't talent or effort. It's convention.
This post explains how Conventional Commits work, why each rule exists, and how I use them in practice. A lot of this builds on Chris Beams's foundational piece, How to Write a Git Commit Message. If you haven't read it, do that first. What follows extends the principles he outlines into the structured format I've settled on.
Every once in a while, someone asks me how this site works. The short answer: it's a static site generated by ProperDocs, themed with MaterialX, deployed to GitHub Pages via GitHub Actions, with Cloudflare Pages serving pull request build previews and Cloudflare handling DNS for the custom domain. No CMS, no database, no runtime server. Every page is a Markdown file in a Git repository. Merges to main trigger the deploy workflow when paths that affect the site change.
This post walks through how all those pieces fit together, with the actual configuration files that run this blog today.