Skip to main content

Monorepo CI, linting, pre-commits, builds etc., using `mise` tasks.

Project description

misek

Monorepo CI, linting, pre-commits, builds etc., using mise tasks

So you have a monorepo (either monolingual or multilingual). You have various projects, each with separate environments, lints (e.g. pre-commits), tests (e.g. CI), build and deploy scripts (e.g. CD). And maybe you're even already capturing all of these using mise, which is an excellent language-agnostic monorepo tool. (If you're new to mise, then see Why mise? in the FAQ below.)

One missing piece: when you make a change (a commit, a PR, a release), then you need to map changed files into changed projects, so that you can run pre-commits / CI / CD.

misek is a tiny extension (two CLI commands, ~100 lines of code) to mise performing that conceptual conversion, thus providing a way to run:

  1. pre-commits (format, lints) for just the files that have changes on the most recent commit.
  2. CI (tests) in a pull request for just the projects that have changes relative to main.
  3. CD (build, deploy) for just the projects that have just been deployed.

Usage is e.g. git diff --name-only | misek run tests to collect the changed files, and then walk your monorepo to find and execute all the [tasks.tests] corresponding to those files.

[!TIP] For specifically linting/pre-commits, then equivalent tooling already exists in the form of prek (the successor to pre-commit), which does a git diff and figures out which pre-commits to run. misek exists to extend the same principle to every other kind of task (test, build, deploy, ...), and for this reason we consider mise+misek to supersede prek/pre-commit.

[!NOTE] For single-project repos then misek doesn't add much. But in multi-project monorepos then misek makes it possible to handle multi-project cross-cutting changes, and to handle changes within nested projects (e.g. maybe a top-level lint prevents committing large files, whilst a project-specific lint is used to format code).

Installation

Add to your top-level mise.toml:

[tools]
"pipx:misek" = "latest"

Usage

CI, tests in pull requests

Add the line

git diff main --name-only | misek run tests

to your CI. This will run all [task.tests] associated with your changes. You can use a different name than tests if you like.


CD, build after merge

Add the line

git diff main --name-only | misek run build

to your CD. This will run all [task.build] associated with your changes.

You can use a different name than buildif you like. You can also add extra task names if e.g. you have separate build and deploy tasks, and would like to invoke both:

git diff main --name-only | misek run build deploy

pre-commits

Run misek install lint to set your git pre-commit hook to run

git diff --cached --name-only | misek run lint

On every git commit, this will run all [tasks.lint] associated with your changes. You can specify a different name than lint if you like.

To make this happen automatically, you can have this automatically performed as part of mise install:

[hooks]
postinstall = "misek install lint"

[settings]
experimental = true  # to enable hooks.postinstall

jj users, you should arrange to run jj diff --name-only | misek run lint in whatever way you most prefer. For example, I like to run jj config edit --repo, and add the following block:

[aliases]
check = ["util", "exec", "--", "bash", "-c", "jj diff --name-only -r 'latest((@-::@) ~ empty())' | misek run lint"]

Manually run lints/tests/etc for just your cwd

You could lint your current working directory with git ls-files --full-name | misek run lint. This is equivalent to running mise run ...all lint tasks, including those defined in parent mise.toml files....

This use-case exemplifies the key conceptual point: when running tasks from mise then we think of them on a per-project basis. When running tasks from misek then we still define them on a per-project basis, but we execute them on a per-file basis.


How does it work? / The algorithm

Click to expand

misek provides two CLI commands:

misek install <task1> <task2> ...
misek run <task1> <task2> ...

The first installs a pre-commit hook that runs git diff --cached --name-only | misek run <task1> <task2> ... on every commit.

The second:

  1. consumes a list of files from stdin;
  2. finds all mise.toml files in all their parent directories (stopping once the monorepo root is found, defined by the presence of a .git directory);
  3. finds all of their tasks with name matching any of <task1>, <task2> etc.
  4. executes all collected tasks in a single mise run <task1> <arg1> ::: <task2> <arg2> ..., so that parallelism, dependencies, error handling etc. are all done correctly as per mise's usual behaviour.
    • Each task is passed a single argument, which is the path to a temporary file listing just those entries from stdin that are within its part of the file system.

misek works when using mise with both monorepo and non-monorepo tasks. (In the former case it does a single big mise run call; in latter case it makes a separate mise run call for each mise.toml it finds.)


Examples

See the examples directory for a few full worked examples.

FAQ

Why mise?

To be clear, misek is not affiliated with mise. I'm just a fan.

mise collects together three key things that are especially useful for both single-project repos and monorepos:

  • pinning dependencies of core tooling like uv (Python), npm (JavaScript), etc.
    • This is like asdf.
    • This ensures that your whole team use the same versions of these dependencies.
  • adding entries to your $PATH based on your cwd.
    • This is like direnv.
    • This automatically enables exactly the dependencies you require, regardless of what else is on your system.
  • defining tasks (with arbitrary names, though 'test', 'build' etc. are common choices), including with dependencies/parallelism between them.
    • This is like make or just.
    • This provides a way to record all the various test/build/etc. scripts that are needed.

