How to Troubleshoot a Failing mif-rs CI Run
How to Troubleshoot a Failing mif-rs CI Run
Section titled “How to Troubleshoot a Failing mif-rs CI Run”Diagnose and fix a failing workflow run on a pull request or push to main
in the mif-rs workspace, and reproduce the failure locally before pushing a
fix.
Prerequisites
Section titled “Prerequisites”ghCLI authenticated againstmodeled-information-format/mif-rs.- A local clone with the same Rust toolchain CI uses (
stable, plus1.95for MSRV checks:rustup install 1.95). justinstalled (the local task runner; runjustwith no arguments to list every recipe).
Step 1 — Read the failing run
Section titled “Step 1 — Read the failing run”gh run list --status failure --limit 10gh run view <run-id>gh run view <run-id> --log-failedgh run view <run-id> --log > ci-logs.txt # full logs, if you need more contextOr in the browser: Actions → the failed run → the failed job (red X) → expand the failed step → “Search logs” to jump to the error.
To retry without changing anything (useful for a suspected flake):
gh run rerun <run-id> --failedgh run rerun <run-id> # re-run the entire workflowStep 2 — Reproduce locally
Section titled “Step 2 — Reproduce locally”mif-rs is orchestrated top-level by pipeline.yml, which calls
ci-checks.yml (fmt, clippy, test, doc, deny, msrv), ci-coverage.yml
(coverage), and the org’s pin-check / validate-workflows (actionlint)
reusable workflows. Reproduce the core gate locally in one command:
just check # = fmt-check + lint + test + doc-build + deny (matches CI, minus msrv)just msrv # separate recipe: cargo +1.95 check --all-featuresRaw cargo equivalents, if you need to run one check in isolation:
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features -- -D warningscargo test --workspace --all-features --verbosecargo doc --workspace --no-deps --all-featurescargo deny checkcargo +1.95 check --all-featuresClippy failures
Section titled “Clippy failures”Workflow: pipeline.yml → ci-checks.yml (clippy job)
Command: cargo clippy --all-targets --all-features -- -D warnings
The workspace runs clippy’s pedantic + nursery + cargo lint groups
([workspace.lints] in the root Cargo.toml), so failures are often stricter
than clippy’s defaults. A subset is denied as hard errors regardless of -D warnings, because they’re incompatible with library code in this workspace:
| Lint | Cause | Fix |
|---|---|---|
clippy::unwrap_used |
Called .unwrap() outside #[cfg(test)] |
Use ? or an explicit match |
clippy::expect_used |
Called .expect() outside #[cfg(test)] |
Use ? or an explicit match |
clippy::panic |
Used panic!() in library code |
Return Result instead |
clippy::todo / clippy::unimplemented |
Placeholder code | Implement it or remove it |
clippy::dbg_macro |
Left dbg!() in code |
Remove it |
clippy::print_stdout / clippy::print_stderr |
Used println!/eprintln! in library code |
Use logging; mif-cli/mif-mcp exempt themselves with #![allow(...)] at the crate root since a CLI/server needs to print |
clippy::missing_errors_doc / missing_panics_doc |
(allowed workspace-wide — opt-in only) | N/A |
#[cfg(test)] code is exempt from unwrap_used/expect_used/dbg_macro/
print_stdout via clippy.toml’s allow-*-in-tests settings — use plain
.unwrap() in tests, not .unwrap_or_default() workarounds.
cargo clippy --workspace --all-targets --all-features -- -D warnings # see all warningscargo clippy --workspace --fix --all-targets --all-features # auto-fix what it canOnly suppress a lint with a genuine reason, inline, with a comment — never a
blanket allow in a crate root or Cargo.toml:
#[allow(clippy::too_many_arguments)] // Builder pattern requires all fieldsfn create_widget(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32, g: u32) -> Widget { // ...}Format failures
Section titled “Format failures”Workflow: pipeline.yml → ci-checks.yml (fmt job)
Command: cargo fmt --all -- --check
cargo fmt --all # fixcargo fmt --all -- --check # check without modifyingConfigured in rustfmt.toml (workspace root): max_width = 100, edition = "2024", Unix newlines, reorder_imports/reorder_modules both on. If
cargo fmt produces unexpected results, confirm your local toolchain matches
CI’s stable channel — the unstable options at the bottom of rustfmt.toml
are commented out and only apply under cargo +nightly fmt.
MSRV failures
Section titled “MSRV failures”Workflow: pipeline.yml → ci-checks.yml (msrv job)
Command: cargo check --all-features on toolchain 1.95
MSRV: Rust 1.95 (rust-version.workspace = true in every crate)
rustup install 1.95cargo +1.95 check --all-features # matches `just msrv`| Cause | Fix |
|---|---|
| Used a language feature newer than 1.95 | Rewrite with MSRV-compatible syntax, or check which version stabilized it |
| A dependency bumped its own MSRV | Pin it to the last compatible version in the relevant crate’s Cargo.toml |
| Used a std API newer than 1.95 | Use a polyfill or feature-gate it |
If the MSRV genuinely needs to move: update rust-version in
[workspace.package] (Cargo.toml), update the msrv input default in
ci-checks.yml, and call it out in the changelog as a potentially breaking
change.
cargo-deny failures
Section titled “cargo-deny failures”Workflow: pipeline.yml → ci-checks.yml (deny job)
Command: cargo deny check
Configuration: deny.toml (workspace root)
cargo deny check advisories # RustSec DB — all advisory types deniedcargo deny check licenses # SPDX allow-list onlycargo deny check bans # openssl, atty deniedcargo deny check sources # crates.io onlyAdvisory failure (error[vulnerability]: RUSTSEC-2024-XXXX): update the
crate (cargo update -p <crate-name>), or — if no fix exists — add a dated,
commented exception to deny.toml’s [advisories] ignore.
License failure (error[rejected]: license 'X' is not in the allow list): either drop the dependency, or add the SPDX identifier to
[licenses] allow if it’s genuinely acceptable — see
DEPENDENCY-UPDATES.md for the current allow-list.
Ban failure (error[banned]: crate 'openssl' is banned): find what pulls
it in (cargo tree -i openssl), then either switch to an alternative that
doesn’t depend on it, or enable a rustls feature flag on the offending
crate instead.
Source failure (error[unknown-registry]: ...): only crates.io is
allowed. A needed git dependency must be added to deny.toml’s
[sources] allow-git.
Test failures
Section titled “Test failures”Workflow: pipeline.yml → ci-checks.yml (test job, matrix:
ubuntu-latest, macos-latest, windows-latest)
Command: cargo test --all-features --verbose
cargo test --workspace --all-features -- --nocapture # show outputcargo test -p <crate> test_name # one testcargo test --workspace -- --test-threads=1 pattern # serialize + filtercargo test --doc # doc tests onlyIf a test passes on one OS but fails on another, check for: hardcoded / or
\ path separators (use std::path::PathBuf), \n vs \r\n assumptions,
temp-directory path format differences (std::env::temp_dir()), or Unix
permission checks that need feature-gating on Windows.
For a flaky test, run it repeatedly before assuming it’s a real regression:
for i in $(seq 100); do cargo test test_name || break; doneDocumentation failures
Section titled “Documentation failures”Workflow: pipeline.yml → ci-checks.yml (doc job)
Command: cargo doc --no-deps --all-features
Environment: RUSTDOCFLAGS="-D warnings"
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-featurescargo doc --open # view locallycargo test --doc # doc examples compile as doctestsEvery public item requires a doc comment (missing_docs = "warn"
workspace-wide). Common failures: an unresolved intra-doc link (fix the
[Type] reference), a doc referencing a private type (make it pub or
remove the reference), or a doc example that doesn’t compile.
Coverage
Section titled “Coverage”Workflow: pipeline.yml → ci-coverage.yml (coverage job)
Tool: cargo-llvm-cov 0.6.14, workspace-wide, all features
Threshold: the job’s own “Check coverage threshold” step fails (exit 1)
if line coverage drops below 90% — the Codecov upload step’s
fail_ci_if_error: false only means the job won’t fail if the external
Codecov service itself is unreachable, not that the threshold check is
advisory. coverage is a separate workflow from ci-checks.yml and isn’t
part of that workflow’s all-checks-pass aggregate.
cargo install cargo-llvm-covcargo llvm-cov --workspace --all-features --lcov --output-path lcov.infocargo llvm-cov --workspace --all-features --html --output-dir coverage-htmlopen coverage-html/index.htmlcargo llvm-cov --workspace --all-features --summary-onlyCodeQL (SAST) failures
Section titled “CodeQL (SAST) failures”Workflow: quality-gates.yml (sast job, calls the org’s
reusable-sast-codeql.yml)
Trigger: push to main, every PR, weekly (Monday 06:00 UTC)
Language: rust, build-mode: none
Review findings under Security → Code scanning alerts. If a finding is a genuine false positive, dismiss it with a reason and a comment explaining why — don’t silently ignore it.
| Finding class | Fix |
|---|---|
| Uncontrolled format string | Sanitize input or use a {} placeholder, not interpolation |
| Path traversal | Validate and canonicalize user-supplied paths |
| Command injection | Avoid a shell; build the command with Command::new() and explicit args |
| Integer overflow | Use .checked_add() / .wrapping_add() instead of unchecked arithmetic |
pin-check and actionlint failures
Section titled “pin-check and actionlint failures”Workflow: pipeline.yml (pin-check and validate-workflows jobs, both
calling org reusables). Required check contexts: pin-check / pin-check and
validate-workflows / actionlint.
- pin-check fails when any
uses:in.github/workflows/*.ymlisn’t pinned to a full 40-character commit SHA (a version tag or branch ref fails). Resolve the SHA and pin it — see DEPENDENCY-UPDATES.md for the general “update a pinned action” flow. - validate-workflows (
actionlint) fails on workflow YAML syntax or semantic errors — a typo’dneeds:reference, an invalidif:expression, etc. Runactionlintlocally against the changed file to see the same error CI reports.
Docker build failures
Section titled “Docker build failures”Workflow: pipeline.yml (docker job, needs ci + gate, calls
release-docker.yml). Runs on every push to main/master and on tags;
on a PR it builds without pushing (push: ${{ github.event_name != 'pull_request' }}). The job itself is gated on needs.gate.outputs.has-bin-target == 'true' (dynamically resolved from cargo metadata, not hardcoded) —
true from day one here, since mif-cli and mif-mcp both carry [[bin]]
targets — so the docker job always runs; it is not tied to any crate’s
publish status (every workspace crate is already published — see
RELEASING.md).
| Error | Cause | Fix |
|---|---|---|
failed to solve: Dockerfile: not found |
Missing Dockerfile |
Confirm it still exists at the repo root |
COPY failed: file not found |
Wrong path in Dockerfile |
Check paths match the crates/<name>/ layout |
denied: denied on push |
Missing registry permissions | Confirm the job has packages: write (only granted when push: true) |
unknown: BIN / build fails immediately |
Missing --build-arg BIN=<name> |
The Dockerfile requires BIN (mif-cli or mif-mcp) to select which workspace binary to build — no default |
The Dockerfile uses cargo-chef for dependency-layer caching (a chef/
planner/builder multi-stage split): the dependency-only layer is cached
independently of application source changes, so a source-only change
rebuilds in seconds rather than recompiling the whole dependency tree.
release-docker.yml builds linux/amd64 only by default — linux/arm64
(QEMU-emulated, much slower even with caching) is added only for tag pushes
(pipeline.yml’s docker job sets platforms conditionally on
github.ref_type == 'tag').
Test locally:
docker build --build-arg BIN=mif-cli -t mif-cli:test .docker run --rm mif-cli:test --version
# Both platforms, matching what CI builds only for an actual tag push:docker buildx build --platform linux/amd64,linux/arm64 \ --build-arg BIN=mif-cli -t mif-cli:test .Release workflow failures
Section titled “Release workflow failures”Workflow: release.yml, Trigger: push of a v*.*.* tag.
The release matrix builds both mif-cli and mif-mcp across 5 platforms and
publishes all 9 crates (mif-core, mif-problem, mif-schema,
mif-frontmatter, mif-ontology, mif-embed, mif-store, mif-cli,
mif-mcp) independently to crates.io. See
RELEASING.md for the full chain and monitoring steps; the
common failure points are:
| Error | Cause | Fix |
|---|---|---|
Cargo.toml version mismatch |
Workspace version doesn’t match the tag |
Ensure version = "X.Y.Z" in [workspace.package] matches tag vX.Y.Z |
| macos-amd64 build fails | Cross-target leg (x86_64-apple-darwin on macos-latest) |
Check the toolchain step’s targets: input; the other 4 legs build natively |
| Cargo Audit gate fails | Real advisory in Cargo.lock |
Fix via a normal PR first (see DEPENDENCY-UPDATES.md), then restart the release |
| Verify Attestations fails | Missing/unverifiable attestation | The fail-closed gate worked — no release was created. Fix the cause and release with a new tag; never re-run against an existing one |
| crates.io publish fails, “No Trusted Publishing config found” | Trusted Publishing not configured for that crate | One-time setup per crate on crates.io: Settings → Trusted Publishing, workflow publish.yml, environment release |
| Homebrew formula not updated | workflow_run trigger missed, or tap token missing |
gh workflow run package-homebrew.yml -f version=X.Y.Z -f dry_run=false; check HOMEBREW_TAP_TOKEN |
Dependabot merge conflicts
Section titled “Dependabot merge conflicts”When a Dependabot PR conflicts on Cargo.lock, ask it to rebase first:
gh pr comment <number> --body "@dependabot rebase"If that doesn’t clear it:
gh pr checkout <number>git merge maincargo update -p <crate-from-dependabot-pr> # regenerate the lockfilegit add Cargo.lockgit commit -m "chore: resolve merge conflict in Cargo.lock"git pushUseful comment commands: @dependabot rebase, @dependabot recreate,
@dependabot merge, @dependabot cancel merge, @dependabot ignore this major version, @dependabot ignore this dependency.
Quick reference: CI jobs and local equivalents
Section titled “Quick reference: CI jobs and local equivalents”| CI job | Local equivalent | In ci-checks.yml’s all-checks-pass? |
|---|---|---|
| Format | cargo fmt --all -- --check |
Yes |
| Clippy | cargo clippy --workspace --all-targets --all-features -- -D warnings |
Yes |
| Test (ubuntu/macos/windows) | cargo test --workspace --all-features --verbose |
Yes |
| Documentation | RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features |
Yes |
| Cargo Deny | cargo deny check |
Yes |
| MSRV | cargo +1.95 check --all-features |
Yes |
| Coverage | cargo llvm-cov --workspace --all-features (90% threshold) |
No — separate workflow (ci-coverage.yml) |
| CodeQL (SAST) | Static analysis, no local equivalent | No — separate workflow (quality-gates.yml) |
| Security Audit | cargo audit --deny warnings |
No — separate workflow (security-audit.yml) |
| pin-check / actionlint | No local equivalent for pin-check; actionlint for the linter |
No — separate pipeline.yml jobs |
Run the full all-checks-pass set at once:
just check && just msrvEvery job above now passes locally, or you know exactly which one to fix before pushing again.