← Back to list

Beyond Git: Engineering a Complete Repository Lifecycle with Bash

How commit_gh evolved from a release helper into a secure repository lifecycle manager.

Raymon Epping · 2026-07-29 15:24 · 0 claps · 20.7 min read
#bash #bash-script #github #github-actions #gitleaks
Open on Medium ↗
Wiki topics: 🔓 · Open Source

Beyond Git: Engineering a Complete Repository Lifecycle with Bash

How commit_gh evolved from a release helper into a secure repository lifecycle manager.

*Programs are meant to be read by humans and only incidentally for computers to execute. — *Harold Abelson

This article is a technical follow-up to my previous article, “How One Small Mistake Changed the Way I Build Every GitHub Repository.” In that article I explained why commit_gh exists. This article explores how it evolved into a complete repository lifecycle manager.

There is something strangely satisfying about creating a brand-new Git repository.

  • An empty folder.
  • A fresh idea.
  • Unlimited optimism.
  • No technical debt.
  • No legacy code.

No mysterious function called doStuff2() that everyone is afraid to touch because "it's been working since 2018."

For a brief moment, life is beautiful.

Then reality shows up.

You initialize Git.

Create a README.

Pick a license.

Write a .gitignore.

Configure GitHub.

Enable branch protection.

Install Gitleaks.

Create release workflows.

Enable Dependabot.

Create issue templates.

Configure CODEOWNERS.

Write a CONTRIBUTING.md.

Configure Secret Scanning.

Configure Push Protection.

Wonder whether you forgot something.

Check another repository.

Copy a few files.

Wonder again.

Eventually start writing code.

Looking back, I found it amusing that the actual software was often the easiest part.

Everything around the software was where the repetition lived.

Every Repository Starts the Same

And yet we somehow keep rebuilding them from scratch

That realization stayed with me for a surprisingly long time.

As engineers, we spend enormous amounts of effort automating infrastructure.

We write Terraform modules so nobody has to provision cloud resources by hand.

We build golden virtual machine images so operating systems start from a known baseline.

We create Kubernetes operators because manually managing hundreds of deployments sounds about as enjoyable as debugging a production issue at 2 AM with one bar of Wi-Fi.

(Yes, oddly specific. Don’t ask.)

Platform engineering exists almost entirely because we’ve collectively agreed that repetitive operational work should become software.

Yet when it comes to Git repositories…

Many of us still happily repeat exactly the same checklist every single time.

That always felt… inconsistent.

Somewhere Along the Way, Git Became the Easy Part

When I first wrote commit_gh, none of this was the plan.

In fact, the very first version had a wonderfully unambitious goal.

I was simply tired of typing the same Git commands every time I released one of my command-line tools.

Commit.

Tag.

Push.

Create a GitHub release.

Update the version file.

Hope I hadn’t forgotten anything.

Repeat.

It wasn’t difficult.

It was repetitive.

And repetition has an interesting property.

The first few times you perform a task, you’re fully engaged.

By the fiftieth time, you’re mostly running on muscle memory.

By the hundredth time, your attention has wandered off to think about lunch while your fingers continue typing.

That’s usually when mistakes begin to creep in.

Some are harmless.

Some are embarrassing.

Some become Medium articles.

So I did what many engineers eventually do after experiencing sufficient amounts of repetition.

I automated it.

Not because typing Git commands is particularly hard.

Because thinking about Git commands was becoming an unnecessary interruption.

There’s a subtle difference.

It was never about saving thirty seconds. It was about never having to think about those thirty seconds again.

The Problem Was Never Git

The more commit_gh evolved, the more I noticed something curious.

Every feature I added solved a problem.

But almost immediately, it exposed another one.

Automating releases was nice.

Until I realized repositories still needed to be initialized.

Repository initialization was nice.

Until I realized they weren’t secure by default.

Security was nice.

Until I realized governance was inconsistent.

Governance was nice.

