mando-cli build context filtering (2026-05-06)
mando build mando was hanging on the upload to dockerd because it was tarring the entire workspace — including target/, .git/, and .worktrees/ — into the build context. Same flaw existed in runner.rs::build_context_tar reachable from mando up with a docker target. This note pins the fix: a yaml-driven build.context_includes field plus a single-source-of-truth helper, build_filtered_tar, used by both call sites.
For Agents
Commits:
6b1f7c7(yaml-driven build context) +302be50(defaults shipped for all 5 app services). Files:src/runtime/build_profile.rs, newsrc/runtime/build_context.rs,src/commands/build.rs,src/runtime/runner.rs,src/config/defaults/*.yaml. Limitation:context_includesare literal paths — no glob support. See “Known limitation” below.
Symptoms
mando build mando hung indefinitely (or many minutes, depending on workspace size) on the “uploading build context” step. dockerd was eating gigabytes of target/ artefacts that the Dockerfile never references. Same hang reproduced via mando up <service-with-docker-target> because that path went through DockerServiceRunner::start → build_context_tar with the same naive tar logic.
Root cause
Both commands/build.rs::create_build_context and runtime/runner.rs::build_context_tar were doing essentially append_dir_all(".", workspace_root). There was no respect for .dockerignore, no inspection of the Dockerfile’s COPY directives, and no opt-in include list. So everything in the workspace got tarred up and shipped to dockerd, every build, blocking on I/O proportional to du -sh ..
Fix shape
1. New yaml field — build.context_includes: Vec<String>
In src/runtime/build_profile.rs:
pub struct ServiceBuildDef {
// …
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub context_includes: Vec<String>,
}#[serde(default)] means existing yamls without this field still parse cleanly. skip_serializing_if = "Vec::is_empty" keeps re-serialised configs minimal.
2. New module — src/runtime/build_context.rs
Single source of truth for build-context tar construction:
pub fn build_filtered_tar(
context_dir: &Path,
dockerfile: &str,
includes: &[String],
service_name: &str,
) -> Result<Vec<u8>, …>Algorithm:
- Always include the Dockerfile (whatever
dockerfileresolves to — see below). - If
includes.is_empty()→ emit awarn!log explaining we’re falling back to the whole context, thenappend_dir_all(".", context_dir)for backward compatibility. Transitional fallback only — once every service ships acontext_includes, this branch becomes a hard error. - Otherwise, iterate
includes:- Dedupe against the dockerfile via a
HashSet(socontext_includes: [Dockerfile, mando-bess]doesn’t tar the dockerfile twice). - Warn-skip missing paths. Don’t error on the build — surface the warning, ship the tar without that path, and let the docker build fail naturally if the path was actually required.
- Recurse for directories, single-add for files.
- Dedupe against the dockerfile via a
3. Used by both call sites
Replaces the bespoke tar code in:
src/commands/build.rs::create_build_contextsrc/runtime/runner.rs::DockerServiceRunner::start
Now neither side can drift from the other.
4. Dockerfile-name regression caught and fixed
While unifying the call sites, runner.rs was found to be hard-coding "Dockerfile" as the dockerfile name. With context_includes active, that would have broken any service using a custom dockerfile: field once it tried to tar+build. Fix: read the yaml’s dockerfile field on the service, thread it through to both build_filtered_tar(... dockerfile, ...) AND docker.build_image(... dockerfile, ...). Both consumers now agree on the same name.
5. Loud config-load failures
ServiceConfiguration::load previously swallowed parse errors into an empty Vec. Now: warn!("failed to parse {path}: {err}") then return empty. A user with a typo in their service yaml gets told instead of silently getting “no includes”.
6. Defaults for all 5 app services (commit 302be50)
Shipped context_includes defaults in src/config/defaults/*.yaml for every app service the workspace builds. Each list was derived directly from that project’s actual Dockerfile COPY directives — so a fresh checkout works out of the box without the user authoring service yamls by hand.
Known limitation — no glob support
Literal paths only
context_includesentries are passed straight to the filesystem. They are NOT globs.A Dockerfile that does
COPY ./.env*only gets.envlisted in our defaults. A user who needs.env.local,.env.development, etc. has to extend the list themselves with each filename.If glob support is added later, mind that:
- Globs change the dedup logic (the
HashSet<&str>becomes aHashSet<PathBuf>after expansion).- Globs need to be evaluated relative to
context_dir, not the CWD.- Watch out for
**performance on large workspaces — the whole reason forcontext_includesis to NOT walk the whole tree.
Related
- mando-cli-build-variants-shelved-2026-05-06 — proposed follow-up: profile-driven build variants (dev runtime-only vs release multi-stage). Rests on this commit.
- mando-cli-v2 — current architecture
- mando-cli-docker-lifecycle — DockerClient + ContainerSpec, the lifecycle layer this build path feeds
- mando-cli-mock-down-idempotent-2026-05-06 — sibling fix from the same session
- mando-cli-status-readonly-2026-05-06 — sibling fix from the same session
- Mando