Tidy automation backend: run duration, folded migrations, imports

- Add AutomationRun::durationInMilliseconds() as the single source of truth
  for the Invocations list and metrics, replacing the duplicated inline diff.
- Fold the variables and root_run_id columns into their create migrations
  (this branch isn't in production) and drop the standalone alters.
- Import Illuminate\Http\Response (aliased) instead of referencing it inline.
This commit is contained in:
Paulo Castellano 2026-06-13 16:30:32 -03:00
parent 2bd2e72656
commit 730cb9d156
9 changed files with 37 additions and 64 deletions

View file

@ -43,8 +43,8 @@ public function __invoke(Automation $automation, CarbonInterface $start, CarbonI
$successRate = $finished > 0 ? (int) round($completed->count() / $finished * 100) : null;
$durations = $completed
->filter(fn ($run) => $run->started_at !== null && $run->finished_at !== null)
->map(fn ($run) => $run->started_at->diffInMilliseconds($run->finished_at));
->map(fn ($run) => $run->durationInMilliseconds())
->filter(fn ($ms) => $ms !== null);
$avgDurationMs = $durations->isNotEmpty() ? (int) round($durations->avg()) : null;
return [

View file

@ -33,6 +33,7 @@
use App\Models\AutomationRun;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response as HttpResponse;
use Inertia\Inertia;
use Inertia\Response;
@ -183,7 +184,7 @@ public function retryRun(
RetryRunFromNode $retry,
Automation $automation,
AutomationRun $run,
): \Illuminate\Http\Response {
): HttpResponse {
$this->authorize('update', $automation);
abort_unless($run->automation_id === $automation->id, 404);

View file

@ -18,16 +18,12 @@ class AutomationInvocationResource extends JsonResource
*/
public function toArray(Request $request): array
{
$durationMs = $this->started_at !== null && $this->finished_at !== null
? $this->started_at->diffInMilliseconds($this->finished_at)
: null;
return [
'id' => $this->id,
'status' => $this->status->value,
'is_manual' => (bool) $this->is_manual,
'node_run_count' => (int) ($this->node_runs_count ?? 0),
'duration_ms' => $durationMs,
'duration_ms' => $this->durationInMilliseconds(),
'error_message' => is_array($this->error) ? ($this->error['message'] ?? null) : $this->error,
'created_at' => $this->created_at,
'started_at' => $this->started_at,

View file

@ -48,6 +48,19 @@ public function rootId(): string
return $this->root_run_id ?? $this->id;
}
/**
* Wall-clock execution time, or null while the run hasn't both started and
* finished. Single source of truth for the Invocations list and metrics.
*/
public function durationInMilliseconds(): ?int
{
if ($this->started_at === null || $this->finished_at === null) {
return null;
}
return (int) $this->started_at->diffInMilliseconds($this->finished_at);
}
/**
* Context for template (`{{ ... }}`) resolution: the run context plus the
* automation's workflow variables, merged in-memory. Variables are NEVER

View file

@ -22,6 +22,7 @@ public function up(): void
$table->string('trigger_type')->nullable();
$table->json('nodes')->nullable();
$table->json('connections')->nullable();
$table->json('variables')->nullable();
$table->timestamp('activated_at')->nullable();
$table->timestamp('paused_at')->nullable();
$table->timestamps();

View file

@ -16,6 +16,7 @@ public function up(): void
Schema::create('automation_runs', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('automation_id')->constrained('automations')->cascadeOnDelete();
$table->foreignUuid('root_run_id')->nullable();
$table->foreignUuid('trigger_item_id')->nullable()->constrained('automation_trigger_items')->nullOnDelete();
$table->string('current_node_id')->nullable();
$table->string('status')->default('pending');
@ -32,6 +33,10 @@ public function up(): void
$table->index(['automation_id', 'status']);
$table->index(['status', 'next_action_at']);
});
Schema::table('automation_runs', function (Blueprint $table) {
$table->foreign('root_run_id')->references('id')->on('automation_runs')->nullOnDelete();
});
}
/**

View file

@ -1,28 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('automations', function (Blueprint $table) {
$table->json('variables')->nullable()->after('connections');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('automations', function (Blueprint $table) {
$table->dropColumn('variables');
});
}
};

View file

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('automation_runs', function (Blueprint $table) {
// Links every run forked by a fan-out back to the run that started the
// execution, so the editor test panel can aggregate all branches of a
// single test under one root. Null on the root run itself.
$table->foreignUuid('root_run_id')->nullable()->after('automation_id')
->constrained('automation_runs')->nullOnDelete();
});
}
public function down(): void
{
Schema::table('automation_runs', function (Blueprint $table) {
$table->dropConstrainedForeignId('root_run_id');
});
}
};

View file

@ -38,6 +38,19 @@
expect($automation->trigger_type)->toBeNull();
});
it('computes run duration only once both timestamps are set', function () {
$start = now()->startOfSecond();
$running = AutomationRun::factory()->make(['started_at' => $start, 'finished_at' => null]);
$finished = AutomationRun::factory()->make([
'started_at' => $start,
'finished_at' => $start->copy()->addSeconds(2),
]);
expect($running->durationInMilliseconds())->toBeNull()
->and($finished->durationInMilliseconds())->toBe(2000);
});
it('relates trigger items, runs and node runs', function () {
$automation = Automation::factory()->create();
$item = AutomationTriggerItem::factory()->for($automation)->create();