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 (three CLI commands, <200 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 three CLI commands:

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

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

misek resolve:

  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. outputs JSON with format [{cwd: string, groups: [{tasks: [string], files: [string]}]}] giving the list of all mise.tomls and their tasks that need running, and which input files are within the scope of that group of tasks.

misek run:

  1. performs misek resolve to gather all tasks that need to run.
  2. executes all collected tasks.
    • if using mise monorepo tasks then they are all executed 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.
    • if using mise non-monorepo tasks, then all tasks from each single mise.toml are executed in a single mise run <task1> <arg1> ::: <task2> <arg2> ....
    • 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.

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.2.tar.gz (13.2 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.2-py3-none-any.whl (16.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: misek-0.1.2.tar.gz
  • Upload date:
  • Size: 13.2 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.2.tar.gz
Algorithm Hash digest
SHA256 1f4cfa27719dbe8ca6f5f9b585cd6cb88591624b7b9f3e108567b545d9189038
MD5 418a12d4794d01c759aabb8f4a30056a
BLAKE2b-256 83a86c33a4c124aa211dfff8baff94c0043443bc90d586f3570bb2e4a617d266

See more details on using hashes here.

File details

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

File metadata

  • Download URL: misek-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 16.6 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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 32cbce2a2ffb0c1c444526c96481f7a9f13a88b2a2239ad67f0be31700ec8e5f
MD5 f98c65f184b8328dbaef2afd01560275
BLAKE2b-256 70b0a320980604d6b58d7fe179c8b4b32cae89e97a126f7fa439638759c84f29

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