CI for me was this thing that it was just supposed to suck and it was supposed to be a pain to work on [...]. It turns out it doesn't have to hurt and it can be fun to work on CI

That was my opening line for why I joined Dagger in Dagger 2024: A Shared Vision. Since then, I've been in situations where I'm reminded that what we are building does make the CI part of the stack fun to work on. Today I'm going to show what that looks like in practice with something I worked on this past week.

Preview environments are great. Although they can be challenging to implement at a large scale, for our small team they work amazingly well. Their ephemeral nature makes them reliable. Unlike traditional long-lived staging environments that end up becoming their own separate application, preview environments come and go since their lifecycle is linked to pull requests. Authors can play with it as they see fit, share public URLs and be confident that what they are changing in a pull request will not break production. Well... Until last week that wasn't the case for us. Due to constraints in engineering time as well as limitations of RDS, preview environments for dagger.cloud were using the same production database. This meant developers needed to take great care and treat the data in  preview environments as real production. It was time to fix that.

Enter neon.tech, a managed Postgres with built-in branching. You create a database, you run neonctl branches create and you have a dedicated instance of your database with an exact copy of production data. But what does all this have to do with CI? Well, to make this work someone has to:

  • Provision the new branch using neonctl
  • Store & share the credentials safely
  • Run pending migrations introduced in the PR
  • Point the preview environment to this new database

If I tell you right now: go and implement this in GitHub Actions or GitLab CI, you will start imagining unspeakable levels of YAML and more than 10 commits titled "Fix CI" and "Does this fix it?". At least that's what I was picturing (based on my own experience) and it definitely does not fit into my definition of fun.

What would implementing this in a fun way look like? For starters I should be able to orchestrate all of this in my favorite programming language. I absolutely love programming and enjoy writing pretty much any kind of code. Secondly, I should be able to have a fast developer loop. Instant feedback, without requiring ephemeral remote environments, is mandatory. It is not fun to git commit -am "Test CI" && git push every time I make a change. And finally, it should just work. Brittle software is not fun. Let's start coding.

First, we create a Dagger module and add a function that provisions the neon branch:

Testing that locally is easy, put an API Key in an environment variable and that is it:

Nice! No "Fix CI" commits so far. Let's keep going.

These branches need to be accessed from our infra. We load secrets from AWS Secrets Manager so lets store the connection string there. Before we do that, we want to check if the neon branch is already provisioned to prevent unexpected failures (no brittle software!):

Test it again:

Should we push it? Nah, we don't need to, we can test the whole thing locally.

Up next is running migrations on this new branch. We use goose and have a set of go files that run migrations for us. We already use Dagger to build the application. This means we already have a function returning a dagger.Container with the app code and dependencies all ready to go. All we have to do is get the container, cd into the right directory, set the connection string and run the command:

Does it work?

Yes, it does 🥳. Onto the next thing, move fast, break nothing.

Now it's time to delete this branch once the pull request gets closed. It should be straight forward:

And one last test:

It works! Time to wire everything together. While YAML is not fun, some of it, in small doses, is necessary (for now...):

I'll spare you from looking at the workflow for tearing things down, this was enough YAML already.

Now that it's all done and already tested locally, we can push it: git commit -am "Build CI" && git push. We are confident that it will work the same as it did locally. Just think about all the "Fix CI" commits which we didn’t have to do!

We might have different definitions of fun, but this for me was definitely fun. The dev loop felt smooth, orchestrating everything in a familiar programming language was great, and I was able to move quite fast and have everything working in the time it takes to drink 1 liter of Mate.

If you want to learn more head over to the Dagger Docs or join our Discord. If you are interested in implementing this yourself you can use this module as a good starting point.

Thanks for reading this short rambling. Until the next one đź‘‹