I think you are doing CI wrong

Continuous integration can be incredibly helpful. Most just slow you down.

Continuous integration is ubiquitous. Almost every project out there has some kind of pipeline set up, and most of them I’ve seen are terrible.
The idea is simple: have all your lints and tests run before something is published, in a consistent environment, preventing a release if something is smelling weird. It’s a good idea, and there really isn’t a codebase where a requirement of running checks is not a good idea. The problem arises when suddenly every single tool becomes a nail once you introduce CI as a hammer.

Moving the wrong things into CI

There are a ton of things that make sense to run in CI - the most obvious being tests. Engineers might have different machines, operating systems, runtime versions, dependency versions. There are an infinite amount of permutations on what a developer setup might look like, and (for server-hosted applications) only one or at most a handful that we’re running on in production. Running our runtime tests on a machine that is similar to the production setup becomes the obvious choice.
There are also checks that we moved to CI that are not a good fit.

Let’s look at formatting. Every formatter I’ve seen has some kind of --check-formatted option. It doesn’t write to any files, but exits with a non-0 code if it finds an unformatted file. The formatter has some distinct differences to running a test suite:

Couple these two points with the fact that CI has a somewhat slow feedback loop: you push a change, wait a few seconds for it to be picked up by a CI runner, setup the environment, install OS dependencies, fetch project dependencies, then run your formatting check. At some point, open the tab in your browser again to see the The following files are not formatted: [...] error, run the formatter, do a format commit and repeat. This can, depending on the pipeline setup, take minutes. In contrast, running the Elixir formatter on a large project I have laying around takes about 3 seconds (and the Elixir formatter is not in the group of fastest tools since it spins up a BEAM VM). That’s multiple orders of magnitude of difference in terms of feedback loop.

What to do instead

CI pipelines are not the only tool we have to ensure that our project is well-formatted. git has supported hooks for an eternity. There are tools like pre-commit or husky to make setting them up easier. So, what happens if we move our formatting check to a pre-commit hook?

The first two points are an obvious trade-off you can make. If your commit history does not have a single format in there, I applaud your discipline. If you are like me and every pull request has at least one of them, it’s probably a good balance to check in the pre-commit stage.

Ignoring performance and a strict requirement on running pipelines

CI pipelines run a lot. I’m on a small team, and there’s barely any time of the day where there isn’t anything running. Our pipelines are also egregiously slow and required. This has a ton of drawbacks, the two main ones for me being:

The first point has an easy fix: allow an engineer to veto the requirement of a pipeline run. It should not be abused in a way that quality suffers, but in some cases it can actually increase quality since we are delivering a fix faster. We should also be able to trust ourselves to use such an option wisely.
Performance is a more difficult topic, as there are so many different reasons a pipeline might be slow, but paying close attention to caching is probably worth it. The main takeaway I have for this point is that investing significant time on improving this is almost always worth it. Count how many pipelines your engineers are running per week, then multiply the time they spend waiting on them with their salary and remember this beautiful xkcd.