Skip to content

CI Troubleshooting

Common CI failure patterns and fixes for rust-template. Use this runbook when a workflow fails on a pull request or push to main.


  1. Go to Actions: https://github.com/zircote/rust-template/actions
  2. Click the failed workflow run
  3. Click the failed job (red X)
  4. Expand the failed step to see the full log
  5. Click “Search logs” (magnifying glass) to search for error messages
Terminal window
# List recent failed runs
gh run list --status failure --limit 10
# View a specific run
gh run view <run-id>
# View logs for failed steps only
gh run view <run-id> --log-failed
# Download full logs
gh run view <run-id> --log > ci-logs.txt
# Re-run failed jobs
gh run rerun <run-id> --failed
# Re-run the entire workflow
gh run rerun <run-id>

Most CI failures can be reproduced locally with the same commands CI uses:

Terminal window
# Run the full CI check suite (matches ci-checks.yml)
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features --verbose
cargo doc --no-deps --all-features
cargo deny check
# MSRV check (requires the specific toolchain)
rustup install 1.92
cargo +1.92 check --all-features

Workflow: ci-checks.yml (clippy job) Command: cargo clippy --all-targets --all-features -- -D warnings

LintCauseFix
clippy::unwrap_usedCalled .unwrap()Use ?, .unwrap_or(), or if let
clippy::expect_usedCalled .expect()Use ? with proper error types
clippy::panicUsed panic!() in library codeReturn Result or Option
clippy::todoLeft todo!() in codeImplement the function or remove it
clippy::dbg_macroLeft dbg!() in codeRemove it or use proper logging
clippy::print_stdoutUsed println!()Use a logging crate or remove
clippy::needless_pass_by_valueFunction takes ownership unnecessarilyChange parameter to &T
clippy::redundant_cloneUnnecessary .clone()Remove the clone
clippy::missing_errors_docPublic function returns Result without # Errors docAdd # Errors section to doc comment
clippy::missing_panics_docFunction can panic without # Panics docAdd doc or remove the panic
clippy::must_use_candidateReturn value should have #[must_use]Add the attribute
Terminal window
# Run clippy and see all warnings
cargo clippy --all-targets --all-features -- -D warnings
# Auto-fix what clippy can fix
cargo clippy --fix --all-targets --all-features
# Check a specific file
cargo clippy --all-features -- -D warnings 2>&1 | grep "crates/myfile.rs"

Only suppress lints when there is a genuine reason. Add an inline allow with an explanation:

#[allow(clippy::too_many_arguments)] // Builder pattern requires all fields
fn create_widget(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32, g: u32) -> Widget {
// ...
}

Never add blanket allows in lib.rs or Cargo.toml to silence clippy globally.


Workflow: ci-checks.yml (fmt job) Command: cargo fmt --all -- --check

Terminal window
# Format all code
cargo fmt --all
# Check what would change (without modifying)
cargo fmt --all -- --check
# Format a specific file
rustfmt crates/lib.rs

Set up your editor to format on save:

VS Code (.vscode/settings.json — already configured in this repo):

{
"editor.formatOnSave": true,
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
}
}

Neovim (with rust-analyzer):

vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*.rs",
callback = function() vim.lsp.buf.format() end,
})

This project uses rustfmt.toml (or settings in Cargo.toml). Key settings:

  • Line length: 100 characters
  • Edition: 2024

If cargo fmt produces unexpected results, check that your local Rust toolchain matches the stable channel used in CI.


Workflow: ci-checks.yml (msrv job) Command: cargo +1.92 check --all-features MSRV: Rust 1.92

CauseExampleFix
Used a newer language featurelet-chains (1.87+)Check which version stabilized the feature
Dependency bumped its MSRVserde requires newer RustPin the dependency to an older version
Used a newer std APIstd::io::IsTerminal (1.70+)Use a polyfill or feature-gate
Terminal window
# Install and test with the exact MSRV
rustup install 1.92
cargo +1.92 check --all-features
# Check what version stabilized a feature
# Look at the Rust release notes or use:
rustup doc --std
  1. If you used a newer feature: Rewrite to use MSRV-compatible syntax
  2. If a dependency requires newer Rust: Pin to the last compatible version in Cargo.toml:
    [dependencies]
    some-crate = ">=1.0, <1.5" # 1.5 requires Rust 1.93+
  3. If MSRV needs to be bumped: Update rust-version in Cargo.toml, update the MSRV check in ci-checks.yml, and note it in the changelog as a potentially breaking change

