Let's make history

When it comes to hosting static websites, I’m used to the gitlab way. It’s quite simple and elegant. You have a CI, with a job called pages. This job has a directory artifact called public. Done, the content of this directory is served as the website.

Because of reasons, I wanted to migrate my old, and now closed, website from gitlab to codeberg. And it does not work the same way. I mean, it can work pretty much the same way. But there is an alternative.

And that alternative led me to the discovery of the --orphan flag of some git subcommands.

What’s up with codeberg?

Codeberg pages uses git-pages.

The idea behind all this is pretty straightforward:

It’s a tiny bit more involved. You’ll need a webhook to automatically update the website. But there it is in essence.

OK, you’re having fun with stuff named pages, we get it. So what?

Well, on gitlab, my repository contains the code for a static site generator to work on. Like, for example, the very website you’re reading is generated using zola. It’s made of separated html templates and markdown content that are then mixed together to create publishable files. Like I said in the introduction: I have a CI pipeline that compiles all this to html and css in a public directory which I declare as a pipeline artifact, effectively turning it into a website served on gitlab.io.

Notice anything?

That’s right! The content of the public directory is never committed to git. Which is radically different from what’s expected from git-pages.

What’s the matter with that?

Well, say, you have your main branch with everything needed to build a website. And now, you have to commit and push to the pages branch. Simple, right? git switch -c pages, then build, then replace the repository’s content with the public content, commit, push, done.

Weird.

Yep, weird. Let’s keep that thing going. You want to update an html template or write a new post so you’ve got to go back to the code with a git switch main, do what you have to do and then… Then you’re stuck. You can’t switch to the pages branch again without losing your whole work because you deleted it earlier. And the issue arises because of the relation between the main and the pages branches. The fact that the parent of the first commit in the pages branch lives in the main branch forbids you from easily editing the latter and publishing the former.

Gosh, how can we avoid such a dramatic conundrum?

Well, last week me thought it’d be best to have two repositories.

One with the source code, one with the built output. I’m fond of the git submodule thingy so it made sense to me to have the pages repository as a submodule of the main repository. It’s simply a matter of git submodule add <pages address> public and done, a zola build updates the pages repository.

But there is something unsatisfactory about it, don’t you think? Having two repositories for such a simple need. I mean, wouldn’t it be better to have a single repository and yet have two unrelated branches?

--orphan to the rescue

Wow, what a surprise! I did not see that coming.

I know, I know… It reads like a good police novel.

You’re on the main branch, you need to create a pages branch, you don’t care about the content of the main branch so you… Create a new worktree of course.

git worktree add --orphan ../pages

The help message for the --orphan flag in that case states: create an unborn branch. It creates a new branch called pages in a new directory ../pages. This branch has no commit, no history, none, nada. Pretty much like when you go git init: there is no previous history, the repository is waiting for its first parentless (orphan) commit.

To be fair, until now I thought there could be only one commit without a parent in a git repository. I was wrong and it turns out there are other ways to create them. For example, you can run git switch --orphan pages. It works, doing so leaves you in the same repository, at the same path, but without main’s history.

Hey, if switch does the job, why do you use a worktree here?

Ah! Thanks for noticing.

Well, simply because the issue I want to tackle is the publication of a static website. And I need both my code on the main branch and my website in the pages branch. Using switch gives me either one or the other but I need both in parallel so that I can build and publish.

zola build
rsync --archive --delete --exclude .git public/ ../pages
git -C ../pages add .
git -C ../pages commit -m "publish"
git -C ../pages push

And, done! One repository, two unrelated branches for a published website.

Hey, what’s up with the rsync command in the middle?

That? Well, it takes the content of the public directory (which is populated by zola build) and copies it to the ../pages directory. The --archive is an alias to a collection of other flags making the copy recursive all the while keeping the permissions and ownership of the files. The --delete ensures there are no leftovers from previous invocations in the ../pages directory so that I don’t publish deleted files. And the --exclude .git tells rsync not to copy the .git directory. Of course, there is no .git in public, that would not make any sense. But it also protects the ../pages/.git directory from the --delete flag.