Until I realized releases weren’t standardized.

Releases were nice.

Until I realized older repositories slowly drifted away from newer ones.

It started feeling less like building features and more like peeling an onion.

Every layer revealed another one underneath.

Thankfully, unlike onions, this project caused significantly fewer tears.

(Although Git merge conflicts occasionally came close.)

Somewhere during that process I stopped asking:

“What should commit_gh do next?"

Instead I started asking:

“What does a well-engineered repository actually look like?”

That turned out to be a much better question.

Thinking Like a Platform Engineer

Working in platform engineering changes the way you look at systems.

You stop asking how to solve today’s problem.

You start asking how to solve tomorrow’s problem before it arrives.

Good platforms aren’t collections of clever features.

They’re collections of sensible defaults.

The easiest path should also be the safest path.

The fastest path should also be the most repeatable path.

The happy path should require the least amount of thinking.

That philosophy gradually found its way into commit_gh.

Without realizing it, I had stopped building a release helper.

I had started building what I can only describe as a repository lifecycle manager.

Not because I set out to invent a new category of tooling.

Because every engineering decision naturally led to the next one.

Every Repository Has a Lifecycle

One afternoon I sketched the entire project on a whiteboard.

Not the code.

The responsibilities.

That’s when something clicked.

Every repository, regardless of programming language or project size, follows roughly the same lifecycle.

It starts life as an empty directory.

Then comes initialization.

Then security.

Then governance.

Then daily development.

Then releases.

Eventually maintenance.

Finally, history.

Looking back at the whiteboard, I realized something almost embarrassingly obvious.

commit_gh already supported every one of those stages.

I simply hadn’t been thinking about it that way.

Suddenly the help screen made sense.

It wasn’t just a list of command-line options anymore.

It was an architecture diagram.

Empty Folder
       │
       ▼
Repository Initialization
       │
       ▼
Security
       │
       ▼
Governance
       │
       ▼
Daily Development
       │
       ▼
Release Engineering
       │
       ▼
Maintenance
       │
       ▼
History

Once I saw it, I couldn’t unsee it.

Every command belonged somewhere.

Nothing felt bolted on anymore.

The project had evolved from “a useful Bash script” into something much more structured.

Not because I planned it.

Because engineering has a funny habit of rewarding consistency.

Why Bash?

This is probably the question I’m asked most.

Usually followed by a look that says,

“Surely you’d have written this in Go today?”

Maybe.

Go is fantastic.

Rust is fantastic.

Python is fantastic.

I enjoy all of them.

But none of them would necessarily have been the better choice for this particular problem.

commit_gh doesn't need to render graphics.

It doesn’t need to implement distributed consensus.

Thankfully, nobody has ever asked it to become a Kubernetes scheduler. I’d politely decline anyway.

What it does need to do is orchestrate tools that already exist.

Git.

GitHub CLI.

Gitleaks.

Homebrew.

SSH.

GPG.

Those tools already speak the language of the shell.

Bash isn’t the language I’d choose to build a database.

It is the language I’d choose to orchestrate developer tooling.

It’s already installed on almost every Unix-like machine.

It’s transparent.

It’s inspectable.

If something goes wrong, you can literally open the file in your editor and read what it’s doing.

There’s no magic runtime.

No hidden daemon.

No framework quietly making decisions on your behalf.

Just a script.

A rather opinionated one, admittedly.

But still just a script.

And perhaps that’s what I like most about it.

Good automation shouldn’t hide complexity.

It should remove unnecessary repetition while remaining completely understandable.

After all, the best engineering tools aren’t the ones that make you feel clever.

They’re the ones that disappear into your workflow until one day you realize you can’t quite remember how you managed without them.

And for me, that’s where the real story of commit_gh begins.

Because initializing a repository turned out to be the easy part.

Making sure every repository starts life secure, repeatable, and maintainable… that’s where things became genuinely interesting.

Repository Initialization Should Be Boring

