# `Sigra.Upgrade`
[🔗](https://github.com/sztheory/sigra/blob/v1.20.0/lib/sigra/upgrade.ex#L1)

Orchestrator for `mix sigra.upgrade` (Phase 18 D-08).

Responsibilities:

  * Refuse dirty git working trees unless `--allow-dirty`
  * Detect source/target schema versions; refuse downgrades;
    short-circuit when already at the target version
  * Compute the plan (files to create, injections to apply,
    migrations to emit)
  * Interactive confirmation when `--yes` is not set
  * Emit the three-section stdout summary
    (`Applied` / `Pending` / `Next steps`)
  * Inject/update the Sigra schema version sentinel in
    `config/config.exs`
  * Optionally emit the personal-orgs data migration shim when
    `--backfill-personal-orgs` is passed

All file mutation flows through `Sigra.Install.Injector` and
`EEx.eval_file/2` so upgrade output stays byte-compatible with the
`Sigra.Install.Runner` walker.

## Zero-org installs (BLOCKER 1)

An upgrade run against an app that installed with
`--no-organizations` must NOT emit the `organizations` ALTER
migrations — the table doesn't exist. Detection is cheap and
pre-Ecto-connect: we scan `priv/repo/migrations/` for any file
whose body contains `create table(:organizations`. No schema file
→ no ALTERs → no backfill.

# `opts`

```elixir
@type opts() :: keyword()
```

Validated opts from Mix.Tasks.Sigra.Upgrade (after NimbleOptions)

# `plan`

```elixir
@type plan() :: %{
  source: String.t(),
  target: String.t(),
  files: [term()],
  injections: [Sigra.Install.Injection.t()],
  migrations: [{String.t(), String.t()}]
}
```

# `run`

```elixir
@spec run(opts()) :: :ok | {:halt, term()}
```

Top-level entrypoint called from the `sigra.upgrade` mix task.

Returns `:ok` on success, may halt via `System.halt/1` on user
dissent at the interactive confirmation prompt, and raises via
`Mix.raise/1` on fatal refusals (dirty tree, downgrade attempt).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
