Skip to content
hareharePublic

About

A task runner that executes code blocks in Markdown files based on section titles.

Topics

Resources

Stars

17 stars

Watchers

0 watching

Forks

Latest commit

 

History

349 Commits

Folders and files

Repository files navigation

mq-task is a task runner that executes code blocks in Markdown files based on section titles. It is implemented using mq, a jq-like command-line tool for Markdown processing, to parse and extract sections from Markdown documents.

demo

Warning

mq-task is currently under active development.

Features

  • Execute code blocks from specific sections in Markdown files
  • Task dependencies with automatic execution ordering, including calls that pass arguments
  • Parallel execution of independent dependencies, joined before the dependent task runs
  • Loops (for over a list, a glob, a matrix or another task's output)
  • defer cleanup tasks that run even after a failure
  • Conditions (if), retries and timeouts (plus ignore_error), confirmation prompts, and passing a task's output to its dependents
  • Importing tasks from other Markdown files
  • Skip up-to-date tasks (sources/outputs or custom status commands) and --watch mode
  • Dependency graph output (graph) and shell completions
  • Finds tasks.md (or README.md) from any subdirectory, completes task names in your shell, and catches typos in meta and task names with a suggestion
  • mq-task check finds unknown keys, missing dependencies, cycles and missing runtimes without running anything
  • CLI options for scripts and CI: --summary, list --json, --output group, --yes
  • Interactive TUI with search, details, argument prompts, per-task progress and a log pane
  • Named task parameters with defaults, plus positional args
  • Per-run --env/--dir overrides, plus meta-declared defaults for environment variables, .env files and working directory (details)
  • Shared variables, fixed or computed by a command (vars = { VERSION = { sh = "git describe" } })
  • Default task, aliases, and private tasks
  • Configurable runtimes for different programming languages, with real exit codes and interactive stdin
  • Support for custom heading levels
  • TOML-based configuration
  • Built on top of the mq query language

See the documentation for the full guide and reference, or how the concepts map if you come from just or Task.

Installation

Quick Install

curl -sSL https://task.mqlang.org/install.sh | bash

The installer will:

  • Download the latest mq-task binary for your platform and verify its checksum
  • Install it to ~/.local/bin/
  • Update your shell profile to add ~/.local/bin to your PATH

Cargo

$ cargo install --git https://github.com/harehare/mq-task.git

Usage

Run a task (shorthand)

# Run from tasks.md, else README.md (searched up to the repository root)
mq-task "Task Name"

# Run from a specific file
mq-task -f tasks.md "Task Name"

Run a task (explicit)

mq-task run "Task Name"
mq-task run --file tasks.md "Task Name"

Pass arguments to a task

You can pass arguments to your task using -- separator:

# Pass arguments to a task
mq-task "Task Name" -- arg1 arg2 arg3

# With explicit run command
mq-task run "Task Name" -- arg1 arg2 arg3

# From a specific file
mq-task -f tasks.md "Task Name" -- arg1 arg2

Arguments are accessible via environment variables:

  • MX_ARGS: All arguments joined by space (e.g., "arg1 arg2 arg3")
  • MX_ARG_0, MX_ARG_1, ...: Individual arguments

Example in a Markdown task:

## My Task

```bash
echo "All args: $MX_ARGS"
echo "First arg: $MX_ARG_0"
echo "Second arg: $MX_ARG_1"
```

Environment variables and working directory

Like just, you can set environment variables and a working directory for a task at invocation time, with no declaration needed in the Markdown file:

# Set one or more environment variables (repeatable)
mq-task deploy --env REGION=eu --env DEBUG=1

# Run the task's commands in a specific working directory
mq-task deploy --dir ../other-project

# Combine both
mq-task run deploy --env REGION=eu --dir ../other-project

--env KEY=VALUE variables are available in the task's shell alongside MX_ARGS/MX_PARAM_*. --dir applies to every command in the task (and its dependencies) for that run. Both flags are available on the shorthand form, run, and tui.

You can also declare default environment variables directly in a task's meta block:

## deploy

```meta
env = ["REGION=staging", "LOG_LEVEL=info"]
```

```bash
echo "deploying to $REGION ($LOG_LEVEL)"
```

A CLI --env flag of the same name overrides the meta default for that run:

mq-task deploy                    # REGION=staging
mq-task deploy --env REGION=prod  # REGION=prod

A working directory can be declared the same way with dir (relative paths resolve against the directory mq-task was invoked from):

## deploy

```meta
dir = "services/api"
```

```bash
pwd  # services/api
```

The CLI --dir flag overrides a meta-declared dir for that run:

mq-task deploy                        # runs in services/api
mq-task deploy --dir services/worker  # runs in services/worker instead

Document-wide defaults

A meta block placed before the first heading in the file applies to every task, not just one. It's useful for defaults you'd otherwise have to repeat in each task's own meta block:

```meta
env = ["REGION=staging", "LOG_LEVEL=info"]
dir = "services/api"
```

## deploy

```bash
echo "deploying to $REGION ($LOG_LEVEL) from $(pwd)"
```

## build

```bash
echo "building in $(pwd)"
```

Both tasks inherit REGION, LOG_LEVEL, and dir from the document-wide block. Precedence, from lowest to highest, is: document-wide meta → task's own meta → CLI --env/--dir. A task's meta only needs to declare the keys it wants to change:

```meta
env = ["REGION=staging"]
```

## deploy

```meta
env = ["REGION=prod"]
```

```bash
echo "$REGION"   # prod (the task's own meta wins)
```

## build

```bash
echo "$REGION"   # staging (falls back to the document-wide default)
```

Task dependencies

You can declare dependencies between tasks using a meta code block (TOML format). When a task is run, its dependencies are automatically executed first in the correct order.

## format

```bash
cargo fmt
```

## lint

```meta
depends = ["format"]
```

```bash
cargo clippy
```

## test

```meta
depends = ["lint"]
```

```bash
cargo test
```

Running mq-task test will automatically execute format → lint → test in order:

▶ test
↳ format (dependency)
...
↳ lint (dependency)
...
(test output)
  • Multiple dependencies are supported: depends = ["format", "lint"]
  • Shared dependencies are executed only once even if multiple tasks depend on them
  • Circular dependencies are detected and reported as an error

Parallel execution

By default a task's dependencies run one after another in the order they are declared. Set parallel = true to run them concurrently; the task itself starts only after all of them have finished (a fan-out / fan-in):

## ci

```meta
depends = ["lint", "test", "build"]
parallel = true
```

```bash
echo "all checks passed"
```
  • Only the real dependency edges constrain the order, so a task starts as soon as its own dependencies are done. Tasks that depend on each other (test depends on build) still run in order
  • Output lines are prefixed with the task name (lint │ ...) and each task reports ✔/✘ with its duration
  • By default the first failure aborts the run: running tasks are stopped (including processes they started) and nothing new begins. Use --keep-going/-k to let independent tasks finish; tasks that depend on a failed task are not run
  • --parallel runs every dependency graph concurrently regardless of parallel in the file, and -j N/--jobs N limits how many tasks run at once (-j 1 forces sequential execution). The default limit is the number of CPUs
  • In parallel runs stdin is closed for tasks; interactive tasks should run sequentially. Dry-run is always sequential

To run several tasks at once, define an aggregate task that depends on them (as ci above).

Conditions

if skips a task when its condition is false. A skipped task does not block the tasks that depend on it.

## deploy

```meta
if = 'flag("CI") && os() == "linux"'
```

```bash
./deploy.sh
```
Expression True when
flag("NAME") the variable is set and not empty, 0 or false
getenv("NAME") == "x", getenv("NAME") != "x" the variable equals / differs from x
os() == "linux" the OS is linux, macos or windows
exists("path") the path exists (relative to the task's working directory)
which("cmd") cmd is on PATH
!expr, a && b, a || b boolean combinations; parentheses group

Variables are looked up in --env, the task's meta/params, then the process environment.

A condition is an mq expression that must evaluate to true or false, so anything else mq offers (string functions, contains, ...) works too. Use getenv/flag rather than mq's built-in env(), which only sees the process environment.

Retries and timeouts

## flaky-integration

```meta
retries = 3          # up to 4 attempts in total
retry_delay = "2s"   # default: 1s
timeout = "5m"       # per attempt; exit code 124 when exceeded
```

```bash
./run-integration-tests.sh
```

Durations accept ms, s, m and h (a bare number is seconds). Only runtime failures are retried; missing runtimes or parameters fail immediately. A task with a timeout runs in its own process group so the whole process tree is stopped; for that reason it cannot read from the terminal.

Passing output between tasks

Set capture = true to expose a task's stdout (trailing newline removed) to the tasks that depend on it as MX_OUTPUT_<TASK> (the title upper-cased, other characters replaced with _). The output is still printed.

## version

```meta
capture = true
```

```bash
git describe --tags
```

## release

```meta
depends = ["version"]
```

```bash
echo "releasing $MX_OUTPUT_VERSION"
```

Captured output is available to the task's direct and transitive dependents. When a task captures its output, stdout is piped, so programs may stop colouring it.

Importing tasks from other files

List files in the document-wide meta block (before the first heading). Their tasks become available as <file name>:<task>:

```meta
imports = ["ci/checks.md", "deploy.md"]
```

## release

```meta
depends = ["checks:lint", "deploy:staging"]
```
  • Paths are relative to the importing file; imports can be nested (a:b:task) and circular imports are rejected
  • Inside an imported file, depends refer to that file's own tasks by their short names
  • The imported file's document-wide env/dir defaults apply to its tasks. Relative dir values resolve against the directory mq-task was invoked from, as usual

Skipping up-to-date tasks and watch mode

Declare the files a task reads with sources (globs). The task is skipped when its sources, code, arguments and environment are unchanged since its last successful run. With outputs, the task also runs if any listed output is missing.

## build

```meta
sources = ["src/**/*.rs", "Cargo.toml"]
outputs = ["target/release/app"]
```

```bash
cargo build --release
```

Fingerprints are stored in .mq-task/cache.json in the directory mq-task runs from (consider git-ignoring .mq-task/). --no-cache ignores them.

--watch/-w re-runs the task whenever any sources of the task or its dependencies, or the task file itself, change:

mq-task test --watch

Dependency graph

mq-task graph ci                     # tree
mq-task graph --format mermaid       # Mermaid diagram of all public tasks
ci [parallel]
├── lint
└── test
    └── build

Interactive TUI

mq-task tui shows a searchable task list with a details pane (description, dependencies, params, options, code).

Key Action
↑/↓, j/k move
/ search (matches titles, aliases, descriptions)
Enter run inside the TUI (prompts for arguments when a param is required)
a run with arguments (name=value, positional; quote values containing spaces)
x leave the TUI and run in the terminal (interactive stdin works)
q/Esc quit

While a task runs, the top pane draws the dependency graph as a left-to-right workflow (one stage per column, each task showing its state ○ pending, spinner running, ✔, ✘, ⊘, ↻ retry and its elapsed time) and the bottom pane the log. Arrow keys (or hjkl) move the selection, v switches to a flat task list, Tab switches between the combined log and the selected task's log, PgUp/PgDn scroll, q/Ctrl+C aborts, and r re-runs. The execution flags (--parallel, -j, -k, --no-cache) apply to the TUI too.

Shell completions

mq-task completions zsh > ~/.zfunc/_mq-task
mq-task completions bash > /etc/bash_completion.d/mq-task   # also completes task names

Named parameters

Declare parameters in the meta block. A bare name is required; name=value sets a default.

## deploy

```meta
params = ["env=staging", "region"]
```

```bash
echo "deploying to $MX_PARAM_ENV / $MX_PARAM_REGION"
```
mq-task deploy -- region=eu           # env falls back to "staging"
mq-task deploy -- prod eu             # positional, filled in declaration order
mq-task deploy -- env=prod region=eu  # named

Parameters are exposed as MX_PARAM_<NAME> environment variables. A required parameter with no value bound is an error.

Aliases and the default task

Give a task alternate names with alias in its meta block:

## build

```meta
alias = ["b"]
```

```bash
cargo build
```

mq-task b now runs build. Set default_task in mq-task.toml to run a task when mq-task is invoked with no task name:

default_task = "build"

Private tasks

A task whose title starts with _, or whose meta block has private = true, is hidden from list/tui output but can still be run directly or used as a dependency:

## _cleanup

```bash
rm -rf tmp/
```

List available tasks

# List tasks from tasks.md, else README.md
mq-task

# List tasks from a specific file
mq-task -f tasks.md
mq-task list --file tasks.md

# Include private tasks
mq-task list --all

Get started

mq-task init            # writes a starter tasks.md
mq-task init --config   # writes mq-task.toml with the default runtimes

mq-task.toml is loaded automatically from the directory of the task file.

Configuration

Create an mq-task.toml file to customize runtime behavior:

# Runtimes configuration
# Simple format: language = "command", execution mode defaults to "file"
[runtimes]
bash = "bash"
sh = "sh"
python = "python3"
ruby = "ruby"
node = "node"
javascript = "node"
js = "node"
php = "php"
perl = "perl"

# Detailed format with execution mode
# Execution modes: "file" (default), "stdin", or "arg"
# - file: write code to a temp file and run it as an argument (keeps stdin interactive)
# - stdin: pipe code into the command's standard input (stdin unavailable to the script itself)
# - arg: pass code as a command-line argument

[runtimes.go]
command = "go run"
execution_mode = "file"

[runtimes.jq]
command = "jq"
execution_mode = "arg"  # jq's filter is a CLI argument, not read from stdin

[runtimes.mq]
command = "mq"
execution_mode = "arg"  # mq uses query as argument

Use execution_mode = "stdin" only when a command needs the code piped in literally (e.g. psql, redis-cli). The task script then can't also read from the terminal.

You can also mix both formats:

[runtimes]
python = "python3"  # Simple format, uses default file mode

[runtimes.go]       # Detailed format with custom execution mode
command = "go run"
execution_mode = "file"
# Using shorthand (from tasks.md by default)
mq-task Build

# From a specific file
mq-task -f tasks.md Build

# Using explicit run command
mq-task run Build
mq-task run --file tasks.md Build

default_task (top-level, outside [runtimes]) sets which task runs when mq-task is invoked with no task name. See Aliases and the default task.

Exit codes

mq-task exits with the same code the task's process exited with, so it composes correctly with &&, CI, and git hooks. A task killed by its timeout exits with 124. When several tasks fail under --keep-going, the first failure's code is used.

License

MIT

About

A task runner that executes code blocks in Markdown files based on section titles.

Topics

Resources

Stars

17 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages