# Authoring an upgrade step

Use an upgrade step for one-time operations that run at deploy time:

- Backfilling new columns on existing rows
- Rewriting JSON blobs to a new schema
- Re-indexing cache keys
- Rotating cryptographic material

**Not for schema changes** — use a normal Laravel migration registered via `HasMigrations`.

## Minimal step

```php
<?php

declare(strict_types=1);

namespace Acme\MyPackage\Upgrade;

use Acme\MyPackage\Models\Widget;
use Capell\Core\Data\UpgradeContext;
use Capell\Core\Support\Upgrade\AbstractUpgradeStep;

class BackfillWidgetSchemaV2 extends AbstractUpgradeStep
{
    public function id(): string
    {
        // Stable forever. Never change. Never reuse.
        return 'acme.backfill-widget-schema-v2';
    }

    public function label(): string
    {
        return 'Backfill Acme widget schema v2';
    }

    public function package(): string
    {
        return 'acme/my-package';
    }

    public function run(UpgradeContext $context): bool
    {
        Widget::query()
            ->whereNull('schema_version')
            ->each(function ($widget): void {
                $widget->update(['schema_version' => 2]);
            });

        return true;
    }
}
```

## Version gating

```php
public function shouldRun(UpgradeContext $context): bool
{
    $current = $context->composerVersion('acme/my-package') ?? '0.0.0';

    return $context->compareVersions($current, '2.0.0') >= 0;
}
```

## Dependencies

```php
public function dependsOn(): array
{
    return ['core.create-widget-schema-version-column'];
}
```

Steps with unsatisfied dependencies are skipped with a clear reason in the log.

## Reversible steps

```php
public function rollback(UpgradeContext $context): bool
{
    Widget::query()
        ->where('schema_version', 2)
        ->each(function ($widget): void {
            $widget->update(['schema_version' => null]);
        });

    return true;
}
```

Invoke: `php artisan capell:rollback --step=acme.backfill-widget-schema-v2`.

## Register the step

In your package service provider `register()`:

```php
$this->app->tag([
    \Acme\MyPackage\Upgrade\BackfillWidgetSchemaV2::class,
], 'capell.upgrade-steps');
```

## Rules

- **Stable id**: never change `id()`; never reuse.
- **Idempotent body**: `run()` must be safe to retry — a failed partial run will be retried next upgrade.
- **DB work only inside the transaction**: `run()` executes inside `DB::transaction()`. Don't do HTTP calls, queue dispatches that need a worker, or file-system work that can't be rolled back by a transaction abort.
- **Priority discipline**: 0–99 = early, 100 = default, 200+ = late.
- **Rollback is optional**: default returns `false`. Only override if your step is genuinely reversible.

## Testing

```php
it('backfills missing schema_version', function (): void {
    Widget::factory()->create(['schema_version' => null]);
    $context = new UpgradeContext([], [], [], false);

    $result = (new BackfillWidgetSchemaV2())->run($context);

    expect($result)->toBeTrue()
        ->and(Widget::where('schema_version', 2)->count())->toBe(1);
});
```