One of the things I appreciate most about good infrastructure tooling is that it’s almost… uneventful.

You don’t celebrate when Terraform successfully creates the same VPC for the hundredth time.

You don’t tweet about Ansible correctly configuring another server.

You certainly don’t call your colleagues over because Kubernetes scheduled a pod exactly where it was supposed to.

That’s the point.

Good infrastructure is boring.

Predictable.

Deterministic.

And I slowly came to the conclusion that repository creation should feel exactly the same.

When I type:

commit_gh --init-repo --init-remote --public --secret-scanning

I don’t want surprises.

I don’t want questions.

I don’t want a wizard asking me seventeen things I answered in the previous repository.

I simply want a repository that starts life with the engineering standards I’ve chosen for myself.

Not because they’re mandatory.

Because they’re repeatable.

A README Before a Single Line of Code

One thing I’ve always found slightly amusing is that many repositories spend months accumulating code before someone finally says,

“We should probably write a README…”

At that point the documentation becomes archaeology.

People are no longer describing what they intended to build.

They’re trying to remember what they already built.

That’s a much harder problem.

So commit_gh does something almost embarrassingly simple.

It creates the README first.

Not because the generated README is particularly exciting.

It is not.

It is intentionally minimal.

Its job is not to document your application. It exists to remove the excuse that documentation can wait until later.

The same applies to the other files.

A LICENSE.

A CHANGELOG.

A CONTRIBUTING.md.

Issue templates.

A pull request template.

A release checklist.

A CODEOWNERS file.

An .editorconfig.

Individually, none of those files are particularly revolutionary.

Collectively, they establish something much more valuable.

Intent.

Before writing a single line of production code, the repository already answers a few important questions.

Who owns this?

How should changes be made?

How are releases managed?

Where does documentation belong?

What standards are we following?

That’s not bureaucracy.

That’s reducing uncertainty.

Repositories Drift. Standards Shouldn’t.

Another realization came from looking at older projects.

Like many engineers, I have repositories that are… let’s call them “historically accurate.”

You know the kind.

One still uses Travis CI.

Another has a README that proudly says “Coming soon.”

A third contains a .gitignore that somehow predates several operating systems.

They’re little time capsules of whoever I happened to be as an engineer at the time.

Nothing is technically wrong with them.

They simply reflect a different set of standards.

The problem is that standards evolve.

A repository I created three years ago isn’t the repository I would create today.

Neither am I the engineer I was three years ago.

That observation led to one of my favourite commands.

commit_gh --harden

The name is quite deliberate.

It doesn’t “upgrade.”

It doesn’t “convert.”

It doesn’t “regenerate.”

It hardens.

The distinction matters.

Infrastructure Taught Me About Idempotency

If you’ve worked with Terraform, Ansible or any other infrastructure-as-code tooling, you’ve probably encountered the word idempotent.

It’s one of those words that sounds far more complicated than it really is.

In practice it simply means this:

Run the same operation once.

Run it again tomorrow.

Run it another hundred times next year.

The result should be exactly the same.

That’s the philosophy behind --harden.

If the repository already contains a README.md, leave it alone.

If the release workflow already exists, don’t replace it.

If the issue templates are already there, move on.

If Gitleaks has already been configured, excellent.

Nothing to do.

Only the missing pieces get added.

That makes the command remarkably boring.

Which, in engineering, is usually a compliment.

I can point it at a repository I’ve maintained for five years without wondering whether it’s about to rewrite half the project.

It checks what is already there, adds only what’s missing, and leaves everything else alone.

Good automation should have the confidence to know when not to do something.

Security Isn’t a Feature

One thing I deliberately avoided was treating security as an optional add-on.

You’ve probably seen tools where security feels like an afterthought.

There’s a checkbox somewhere.

Maybe a plugin.

Maybe a separate installation guide.

Maybe something you’ll “come back to later.”

We all know how that story usually ends.

Later rarely arrives.

So I decided to invert the question.

Instead of asking,

“How do I add security?”

I asked,

“Why would a repository ever start without it?”

That completely changed the design.

Security stopped becoming a feature.

It became part of initialization.

Every repository now starts with sensible defaults.

Not maximum security.

Not enterprise compliance.

Just practical guardrails that make common mistakes harder to make.

Three Layers, Three Different Problems

When people hear “Gitleaks,” they often assume that’s the entire security story.

It isn’t.

Gitleaks is one layer.

An important layer.

But still just one layer.

The first line of defence is intentionally almost primitive.

Filename validation.

If I accidentally try to commit something called:

.env
id_rsa
production.pem
.vault.production

I don’t need entropy analysis.

I don’t need machine learning.

I don’t need a sophisticated scanner.

The filename already tells me almost everything I need to know.

Those files deserve a second look before they ever become part of a commit.

Fast.

Simple.

Deterministic.

The second layer is where Gitleaks takes over.

Because not every secret lives in an obvious file.

Some of the most interesting leaks hide inside perfectly innocent-looking documents.

Terraform variables.

YAML files.

JSON configuration.

Shell scripts.

Even source code.

Now the filename is useless.

Only the content matters.

That’s exactly what Gitleaks is exceptionally good at.

Finally comes GitHub itself.

Secret Scanning.

Push Protection.

The last safety net.

Not because I expect the first two layers to fail.

Because good engineering assumes that, eventually, something somewhere probably will.

Three layers.

Three different failure modes.

Three opportunities to catch one very human mistake.

That’s not paranoia.

That’s defence in depth.

Infrastructure engineers have been designing systems that way for years.

I simply borrowed the idea for Git repositories.

Automation Should Inform, Not Decide

One thing I felt strongly about while building all of this was resisting the temptation to make the tool “too smart.”

There’s a fine line between helpful automation and annoying automation.

I never wanted commit_gh silently deciding what belongs in .gitignore.

Or automatically suppressing security findings because they looked inconvenient.

That’s not automation.

That’s gambling.

Instead, the workflow is intentionally simple.

If Gitleaks finds something suspicious, it tells me.

Exactly what.

Exactly where.

Exactly why.

From there, I make the decision.

Did I accidentally commit a secret?

Great.

I’ll remove it.

Should that file never have been tracked?

I’ll update .gitignore.

Did I hit a genuine false positive?

I’ll teach Gitleaks by updating .gitleaks.toml.

That last part turned out to be surprisingly important.

Early on, one of my own projects triggered a false positive on a perfectly harmless string.

I could have weakened the scanner.

Instead, I taught it the exception.

That tiny decision reflects something much larger.

Security tools shouldn’t become quieter.

They should become more accurate.

There’s an important difference.

Automation should remove repetitive work.

Engineering judgement should remain exactly where it belongs.

With the engineer.

A Repository Eventually Has to Ship Something

By this point, commit_gh had become much more than a repository bootstrapper. It could initialize repositories, secure them, and bring older projects back towards the same engineering baseline.

But repositories do not exist simply to be created and admired.

Eventually, they have to evolve. They have to ship software, communicate what changed, and preserve enough history that somebody can understand the decisions behind a release months later.

That is where release engineering enters the lifecycle.

And, as I discovered, release engineering turned out to be every bit as interesting as repository engineering itself.

Release Engineering Is More Than Creating Tags

If there’s one area where my opinion has changed the most over the past few years, it’s release engineering.

I used to think a release was simply a tag.

Create it.

Push it.

Move on.

Technically, that’s true.

Practically, it’s only a small part of the story.

A release is really a conversation between the people who built the software and the people who are about to trust it.

It answers questions like:

What changed?

Can I upgrade safely?

Who contributed?

What version am I actually running?

Where do I find the binaries?

How do I roll back if something goes wrong?

Those aren’t Git questions.

They’re engineering questions.

