· Brayen Gavilanes · SysAdmin · 8 min read
Boost Your Development Workflow with a Faster CI Pipeline
Learn how to optimize your development workflow with a faster CI pipeline using Rust-based development utilities.
For one of my website projects, using AstroJS, I wrote a useful CI pipeline:
( pnpm check || pnpm fix ) && pnpm build && pnpm preview
Where provided scripts in the package manifesto follows:
(...)
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"check": "npm run check:astro && npm run check:eslint && npm run check:prettier",
"check:astro": "astro check",
"check:eslint": "eslint .",
"check:prettier": "prettier --check .",
"fix": "npm run fix:eslint && npm run fix:prettier",
"fix:eslint": "eslint --fix .",
"fix:prettier": "prettier -w ."
},
(...)
The behaviour of my pipeline being:
- First, I run
( pnpm check || pnpm fix )
.- This is quite interesting. The
pnpm check
command runs thecheck
script defined in thepackage.json
file.- This script checks the code for errors and warnings using tools like ESLint, Prettier, and Astro.
- This is quite interesting. The
- If the
pnpm check
command fails, i.e., returns a non-zero exit code, then thepnpm fix
command is executed.- This command runs the
fix
script defined in thepackage.json
file.- This script attempts to fix any errors or warnings found by the
check
script.
- This script attempts to fix any errors or warnings found by the
- This command runs the
- Next, if the previous command is successful, i.e., returns a zero exit code, then the
pnpm build
command is executed.- This is quite useful. The
pnpm build
command runs thebuild
script defined in thepackage.json
file.- This script builds the project using Astro.
- This is quite useful. The
- Finally, if the
build
command is successful, then thepnpm preview
command is executed.- The
pnpm preview
command runs thepreview
script defined in thepackage.json
file.- This script starts a development server and previews the built project.
- The
And, altought this was quite handy, I did have some hesitation: the pipeline was slow. Hella mad about that.
Don’t get me wrong: ESLint and Prettier are almost a standart in projects such as this. However, I do development in terminal-first environtment; and, it was painful to make a small change and wait for all the checks, especially pnpm astro check
, to finish. Moreover, npm
and friends are not task runners: as seen in the provided JSON, declaring targets depdencies is finicky at best.
The Rise of Rust-based development utilities
The first experience I got for using a Rust-based toolkit around a easier-to-implement, but not that performant, language was ruff
. I wrote a webscrapper using Selenium, in python. I used poetry
as a package and dependency manager; and, for linter a formatter, the mentioned ruff
. The pipeline for that project is roughtly describe as:
( ruff check --fix && ruff format ) && poetry run
Which:
First, I run
( ruff check --fix && ruff format )
.- The
ruff check --fix
command runs theruff
linter with the--fix
option, which attempts to automatically fix any errors or warnings found in the code as per the linter specification.
- The
If the
ruff check --fix
command is successful, i.e., returns a zero exit code, then theruff format
command is executed.- This command runs the
ruff
formatter to format the code according to the project’s style guidelines.
- This command runs the
Next, if the previous command is successful, i.e., returns a zero exit code, then the
poetry run
command is executed.- The
poetry run
command runs the project’s main command or script usingpoetry
- The
It was a pleasant experience to work with such a toolkit. Parametrize, write a Makefile, containerize it and call it a day. Crack a cold one open with the boys, yeah!
It was a surprise to know there was, indeed, similar workflow for web development. As I hinted, this works best when you use a proper task runner.
make
as task runner
make
, or more precisely in modern Linux boxes, gmake
— for GNU Make —, is a build system for C/C++ projects. It’s part of the POSIX set of standarts, so you can rely it’s present in most UNIX-based systems. Using .PHONY
targest, as per GNU Make spec, or FRC
— for For ReCompoline — convention, you can use make as your task runner, almost for free.
The Makefile
for this webpage is as follows:
all: dev
check: FRC
dprint fmt
biome check --write
build: check
pnpm run build
preview: build
pnpm run preview
dev: check
pnpm run dev
FRC:
Biome and Dprint as rust-based toolkit for web development
biome
is
A toolchain for web projects, aimed to provide functionalities to maintain them. Biome offers formatter and linter, usable via CLI and LSP.
that’s performant. I was hesitant to migrate from ESLint + Prettier for several reasons; but two things change my mind:
biome import
CLI interface is quite handy to manage correspondent migrations,- In the benchmark section, the developers proved Biome’s implementation were around 20 times faster than
xargs -P
- Reading throught the the provided README, not only they wrote the benchmark using a highly optmize GNU
find
and GNUxargs
combo; but even wrote them usinghyperfine
- Reading throught the the provided README, not only they wrote the benchmark using a highly optmize GNU
In that moment, I knew Biome’s developers did know what they are into.
Migrate fast, and often
Once all doubts settle, I began installing appropiate software:
nix-shell -p dprint biome
and then
biome init
biome migrate eslint --write
biome migrate prettier --write
Biome currently has partial support for HTML specs and superset so I use dprint, a similar project — but just for formatting, no linting —, for at least formatting everything not at least now supported by Biome.
dprint init
Select plugins (use the spacebar to select/deselect and then press enter when finished):
[x] dprint-plugin-typescript
[x] dprint-plugin-json
[x] dprint-plugin-markdown
[x] dprint-plugin-toml
[x] dprint-plugin-dockerfile
[ ] dprint-plugin-biome
[ ] dprint-plugin-ruff
[ ] dprint-plugin-jupyter
[x] g-plane/malva
[x] g-plane/markup_fmt
> [x] g-plane/pretty_yaml
[ ] g-plane/pretty_graphql
Makeshift testing for UNIX lads, lasses, and others
We’ll use hyperfine
as a benchmark util.
nix-shell -p hyperfine
First, let’s do a simple test; whether the — almost same — intended UNIX pipe does perform better with another toolset:
hyperfine -w 1 -r 1 '(pnpm check || pnpm fix) && pnpm build' 'pnpm astro check && dprint fmt && biome check --write && pnpm build'
Benchmark 1: (pnpm check || pnpm fix) && pnpm build
Time (abs ≡): 50.117 s [User: 91.089 s, System: 10.807 s]
Benchmark 2: pnpm astro check && dprint fmt && biome check --write && pnpm build
Time (abs ≡): 31.803 s [User: 63.479 s, System: 7.478 s]
Indeed, it does!
Summary
pnpm astro check && dprint fmt && biome check --write && pnpm build ran
1.58 times faster than (pnpm check || pnpm fix) && pnpm build
However, this is a not-so-fair test. pnpm astro check
is a bottleneck, and we are not testing for the whole pipeline performance, but for ESLint + Prettier vs dprint + biome’s.
hyperfine -w 1 -r 1 '(pnpm check || pnpm fix) && pnpm build' 'dprint fmt && biome check --write && pnpm build'
Benchmark 1: (pnpm check || pnpm fix) && pnpm build
Time (abs ≡): 25.641 s [User: 52.933 s, System: 7.568 s]
Benchmark 2: dprint fmt && biome check --write && pnpm build
Time (abs ≡): 14.619 s [User: 29.942 s, System: 4.688 s]
Summary
dprint fmt && biome check --write && pnpm build ran
1.75 times faster than (pnpm check || pnpm fix) && pnpm build
Now, let’s do a full check:
hyperfine -w 3 -r 10 '(pnpm check || pnpm fix) && pnpm build' 'dprint fmt && biome check --write
&& pnpm build'
Benchmark 1: (pnpm check || pnpm fix) && pnpm build
Time (mean ± σ): 25.358 s ± 0.616 s [User: 51.724 s, System: 7.522 s]
Range (min … max): 24.839 s … 26.294 s 10 runs
Benchmark 2: dprint fmt && biome check --write && pnpm build
Time (mean ± σ): 14.245 s ± 0.083 s [User: 29.403 s, System: 4.612 s]
Range (min … max): 14.122 s … 14.386 s 10 runs
Summary
dprint fmt && biome check --write && pnpm build ran
1.78 ± 0.04 times faster than (pnpm check || pnpm fix) && pnpm build
I would like to point out that even in this test, we are not testing just our toolset’s performance, for there’s the pnpm build
— assigned to pnpm astro build
— step.
hyperfine -w 1 -r 1 'pnpm fix' 'dprint fmt && biome check --write'
Benchmark 1: pnpm fix
Time (abs ≡): 11.463 s [User: 23.603 s, System: 3.205 s]
Benchmark 2: dprint fmt && biome check --write
Time (abs ≡): 615.1 ms [User: 1420.3 ms, System: 203.6 ms]
Summary
dprint fmt && biome check --write ran
18.64 times faster than pnpm fix
astro
components — and pnpm invocations — are the bottleneck in our pipeline.
Next steps
One of the problems with the scripts declared in the package manifesto is that the abstraction pnpm check
is too wasteful: astro check
should be done by itself; and then, prettier check . || prettier --write .
, the same for ESLint.
On the case of astro check
: in my testing, it just checked for Astro-releated typesafety features, so I’m asuming ESLint and biome are sufficient. However, I would at least invoke it once, as a pre-build procedure.
Even if those optimizations were implemented, dprint + biome is faster. Don’t forget the fact that we are using dprint as a replacement for when biome does support HTML + supersets, that includes .astro.
I wouldn’t advice for the use of a JS/TS-based task runner; but, I do know that make is not well known, even though is part of a UNIX standart.
Performant, local-first development for freedom volunteers
My collegue, Kernel Chaos, and I are co-leading a meta-community, Ecuador in Tech that fosters and empowers tech-related communities. For the community, by the community! We are all volunteers!, and we intend to provide tools, capacitation and infraestructure for the betterment of our communities and volunteers.
Please, drop in to Ecuador in Tech and say hello!