Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

Short answer: read the component failure list before you read anything else, and work out whether you are looking at a component error or a test error, because they have nothing to do with each other. A component error means the metadata would not compile or save. A test error means it was accepted and your Apex tests then failed. Fixing the second when you have the first wastes an afternoon.

Get the full failure list, not the summary

The default terminal output truncates. Ask for everything:

sf project deploy start --source-dir force-app --test-level RunLocalTests --verbose
sf project deploy report --job-id 0Af... --verbose
sf project deploy report --job-id 0Af... --json > failure.json

The job id prints when the deployment starts and is the only thing you need if the terminal session dies. deploy resume reattaches to a running deployment; report pulls the result of a finished one. Pipe to JSON once there are more than a handful of failures, because the human output is not sorted usefully.

The two kinds of failure

In the report, componentFailures and runTestResult are separate sections, and they fail for separate reasons.

A component failure names a file, a line, and a problem. The metadata was rejected, and nothing was saved, because a deployment is one transaction and a single rejected component rolls it all back.

A test failure names a class, a method, and a stack trace. Everything compiled. Your code is wrong, your test data is wrong, or the target org’s configuration breaks an assumption the test makes.

A coverage failure is a third thing dressed as the second. The message reads along the lines of average test coverage across all Apex Classes and Triggers is 71%, at least 75% test coverage is required. No test failed. There are simply not enough covered lines in the org.

The five causes that account for most of it

1. A reference to metadata that is in neither the deployment nor the target. The classic form is a permission set listing a field that does not exist yet, giving something like In field: field - no CustomField named Account.Region__c found. Include the field or trim the reference. Hand-built manifests fail this way far more often than full-directory deployments.

2. Deploy order inside one request. Usually the platform sorts this out. Not always, particularly with record types referencing picklist values and flows referencing Apex in the same payload. Splitting into two deployments is a legitimate answer.

3. Test failures caused by org configuration, not code. A required field added in the target, a validation rule the test data violates, a duplicate rule, an active flow that errors on insert. Ask what exists in production that does not exist in the sandbox where the test passed.

4. Coverage drift. Someone deleted a test, or a class grew and its test did not. The org-wide number gates production, so a deployment can fail on a class untouched for two years.

5. Something is locked. Another deployment is running, a setup change is in flight, or a package install is processing. Nothing to fix. Wait, then retry.

Triage in the order that saves time

  1. Is there a component failure? If yes, ignore the test section entirely. It may not even have run.
  2. Is the same error repeated? One missing field can produce twenty failures. Fix the cause, not the twenty.
  3. Does the error name something you did not change? That points at a gap in the target rather than a fault in your branch.
  4. For a test failure, can you reproduce it in a sandbox? If not, the difference is configuration. Compare both orgs on the object the test touches.
  5. For a coverage failure, get per-class numbers with sf apex run test --code-coverage --result-format human.

Two habits that prevent most of this

Validate before you deploy. --dry-run runs the whole thing, tests included, and changes nothing. It costs the same time as the real deployment and tells you the same answer, which makes skipping it hard to defend.

Better still, validate ahead of the window and quick-deploy. A validation that passed with tests recently can be committed with sf project deploy quick --job-id 0Af..., which skips the test run entirely. That is how a two hour production release becomes a short one.

Frequently asked

Why does the CLI report success when nothing changed? Source tracking decided your files already match the org. sf project deploy preview shows what it thinks; sf project reset tracking clears it.

Can I deploy anyway and fix the failing test later? Not to production. Tests are mandatory there and there is no override.

How do I cancel a slow deployment? sf project deploy cancel --job-id 0Af.... It rolls back cleanly, since nothing was partially applied.