That’s why the release workflow inside commit_gh gradually became much more opinionated than I originally intended.

Shipping Software Should Feel Deliberate

Today, releasing a new version looks almost boring.

That’s intentional.

commit_gh --release patch

Behind that single command, a surprising amount happens.

The VERSION file is updated.

The internal script version is synchronized.

A commit is created.

A Git tag is generated.

The branch and tag are pushed.

A GitHub Release is created.

Optionally, release notes are generated directly from merged pull requests.

None of those steps are particularly difficult on their own.

The challenge is remembering to perform all of them, in the right order, every single time.

Humans are wonderfully creative.

We’re considerably less reliable as state machines.

Order Matters More Than Most People Realize

One of the more interesting lessons I learned while building the release workflow is that sequencing matters.

A lot.

Suppose you create a Git tag before updating the version file.

Now the repository says one thing.

The release says another.

The binary might say something else entirely.

None of those mistakes are catastrophic.

They’re simply annoying.

The kind of annoying that wastes half an hour while everyone asks:

“Wait… which version are we actually looking at?”

So the release pipeline became intentionally strict.

Version first.

Commit second.

Tag third.

Push fourth.

Release last.

Each step validates the previous one before continuing.

It sounds almost trivial.

Until you’ve experienced the opposite.

The Repository Becomes the Source of Truth

One subtle design decision that gradually emerged is that I stopped treating Git tags as the only source of version information.

Instead, the repository itself became the authority.

That’s why commit_gh can inspect multiple version sources.

commit_gh --read-version-only

Instead of blindly trusting one place, it compares the repository’s understanding of the current version.

The VERSION file.

The script.

The latest Git tag.

If those disagree, something deserves attention.

Again, the goal isn’t to stop the release.

The goal is to prevent ambiguity.

Because ambiguity has a remarkable talent for appearing at exactly the wrong moment.

Usually five minutes before a demo.

Sometimes the Best Feature Is Undo

Most articles about release tooling spend all their time talking about shipping software.

Almost none talk about unshipping it.

Reality is less optimistic.

Sometimes a release shouldn’t exist.

Wrong binary.

Wrong version.

Wrong notes.

Wrong day.

That’s why rollback became part of the lifecycle instead of an afterthought.

commit_gh --rollback v2.3.0

Notice something.

Rollback isn’t a Git command.

It’s an engineering command.

It’s acknowledging that humans occasionally press the wrong button.

Designing only for success is easy.

Designing for recovery is where systems become resilient.

Governance Isn’t Bureaucracy

The word governance has a habit of making developers nervous.

It sounds like meetings.

Spreadsheets.

Someone saying, “We should probably establish a steering committee.”

That’s not what I mean.

To me, governance simply answers one question.

Can multiple people work safely in this repository?

That question led to several capabilities that, at first glance, don’t appear related.

Branch protection.

Commit signing.

Issue labels.

Pull request creation.

Milestones.

Contributors.

They all exist for the same reason.

Not because Git requires them.

Because collaboration does.

Take branch protection, for example.

commit_gh --protect

There’s nothing particularly exciting about requiring a review before merging into main.

Until the day somebody accidentally pushes directly to it.

Good governance isn’t about assuming people are careless.

It’s about recognizing that everyone has an off day eventually.

Including me.

Engineering Is a Team Sport

Another feature that quietly grew out of real-world usage was automatic pull request creation.

commit_gh --pr

On the surface, it’s just another convenience.

In reality, it’s encouraging a workflow.

Feature branch.

Commit.

Push.

Open a pull request.

Review.

Merge.

Repeat.

Notice how none of those steps mention code.

They’re about collaboration.

Repositories are where software lives.

Pull requests are where engineering happens.

Those are two different things.

Before You Release, Ask Better Questions

One of my favourite additions isn’t actually related to releasing software at all.

It’s checking whether I’m ready to release software.

commit_gh --doctor

Think of it as a quick health check.

