Skip to content

feat(cli): ferrflow why <package> — explain bump/no-bump decision #501

Description

@BryanFRD

Problem

When a monorepo release runs and a package doesn't get bumped, debugging is opaque. Was it not touched? Did the commits not classify as bumping? Was a shared_path matched/missed? The diagnostic info exists during the run (logged at `verbose`) but is hard to reproduce.

Proposal

`ferrflow why ` outputs a structured explanation for a single package:

```
Package: api
Path: services/api
Last tag: api@v1.2.3 (commit abc1234, 2 days ago, reachable from HEAD ✓)
Strategy: SemverStrict

Commits since api@v1.2.3 (4):
abc1234 feat(api): add events endpoint → MINOR
def5678 fix(deps): bump tokio → no-bump (does not touch services/api or shared_paths)
9ab0cde chore(api): update README → no-bump (chore: doesn't bump)
fedc234 feat(core)!: breaking core change → cascade via dependsOn: [core]

Files touched in considered commits:
services/api/src/events.rs → match "services/api/"
Cargo.lock → no match (not in path, not in shared_paths)
services/api/README.md → match
packages/core/src/lib.rs → no match for api, but triggers cascade via dependsOn

Decision: MINOR bump (1.2.3 → 1.3.0)
```

`--json` for machine consumption.

Implementation

Most of this data is already computed in `src/monorepo/run/mod.rs::run_release_logic`. Extract the decision tracing into a typed structure (`PackageDecision { reasons: Vec<...> }`) and emit it. New `Commands::Why { package: String, format: OutputFormat }` calling into the same orchestrator with a "dry-run + explain" flag.

Why this is high-value

  • Top question users will have on first failed release. Without this, every "why didn't my package bump?" support thread requires the user to enable verbose, paste 200 lines, and hope someone matches the right rule.
  • Differentiates from changesets/release-please which both have similarly opaque skip logic.
  • Cost is small: the logic already exists, this is mostly serialization.

Test plan

  • Fixture: package not touched → why explains shared_paths mismatch
  • Fixture: prerelease branch with non-prerelease commit → why explains strategy decision
  • Fixture: orphaned tag → why explains the OrphanedTagStrategy choice

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementImprovement to existing feature

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions