Skip to content

Contributing

Contributions are welcome. Start with the contributor guidelines, which cover commit sign-off (DCO), employer contributions, and how reviews work.

Every PR needs a release-note block in its description (or NONE for changes with no user-facing impact) — CI validates this automatically and comments on the PR if the block is missing or malformed.

Adding a beta feature

When a new capability is not yet ready for all users, gate it behind a Cargo feature and a runtime flag. The full lifecycle:

private branch / draft PR
       ↓
Cargo feature: beta-<name>    ← compiled in, invisible by default
       ↓
mur beta enable <name>        ← user opts into the public beta
       ↓
graduate: remove is_enabled() check, remove #[cfg] guards
       ↓
(optional) remove Cargo feature if it is now core behaviour

Step 1 — Add a Cargo feature

In crates/murmur-cli/Cargo.toml:

[features]
default = []
beta = []
beta-blueprint = ["beta"]   # ← add one line per new feature

Step 2 — Register in the feature list

In crates/murmur-cli/src/beta.rs, add a block to compiled_beta_features():

#[cfg(feature = "beta-blueprint")]
features.push(BetaFeature {
    name: "blueprint",
    description: "Blueprint file support in taskflow stage slots (preview)",
});

Step 3 — Gate the code

Wrap any new commands, handlers, or registrations in #[cfg(feature = "beta-blueprint")]. For runtime visibility, also check the enabled flag before registering subcommands in main.rs:

#[cfg(feature = "beta-blueprint")]
{
    let beta_cfg = load_mur_config().map(|c| c.beta).unwrap_or_default();
    if beta_cfg.is_enabled("blueprint") {
        // register the subcommand
    }
}

Step 4 — Graduate to stable

When the feature is ready for all users:

  1. Remove the #[cfg(feature = "beta-blueprint")] guards from main.rs and the command file.
  2. Remove the is_enabled("blueprint") check — always register the subcommand.
  3. Remove the entry from compiled_beta_features() in beta.rs.
  4. Optionally remove the beta-blueprint Cargo feature (keeping it as a no-op is harmless).

Running tests

Run the unit tests plus the integration suites for the area you touched before submitting:

cargo test --workspace --lib --bins     # unit tests across all crates
cargo test -p murmur-cli --test build   # one integration suite (see crates/murmur-cli/tests/)
cargo test --workspace --no-fail-fast   # the full suite

Run the full suite with --no-fail-fast. Plain cargo test stops at the first test binary that fails and never runs the binaries after it, and its output reports the failure without saying what it skipped — murmur-cli alone has over a hundred integration-test binaries. The last lines of a run tell the two apart:

The run ends with Meaning
error: N targets failed: (error: 1 target failed: for one) and one `-p <crate> --test <name>` line per failed target Every test binary ran; that list is the whole set of failures
error: test failed, to rerun pass … with no targets failed list The run stopped at that binary; the binaries after it never ran

To count how many binaries a stopped run never reached, compare the binaries a full run executes with the ones the stopped run started, using the same package selection as the stopped run. Cargo prints its Running lines to standard error, so save a run with 2>&1, for example cargo test --workspace 2>&1 | tee stopped-run.log:

cargo test --workspace --no-run 2>&1 | grep -c '^ *Executable'   # binaries a full run executes
grep -c '^ *Running' stopped-run.log                               # binaries the stopped run started

The difference is the number of binaries the stopped run did not reach.

Every PR that changes behavior must include tests — see the testing section of the contributor guidelines for where tests go and how much coverage is expected. No test reads a default-artifacts checkout. A contract that repository owns belongs in a test there, where the change that breaks it is the change that reddens it.

CI runs the full workspace suite, including both beta CLI surfaces, on every push and pull request. Tests that need a host able to isolate a capsule — a delegated cgroup v2 scope, or a capsule network namespace — skip themselves with a [SKIP-HOST]-prefixed line instead of failing, since a CI runner provides neither; the job's step summary reports how many tests were skipped for that reason and points at docs/content/reference/resource-limits-manual-verification.md, which covers them by hand.

Changing a WIT interface

The repository commits compiled WASM components (test fixtures and the escape-conformance driver), each built against a WIT directory under crates/capsule-runtime/wit/. A component built against an interface version the tree no longer declares stops loading, so a version bump has to rebuild every component that uses the bumped package. components.list names each committed component, the WIT directory it builds against, and the crate it is built from.

Run these from the repository root:

Command What it does
scripts/rebuild-components.sh --check Writes nothing. Names every component whose murmur:* interface versions differ from the ones its WIT directory declares, with the command that rebuilds it, and exits 1 if any differ
scripts/rebuild-components.sh <name> Rebuilds one component from source and copies it over its committed file
scripts/rebuild-components.sh Rebuilds every component the list does not mark frozen:

Rebuilding needs the wasm32-wasip2 target (rustup target add wasm32-wasip2). The check does not need it. Commit the files a rebuild rewrites. A rebuild can change a component's bytes without changing its interfaces, for example when you build with a different Rust toolchain, so to keep the diff small, commit only the components --check named before the rebuild.

The same check runs in cargo test --workspace, and as the wit-components CI job on any pull request that changes crates/capsule-runtime/wit/. Both also fail on a committed .wasm under crates/ that no line in components.list names, so a new committed component needs a line there. The columns are described at the top of the file. The full bump procedure, including the default-artifacts rebuild, is in wit/VERSIONING.md.

Optional allocator features

A default build of mur uses the system allocator. Two optional features on murmur-cli swap in a different one, and a third builds the tool that compares them:

Feature Effect
jemalloc mur allocates through jemalloc
mimalloc mur allocates through mimalloc, unless jemalloc is also on, which wins
alloc-bench Builds the mur-alloc-bench binary, which times component compilation and the runtime's per-session allocation churn

Every build of mur already needs a C compiler, because some of its dependencies ship C and assembly. jemalloc adds one more tool: it configures and builds jemalloc from source, so --features jemalloc and any --all-features build also need make on PATH.

To compare the three allocators on your own machine:

scripts/alloc-bench.sh                      # 15 rounds, ~10 minutes with a warm target directory
scripts/alloc-bench.sh --cpu 3 --rounds 21  # different core, more rounds

The script builds all three configurations into separate target directories, runs them one round each per pass pinned to a single core with taskset, and prints a table of median component compile time and median per-session allocation time with the per-round spread. Raw per-round figures are left at target/alloc-bench/rounds.tsv. The measurement resolves differences of a few percent, so run it on an otherwise idle machine: when the system row's own min/max spans more than about 10%, the run is measuring the scheduler rather than the allocator.

Formatting and lints

CI also runs a lint job on every push and pull request:

cargo fmt --all --check
cargo clippy --workspace --all-targets --all-features -- -D warnings

Run both locally before submitting a PR. --all-features includes the beta CLI surfaces (topology_cmd, deploy_cmd) in the clippy pass, so a change gated behind a beta feature is still checked; it also turns on the allocator features above. An #[allow(...)] is acceptable when the lint's default judgment is wrong at that specific site, but it needs a comment saying why — a bare #[allow(...)] with no justification, or a crate-level #![allow(...)], will not pass review.