Is Git available?

Is Gitleaks installed?

Can GitHub CLI authenticate?

Is SSH working?

Is everything in the environment ready?

I’ve found that solving problems before they interrupt my workflow is significantly more enjoyable than solving them halfway through a release.

Closely related is another command I use surprisingly often.

commit_gh --verify

Not because I don’t trust the repository.

Because I like confirmation.

Verification isn’t pessimism.

It’s confidence with evidence.

The Quiet Satisfaction of --history

Every now and then I find myself wondering when a feature was introduced or how a project has evolved over time.

Git can answer those questions.

Eventually.

After enough log commands, filters, and coffee.

Or…

commit_gh --history

Sometimes the simplest quality-of-life improvements end up becoming the ones you miss the most when they’re gone.

History isn’t just about nostalgia.

It’s about understanding the evolution of a project.

The same applies to contributors, milestones, generated changelogs, and machine-readable history that can be consumed by scripts or other tooling.

None of those commands exist because they were difficult to implement.

They exist because they remove tiny pockets of friction that accumulate over hundreds of releases.

And that’s a recurring theme you’ll probably notice throughout commit_gh.

Very few features save hours.

Most save seconds.

But those seconds occur every single day.

Eventually they compound into something much larger than the sum of their parts.

The Day It Finally Felt Like a Real Unix Tool

There’s one milestone I don’t think anyone else will ever celebrate quite as much as I did.

It wasn’t a new feature.

It wasn’t another release.

It wasn’t even crossing a particular version number.

It was typing this.

man commit_gh

And seeing an actual manual page.

That probably sounds slightly ridiculous.

It certainly did when I explained it to a colleague.

But Unix has always had a tradition of tools documenting themselves.

Some of the utilities we still use every day haven’t fundamentally changed in decades.

Not because they stopped evolving.

Because they reached a point where the interface felt complete.

I’m certainly not suggesting commit_gh belongs in the same category.

Not even remotely.

But writing the manual somehow made the project feel… grown up.

Like it had quietly crossed the line from “weekend script” to “something I’d happily install on a new machine.”

Oddly enough, that’s the moment I stopped thinking about what feature to build next.

Instead, I started asking a much more difficult question.

Has the architecture finally become complete?

Looking at the Help Screen Differently

Something funny happened while I was preparing this article.

I opened a terminal, typed:

commit_gh --help

…and for the first time in quite a while, I actually read the help screen instead of skimming it.

Not because I’d forgotten how the tool worked.

Because I wanted to sanity-check the documentation before publishing.

Instead, I noticed something I hadn’t consciously designed.

The help output wasn’t organised alphabetically.

It wasn’t grouped by implementation.

It wasn’t grouped by source code.

It was grouped by lifecycle.

Setup
Security
Release
Commit
Utilities

At first I thought that was simply good organisation.

Then it hit me.

The help screen had become an architecture diagram.

Every section represented another stage in the life of a repository.

Initialization.

Protection.

Development.

Releases.

Maintenance.

History.

Nothing felt random anymore.

Every command had a home.

That was probably the first moment I genuinely believed the project had reached architectural maturity.

When Features Stop Feeling Like Features

Every engineer has experienced feature creep.

You add one thing.

Then another.

Then another.

Before long you’re afraid to open your own source code because you’re no longer entirely sure how everything fits together.

That’s usually the moment software starts becoming difficult to maintain.

The interesting thing about commit_gh is that the opposite happened.

As more functionality was added, the project actually became easier to reason about.

Not because there were fewer commands.

Because the commands naturally grouped themselves into responsibilities.

Initialization.

Security.

Governance.

Development.

Release engineering.

Maintenance.

Observability.

History.

Those aren’t features.

They’re domains.

That’s a subtle but important distinction.

Features solve individual problems.

Domains solve classes of problems.

Once I started thinking in terms of domains instead of features, deciding whether something belonged inside commit_gh became surprisingly easy.

Does it contribute to the lifecycle of a repository?

If the answer is yes, it probably belongs.

If not, it’s probably someone else’s problem to solve.

That single question has prevented me from adding far more features than it has encouraged.

Good architecture isn’t just knowing what to build.

It’s also knowing what not to build.

Why I Think It’s Done

Software is never finished.

There will always be another GitHub API.

Another Git feature.

Another security scanner.

Another release workflow.

Another request in the issue tracker.

I have no doubt commit_gh will continue to evolve.

Version numbers will increase.

Dependencies will change.

GitHub will inevitably release another feature that makes me think,

“Well… that would actually be quite useful.”

That’s just software.

But architectures eventually reach a point where they feel… complete.

Not feature complete.

Conceptually complete.

Every capability has somewhere to live.

Nothing feels bolted on.

Nothing feels like an exception.

The architecture has symmetry.

When I look at commit_gh today, I don't see a collection of commands anymore.

I see a complete repository lifecycle.

Initialize
Secure
Protect
Develop
Release
Maintain
Observe
Repeat

That doesn’t mean it’s perfect.

Far from it.

It simply means that, for the first time, adding new functionality no longer requires inventing new concepts.

The foundation is already there.

That’s an incredibly satisfying place for any engineering project to reach.

Engineering Is Never Really Finished

One thing I’ve learned about myself over the years is that I rarely look at a project and think,

“Perfect. That’s finished.”

My brain almost immediately asks a different question.

Can we do more?

Sometimes that question becomes,

Can we do it better?

Sometimes it’s,

Can we make it simpler?

Sometimes,

Can we make it safer?

Or perhaps my favourite,

Can we remove one more thing an engineer has to remember?

That’s probably the real reason commit_gh grew from a release helper into everything you've read about in this article.

There was never a grand roadmap.

No five-year vision.

No product strategy.

Just a continuous feedback loop.

Observe
Question
Improve
Use
Observe again

I use the tool every day.

Eventually I notice a tiny bit of friction.

Maybe I typed the same command twice.

Maybe I copied another configuration file.

Maybe I forgot to enable branch protection.

Maybe I wondered whether an older repository still reflected the standards I’d use today.

Those tiny moments rarely look important on their own.

But they all trigger the same question.

Can we do this better?

Sometimes the answer becomes another command.

Sometimes it becomes deleting code.

Sometimes it becomes simplifying something I thought was already simple.

That’s engineering too.

I’ve never believed iteration is evidence the original design was wrong.

Quite the opposite.

Iteration is usually evidence that software is actually being used.

Real projects expose real problems.

Good ideas survive contact with reality.

The weaker ones quietly disappear.

So yes…

I genuinely believe the architecture behind commit_gh is complete.

But I also know myself well enough to make one prediction.

The next time I catch myself repeating something unnecessarily…

The next time a release makes me stop and think,

“There has to be a better way.”

I’ll probably open my editor.

Again.

Test.

Rinse.

Repeat.

And honestly…

I hope I never stop asking those questions.

One Bash Script

People occasionally ask me how large the project has become.

They’re usually expecting a conversation about frameworks.

Libraries.

Build systems.

Configuration files.

Containers.

Cloud services.

The answer is always slightly disappointing.

It’s still one Bash script.

Yes, it’s accompanied by documentation.

A proper Unix man page.

GitHub Actions.

A Homebrew formula.

Some supporting assets.

But the heart of the project remains exactly what it always was.

One script.

I actually like that.

Not because Bash is somehow superior to every other language.

It isn’t.

Because there’s something refreshingly honest about being able to open one file and understand exactly what your tooling is doing.

No hidden services.

No background processes.

No mysterious “magic.”

Just plain text.

I think there’s value in that.

Especially for tooling that’s supposed to build trust rather than mystery.

Repositories Are Part of the Platform

In many ways, they’re the first piece of infrastructure every project owns.