Workflow: ci-checks.yml (deny job) Command: cargo deny check Configuration: deny.toml

error[vulnerability]: RUSTSEC-2024-XXXX: <crate> - <description>

Fix: Update the affected dependency:

Terminal window
cargo update -p <crate-name>
cargo deny check advisories

If no fix is available, add a temporary exception in deny.toml:

[advisories]
ignore = [
"RUSTSEC-2024-XXXX", # No fix available; not exploitable in our usage. Revisit by YYYY-MM-DD.
]
error[rejected]: license 'GPL-3.0' is not in the allow list

Fix options:

  1. Remove the dependency and find an alternative with a permissive license
  2. If the license is acceptable, add it to the allow list in deny.toml:
    [licenses]
    allow = [
    # ...existing...
    "NEW-LICENSE-SPDX",
    ]
error[banned]: crate 'openssl' is banned

Fix: The crate (or one of its transitive dependencies) pulls in a banned crate. Check the dependency tree:

Terminal window
cargo tree -i openssl

Then either:

  • Use an alternative crate that does not depend on the banned one
  • Enable a feature flag that uses a different backend (e.g., rustls instead of openssl)
error[unknown-registry]: crate 'foo' sourced from unknown registry

Fix: Only crates from crates.io are allowed. If you need a git dependency, add it to deny.toml:

[sources]
allow-git = ["https://github.com/owner/repo"]
Terminal window
cargo deny check advisories
cargo deny check licenses
cargo deny check bans
cargo deny check sources

Workflow: ci-checks.yml (test job, runs on ubuntu, macos, windows) Command: cargo test --all-features --verbose

Terminal window
# Run all tests with output
cargo test --all-features -- --nocapture
# Run a specific test
cargo test test_function_name
# Run tests in a specific module
cargo test module_name::
# Run tests matching a pattern
cargo test --all-features -- --test-threads=1 pattern
# Run only integration tests
cargo test --test integration_test
# Run only doc tests
cargo test --doc
# Show which tests are running
cargo test --all-features -- --list

If tests pass on one OS but fail on another:

SymptomCommon causeFix
Path separator issuesHardcoded / or \Use std::path::PathBuf
Line ending issues\n vs \r\nNormalize with .trim() or use \r?\n in regex
Temp directory issuesDifferent temp path formatsUse std::env::temp_dir()
File permission issuesUnix permissions on WindowsFeature-gate Unix-specific code

If a test passes sometimes and fails sometimes:

  1. Run it many times: for i in $(seq 100); do cargo test test_name || break; done
  2. Check for race conditions in async code
  3. Check for reliance on system time
  4. Check for reliance on HashMap iteration order

Workflow: quality-gates.yml (the sast job; SAST runs via the central reusable-sast-codeql.yml) Schedule: Weekly (Monday 06:00 UTC) + pushes to main

FindingDescriptionFix
Uncontrolled format stringUser input in format!()Sanitize or use {} placeholder
Path traversalUser input in file pathsValidate and canonicalize paths
Command injectionUser input in shell commandsAvoid shell; use Command::new()
Integer overflowUnchecked arithmeticUse .checked_add() or wrapping_add()

CodeQL uses the cpp extractor for Rust, which can produce false positives. To dismiss:

  1. Review the finding in Security > Code scanning alerts
  2. If it is a false positive, click “Dismiss alert” and select a reason
  3. Add a comment explaining why it is not a real issue
Terminal window
# Install CodeQL CLI: https://github.com/github/codeql-cli-binaries
codeql database create my-db --language=cpp --command="cargo build --all-features"
codeql database analyze my-db --format=sarif-latest --output=results.sarif

Workflow: release.yml Trigger: Push of v*.*.* tag

ErrorCauseFix
Cargo.toml version mismatchVersion in Cargo.toml does not match the tagEnsure version = "X.Y.Z" matches tag vX.Y.Z
no [[bin]] target found in Cargo.tomlThe meta job resolves the binary name from cargo metadataEnsure Cargo.toml declares a [[bin]] target
error[E0658]: feature not availableTarget requires newer RustCheck MSRV compatibility for all targets
macos-amd64 build failsCross-target build (x86_64-apple-darwin on macos-latest)Check the targets: input on the toolchain step; all other legs build natively

If the release is created but some binaries are missing:

Terminal window
# Check which assets exist
gh release view vX.Y.Z
# Re-run just the failed matrix job
gh run rerun <run-id> --failed