What else can the --orphan flag do for you?

Well, it depends. Honestly, as a developer, not much.

In my experience, trunk based development still is the best way to ship software. You can have as many feature/fix/whatever branches as you want. Once your work reaches the main branch, it gets sent to a staging environment. And if you ever choose to release that version, the staging version gets promoted to prod. In that case, main is the source of truth and everything is supposed to get to main at some point.

Hey, we’re not all devs here!

Yeah, that’s true. And if you’re on the operational side of the workforce, I may have something for you.

Infrastructure as Code

It’s no surprise that devs and ops work together. By that I mean they often push code to the same repository. And that’s normal: in the age of cloud computing, deploying an application requires some associated infrastructure. Be it a bucket, a database or a set of permissions, it’s good to put it in code too.

Now, in my experience, this is where things get complicated. Not because they’re technically hard but because they seem unrelated at first and both devs and ops have different needs. For a start, we don’t have the same requirements. When I’m working on an application, I need my dev environment to compile, lint, run various classes of tests. And when I’m working on the infrastructure, I need my ops environment to plan and apply changes to various providers. Those are somehow completely unrelated codebases: I can work on either by mostly ignoring the other and that’s a good thing as it means they’re decoupled.

Your worktree thing again?

Well, no, not this time. This time the git switch --orphan iac does the trick nicely. It allows you to have one history for the application and one for its infrastructure. They’re neatly separated and both teams can work on either without stepping on each other’s toes.

And it comes with some collaboration perks too. It becomes much easier to determine who’s in charge of code reviews on the forge. Also, you’re sure that merging the IaC won’t trigger a new application build and this goes without the CI having to be smart about it. Anyone allowed to work on a project can do so on every aspect, but concerns are well separated.

What else?

Well, why stop at such a good point? Granted, the next bit may be a tad controversial but here is my idea. Beware, it’s just an extension of my previous point.

If you ever deploy an application on kubernetes using argocd, you know you have to write some extra infrastructure as code. This code does not manage resources on the cloud provider side. Instead, it defines some runtime settings using yaml values files. Well, I think this deserves to be in the same repository as well.

The argocd App of Apps pattern most of platform teams use is great. It allows you to declare what applications to deploy on a kubernetes cluster without manually applying kubernetes manifests. But I think it should stop there and not define configuration. The said configuration should live in an argocd branch on the same repository.

Hey, who cares? It’s yaml files, why does it matter anyway?

Yep, one could think this way. But it’s in fact both simpler and, most importantly, safer this way.

Remember when I spoke about promoting staging to prod? The way it’s done is to have a docker tag for both environments. And promoting staging to prod consists of moving the tag from the staging values to the prod values. Boom, done, no rebuild, tags are still immutable and yet the staging image is now used in prod in a blink and kubernetes handles the pods rollout for us so that there is no service interruption.

How is that any simpler and/or safer?

If those yaml files are in the main branch, pushing a new version might trigger non-desired CI pipelines if you’re not careful enough in your CI configuration. But if they live in a dedicated branch, this is one less thing to think about, it’s trivial to ensure that no new application build happens, for example. So, yes, it’s simpler because side-effects are less likely to appear.

And now, my favorite part. It’s your repository’s CI which is in charge of updating the yaml values. Meaning, if those values are in another repository as it’s often the case with the App of Apps pattern where deployments and versions are stored in the same repository, your repository’s CI must have permissions to push to that App of Apps repository. No matter the implementation details (credentials, service account…), the consequence is that a compromised CI can wreak havoc on every other deployment by messing with the values.yaml in that shared repository. With the proposed solution, the blast radius is contained to a single repository/application. So, yes, it’s safer because side-effects are less likely to appear.

Conclusion

Well, a few days back I thought every single commit in a git repository where in some way the descendent of that one first commit. Turns out not only I was wrong but I see great values in the possibility to maintain multiple unrelated trees in a single repository.

Feel free to drop me a line on mastodon if you want to have a chat about all this!