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 (and maybe sub-projects, etc.), each with separate environments, lints (e.g. pre-commits ran locally), tests (e.g. CI ran remotely), build and deploy scripts (e.g. CD ran remotely). And great news, maybe you're 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.

The idea is simple:
(a) specify entry points for lints/tests/etc. using mise's tasks, e.g. add a [tasks.tests] to a project's mise.toml.
(b) use git diff --name-only | misek run tests to walk your monorepo to find and execute all the tests corresponding to your latest changes.

[!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 detects and blocks large files, whilst a project-specific lint formats 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.

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.0.tar.gz (12.5 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.0-py3-none-any.whl (15.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: misek-0.1.0.tar.gz
  • Upload date:
  • Size: 12.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for misek-0.1.0.tar.gz
Algorithm Hash digest
SHA256 0f2db063b0297bff99bf79834cefeea314ae7cf291e86567e4dcf767032949b8
MD5 59ae47f2a7fa23d9fa01c6ba2c29231c
BLAKE2b-256 90886c099fcc75ddb15e2d87eb7e127188976081360e1f251bd012fb44d2050a

See more details on using hashes here.

File details

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

File metadata

  • Download URL: misek-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 15.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for misek-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d28bc71d1987915cc5b281fd8bb707a209ddfb1ea3e07a838d2dfa65aee5b296
MD5 c4b81578a945c699c70d723ef7309095
BLAKE2b-256 838281c2f1a5eb95749e6958977ce95e37aade7a8a94df4f39cea78e2386578b

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