Importantly, it's also language-agnostic. No special-casing to e.g. the webdev stack.

Together with misek, one then ends up with a unified system that also covers:

  • pre-commit/prek;
  • the usual custom CI/CD scripts;
  • simple build systems.

When should you not use mise/misek?

The main reason not to use mise is if you need the heavyweight stuff: remote caching or hermiticity. Use Bazel or Buck2 instead.

You can often also get away without mise if you have a monolingual single-project repos, for which you may prefer to stick to just uv (Python) / just npm (JavaScript) / etc.

Otherwise, use mise.

Finally, use misek whenever you have a monorepo (either monolingual or multilingual) that uses mise.


Where does the name misek come from?

misek = mise + k, with the k meant to inspire thoughts of:

  • prek (which is pre-commit + k);
  • make (which it sounds a bit like);
  • cute names for bears in Polish and Russian (for which reason I pronounce 'misek' as 'mish-ek' 🐻).

How to best configure mise for use with misek?

Once you start using mise with monorepo tasks, then it will ask you to list the child mise.toml in [monorepo].config_roots.

Personally I think this is a wart (one of the few in mise), as the point of a monorepo is distributed-configuration-by-convention, not centralised declarations.

You can work around this by adding the following to your root mise.toml (up to whatever depth you require):

[monorepo]
config_roots = ["*", "*/*", "*/*/*", "*/*/*/*"]

Hurrah! All possible configs are located (up to some depth).


Versus prek, pre-commit, make, just, Bazel, etc. etc?

It took me a while to realise (despite... working with Blaze at Google...) that tooling like pre-commit and prek belong in the same category of thought as mise, make, Blaze, Buck2, etc.

That is: linting, formatting, tests, builds, deploys, etc., are all special cases of the exact same flow:

  1. git diff --name-only to see what changed in this commit / in this PR.
  2. walk up the file tree from all changed files, to look for .pre-commit-config.yaml/mise.toml/etc., which specify what tasks need to run based on file location.
  3. filter down to which tasks need to be run based on the trigger:
    • pre-commit: linting + formatting.
    • pre-merge PRs: linting + formatting + tests.
    • post-merge PRs: builds + deploys.
  4. run them!

Honestly what's surprising to me is how few options there seem to be supporting this exact flow, given how ubiquitous this use-case is. All the tooling I'm aware of only seems to handle some slice of this.

  • Bazel and Buck2: the heavyweight solutions, the right choice if you're a big org with big needs.
  • Dagger: I've looked through the documentation but never managed to get past the boilerplate.
  • Earthly: has now shut down.
  • GitHub/GitLab: .github/workflows/.gitlab-ci.yml are good as a shim to call your 'real' CI scripts, but serious engineering should not be included here, to avoid vendor lock-in. (Also these are terrible YAML-masquerading-as-code.)
  • moonrepo, turborepo, Nx: largely based around the webdev stack, restrictive in the general case.
  • devenv: largely based around the Nix stack, this is again kind of a heavyweight choice.
  • mise: this does a good job defining tasks ('fancy bash scripts', to quote the author). It doesn't do any of the rest of the flow on its own though.
  • make: is explicitly just a build system (caches based on the presence of files, etc.), but doesn't do the rest.
  • just, taskfile: are both explicitly just task runners, basically superseded by mise tasks if one is already using mise.
  • pre-commit: superseded by prek.
  • prek: actually does the git-diff > walk through file tree > run tasks flow! This is by far the closest to what's discussed here. However it specifically only really handles pre-commits. Whilst it does have a general concept of stages, (a) I couldn't get these working, (b) these are specifically tied to the git-commit-push-etc model and aren't concepts like 'test' or 'build'.

It's also informative to consider prek/pre-commit's model here, in which the tool tries to install+cache an environment for you. I have come to regard this as an antipattern, as typically we actually want to run in the same environment as the rest of our tooling is using, e.g. in Python this would be the .venv created by uv run. This is another one of the reasons that has pushed me away from prek/pre-commit.


Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

misek-0.1.1.tar.gz (12.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

misek-0.1.1-py3-none-any.whl (15.9 kB view details)

Uploaded Python 3

File details

Details for the file misek-0.1.1.tar.gz.

File metadata

  • Download URL: misek-0.1.1.tar.gz
  • Upload date:
  • Size: 12.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for misek-0.1.1.tar.gz
Algorithm Hash digest
SHA256 e014f1f8ddbc44de91911d771a500812dab938c128e8f0012d79a071f8ca1775
MD5 0a0ea0b0741970a2e2ad4f215c815c37
BLAKE2b-256 0a43c97890f9c7f812cdb925a86fb783b65ce2759bc3e8ea639488b67f7498f3

See more details on using hashes here.

File details

Details for the file misek-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: misek-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 15.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for misek-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ebd4c69b029e74b2fc65040af580ee16860a10f44990442d4bd3915b4985783f
MD5 6fa6140c0f58b62ebfdeb382816ada43
BLAKE2b-256 44ed5a48bc9fed243650470004e489aa9be91c727ae97afa894959e7c652388b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page