Working in platform engineering, and particularly at HashiCorp, has probably influenced the way I think about software more than I realised.

We spend a lot of time talking about platforms.

About reducing cognitive load.

About creating paved roads instead of obstacle courses.

About giving developers secure, repeatable foundations so they can focus on solving business problems instead of infrastructure problems.

Somewhere along the way I realised repositories deserve exactly the same treatment.

A Git repository isn’t just a folder containing code.

It’s the front door to a project.

It’s where new contributors form their first impression.

It’s where releases are born.

It’s where documentation lives.

It’s where ownership becomes visible.

It’s where security either starts… or slowly becomes technical debt.

We spend enormous amounts of effort standardising cloud infrastructure.

Why wouldn’t we standardise the engineering infrastructure that exists before the first container is ever built?

The more I thought about it, the more obvious the answer became.

Repositories aren’t separate from the platform.

They are part of the platform.

Perhaps one of the earliest parts.

Looking Back

When I wrote the first version of commit_gh, I genuinely believed I was automating Git.

Looking back, I wasn’t.

I was automating engineering discipline.

Git just happened to be where the journey started.

Every feature that followed was simply another attempt to remove a little more repetition.

A little more uncertainty.

A little more cognitive load.

Not because those things are impossible.

Because they’re unnecessary.

One of my favourite observations about engineering is that the best automation rarely feels clever.

It simply feels inevitable.

Almost boring.

You stop noticing it because it quietly becomes part of how you work.

I think that’s probably the highest compliment I can pay any piece of software.

Final Thoughts

A few years ago, if someone had asked me what commit_gh was, I would have answered:

“It’s a helper for Git commits and releases.”

Today I’d answer differently.

I’d probably say:

“It’s the way I believe repositories should begin their lives.”

That may sound like a small distinction.

I don’t think it is.

Because this project was never really about Git.

It was never really about GitHub.

And it certainly wasn’t about Bash.

It was about creating an environment where good engineering becomes the default rather than something you remember to do later.

If there’s one idea I’d like you to take away from this article, it’s this.

Treat your repositories the same way you treat the rest of your platform.

Give them sensible defaults.

Secure foundations.

Clear ownership.

Repeatable workflows.

And enough automation that your future self can spend time building software instead of remembering checklists.

The code deserves your creativity.

The process deserves your consistency.

Everything else is just repetition waiting to be automated.

Getting Started

Curious to see how the complete repository lifecycle works in practice?

commit_gh is open source, so you can inspect the implementation, read the documentation, report an issue, or run it against one of your own repositories:

github.com/raymonepping/homebrew-commit-gh-cli

Install it through Homebrew:

brew tap raymonepping/commit-gh-cli
brew install commit-gh-cli

Then verify that the local environment is ready:

commit_gh --doctor

Create a new repository with the full engineering baseline:

commit_gh \
  --init-repo \
  --init-remote \
  --public \
  --secret-scanning \
  --protect \
  --sign \
  --labels

Or bring an existing repository closer to the same standard:

commit_gh --harden --audit

And, if you are anything like me, begin with the oldest Unix tradition of them all:

man commit_gh

If you’d like to explore the project yourself, you’ll find it here:

https://github.com/raymonepping/homebrew-commit-gh-cli

Whether you decide to use it or simply borrow a few ideas, I hope it encourages you to think about repositories a little differently.

Because repositories deserve engineering too.


메타데이터
post_id
cd36d8f793f7
slug
beyond-git-engineering-a-complete-repository-lifecycle-with-bash-cd36d8f793f7
url
https://medium.com/@raymonepping/beyond-git-engineering-a-complete-repository-lifecycle-with-bash-cd36d8f793f7
canonical_url
https://medium.com/@raymonepping/beyond-git-engineering-a-complete-repository-lifecycle-with-bash-cd36d8f793f7
author_url
https://medium.com/@raymonepping
status
ok
fetched_at
2026-08-18 16:37:06