crates.io publishing (publish.yml) uses Trusted Publishing (OIDC) — there is no registry token secret. If authentication fails with “No Trusted Publishing config found”, complete the one-time setup on crates.io: crate Settings > Trusted Publishing > add this repo with workflow publish.yml and environment copilot.

Terminal window
# Verify secrets are configured (you can't read them, just confirm they exist)
gh secret list

Secrets and permissions involved:

  • HOMEBREW_TAP_TOKEN for Homebrew formula pushes (package-homebrew.yml)
  • id-token: write and attestations: write job permissions for attestation steps
  • GITHUB_TOKEN is automatic

Workflow: docker.yml Trigger: Push to main, tag push, or PR (build-only)

ErrorCauseFix
failed to solve: Dockerfile: not foundMissing DockerfileEnsure Dockerfile exists in repo root
COPY failed: file not foundWrong path in DockerfileCheck paths match crates/ structure
denied: denied on pushMissing permissionsVerify packages: write in workflow and “Read and write permissions” in repo settings
ERROR: Multiple platforms not supportedBuildx not configuredThe workflow sets up Buildx; check that step
Build arg mismatchRUST_VERSION arg wrongCheck build-args in the workflow matches a valid Rust version
Terminal window
# Build for current platform only
docker build -t rust-template:test .
# Run the test image
docker run --rm rust-template:test --version
# Build for multiple platforms (requires buildx)
docker buildx build --platform linux/amd64,linux/arm64 -t rust-template:test .

When Dependabot PRs have merge conflicts in Cargo.lock:

Close and reopen the PR to trigger Dependabot to rebase:

Terminal window
gh pr close <number>
gh pr reopen <number>

Or comment on the PR:

Terminal window
gh pr comment <number> --body "@dependabot rebase"
Terminal window
# Checkout the Dependabot branch
gh pr checkout <number>
# Merge main into it
git merge main
# Resolve Cargo.lock conflicts by regenerating it
git checkout --theirs Cargo.lock
cargo update
# Or if specific crate needs updating
cargo update -p <crate-from-dependabot-pr>
# Commit and push
git add Cargo.lock
git commit -m "chore: resolve merge conflict in Cargo.lock"
git push

Comment these on any Dependabot PR:

CommandEffect
@dependabot rebaseRebase the PR on the latest base branch
@dependabot recreateClose and recreate the PR from scratch
@dependabot mergeMerge the PR (if CI passes)
@dependabot squash and mergeSquash merge the PR
@dependabot cancel mergeCancel a pending auto-merge
@dependabot ignore this major versionStop updates for this major version
@dependabot ignore this dependencyStop all updates for this dependency

Workflow: ci-checks.yml (doc job) Command: cargo doc --no-deps --all-features Environment: RUSTDOCFLAGS="-D warnings"

ErrorCauseFix
unresolved linkBroken intra-doc link [Type]Fix the type name or path in the doc comment
missing documentationPublic item without /// docAdd documentation to the public item
private item in public docDoc references a private typeMake the type public or remove the reference
broken code exampleDoc test does not compileFix the code in the ```rust block
Terminal window
# Build docs with warnings as errors (matches CI)
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
# Open the docs in a browser
cargo doc --open
# Run only doc tests
cargo test --doc

Workflow: pipeline.yml (coverage job) — this job does not block merging (fail_ci_if_error: false on Codecov upload)

If coverage generation fails:

Terminal window
# Install cargo-llvm-cov
cargo install cargo-llvm-cov
# Generate coverage locally
cargo llvm-cov --all-features --lcov --output-path lcov.info
# Generate an HTML report
cargo llvm-cov --all-features --html
open target/llvm-cov/html/index.html

CI JobLocal equivalentBlocks merge?
Formatcargo fmt --all -- --checkYes
Clippycargo clippy --all-targets --all-features -- -D warningsYes
Test (ubuntu)cargo test --all-features --verboseYes
Test (macos)Same (run on macOS)Yes
Test (windows)Same (run on Windows)Yes
DocumentationRUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-featuresYes
Cargo Denycargo deny checkYes
MSRVcargo +1.92 check --all-featuresYes
Coveragecargo llvm-cov --all-featuresNo
CodeQLBuild + static analysisNo (scheduled)
Security Auditcargo audit --deny warningsNo (scheduled)
Terminal window
cargo fmt --all -- --check \
&& cargo clippy --all-targets --all-features -- -D warnings \
&& cargo test --all-features --verbose \
&& RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features \
&& cargo deny check \
&& cargo +1.92 check --all-features