From 1b7826303c4786014393f3c9cdfd317b9a83bf3e Mon Sep 17 00:00:00 2001 From: Simon Rubuano Date: Wed, 5 Apr 2023 10:03:01 +0200 Subject: [PATCH] feat: split deployments into tasks --- .../ProjectDeploymentController.php | 30 +++- app/Jobs/DeployJob.php | 117 +++++----------- app/Jobs/ProcessDeploymentTaskJob.php | 102 ++++++++++++++ app/Models/Deployment.php | 7 + app/Models/DeploymentTask.php | 33 +++++ .../2023_03_20_074952_create_jobs_table.php | 11 -- ...4_134137_create_deployment_tasks_table.php | 24 ++++ ..._04_04_141413_create_job_batches_table.php | 23 ++++ .../js/Pages/Projects/Deployments/Index.vue | 8 +- .../js/Pages/Projects/Deployments/Show.vue | 129 +++++++----------- .../Pages/Projects/Deployments/TaskOutput.vue | 21 +++ resources/js/Pages/Projects/Index.vue | 8 +- resources/js/Shared/Modal.vue | 9 +- routes/web.php | 1 + 14 files changed, 336 insertions(+), 187 deletions(-) create mode 100644 app/Jobs/ProcessDeploymentTaskJob.php create mode 100644 app/Models/DeploymentTask.php create mode 100644 database/migrations/2023_04_04_134137_create_deployment_tasks_table.php create mode 100644 database/migrations/2023_04_04_141413_create_job_batches_table.php create mode 100644 resources/js/Pages/Projects/Deployments/TaskOutput.vue diff --git a/app/Http/Controllers/ProjectDeploymentController.php b/app/Http/Controllers/ProjectDeploymentController.php index a5748f2..5ef420d 100644 --- a/app/Http/Controllers/ProjectDeploymentController.php +++ b/app/Http/Controllers/ProjectDeploymentController.php @@ -4,6 +4,7 @@ use App\Jobs\DeployJob; use App\Models\Deployment; +use App\Models\DeploymentTask; use App\Models\Project; use Illuminate\Http\Request; use Illuminate\Validation\Rule; @@ -90,14 +91,37 @@ public function index(Project $project) ->latest() ->paginate(); - return inertia('Projects/Deployments/Index', compact('project', 'deployments')); + return Inertia::render('Projects/Deployments/Index', [ + 'project' => $project, + 'deployments' => $deployments, + ]); } public function show(Project $project, Deployment $deployment) { abort_if($deployment->project_id != $project->id, 404); - $deployment->makeVisible(['raw_output']); - return inertia('Projects/Deployments/Show', compact('project', 'deployment')); + $tasks = $deployment + ->tasks() + ->with('server') + ->get() + ->groupBy('name'); + + return Inertia::render('Projects/Deployments/Show', [ + 'project' => $project, + 'deployment' => $deployment, + 'tasks' => $tasks, + ]); + } + + public function showTaskOutput(Project $project, Deployment $deployment, DeploymentTask $task) + { + abort_if($deployment->project_id != $project->id, 404); + abort_if($task->deployment_id != $deployment->id, 404); + + return Inertia::modal('Projects/Deployments/TaskOutput', [ + 'output' => $task->output, + ]) + ->baseRoute('projects.deployments.show', [$project, $deployment]); } } diff --git a/app/Jobs/DeployJob.php b/app/Jobs/DeployJob.php index c6f190d..e150776 100644 --- a/app/Jobs/DeployJob.php +++ b/app/Jobs/DeployJob.php @@ -3,41 +3,36 @@ namespace App\Jobs; use App\Models\Deployment; -use App\Notifications\DeploymentFailed; -use App\Notifications\DeploymentStarted; -use App\Notifications\DeploymentSuccessful; +use App\Models\DeploymentTask; use App\Utils\ShellScriptRenderer; +use Illuminate\Bus\Batch; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Storage; -use Spatie\Ssh\Ssh as SSH; -use Symfony\Component\Process\Exception\ProcessFailedException; +use Illuminate\Support\Facades\Bus; +use Throwable; class DeployJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected $ssh; - protected Deployment $deployment; - protected $tasks; protected $github_deployment_id; - public function __construct(Deployment $deployment) - { - $this->deployment = $deployment; + public function __construct( + protected Deployment $deployment + ) { } - public function handle() + public function handle():void { $this ->before() ->computeTasks() - ->runTasks() - ->after(); + ->runTasks(); } protected function before() @@ -46,27 +41,6 @@ protected function before() $this->deployment->started_at = now(); $this->deployment->save(); - $this->ssh = SSH::create( - $this->deployment->server->ssh_user, - $this->deployment->server->ssh_host, - $this->deployment->server->ssh_port, - ) - ->usePrivateKey(Storage::path('keys/' . $this->deployment->server->id)) - ->disableStrictHostKeyChecking(); - - $this->github_deployment_id = rescue(function () { - $gh_client = $this->deployment->project->user->github()->deployments(); - [$user, $repo] = explode('/', $this->deployment->project->repository); - - return $gh_client->create($user, $repo, [ - 'ref' => $this->deployment->commit['from_ref'] ? 'refs/' . $this->deployment->commit['from_ref'] : $this->deployment->commit['sha'], - 'environment' => $this->deployment->project->environment, - 'auto_merge' => false, - ])['id']; - }, null, false); - - $this->deployment->project->notify(new DeploymentStarted($this->deployment, $this->github_deployment_id)); - return $this; } @@ -119,59 +93,34 @@ protected function defineTask($name, $commands) protected function runTasks() { + $jobs = []; + foreach ($this->tasks as $name => $task) { - $this->runTask($name); + $model = DeploymentTask::create([ + 'deployment_id' => $this->deployment->id, + 'server_id' => $this->deployment->server_id, + 'name' => $name, + 'commands' => $task, + ]); + + $jobs[] = (new ProcessDeploymentTaskJob($model)); } - return $this; - } - - protected function runTask($name) - { - $this->ssh->onOutput(fn ($type, $line) => $this->appendToOutput($type, $line, $name)); - - $process = $this->ssh->execute($this->tasks[$name]); - - if (! $process->isSuccessful()) { - throw new ProcessFailedException($process); - } + $deployment = $this->deployment; + + $batch = Bus::batch([ + [...$jobs], + ])->then(function (Batch $batch) use ($deployment) { + $deployment->status = 'success'; + $deployment->save(); + })->catch(function (Batch $batch, Throwable $e) use ($deployment) { + $deployment->status = 'error'; + $deployment->save(); + })->finally(function (Batch $batch) use ($deployment) { + $deployment->ended_at = now(); + $deployment->save(); + })->dispatch(); return $this; } - - protected function after() - { - $this->deployment->status = 'success'; - $this->deployment->ended_at = now(); - $this->deployment->save(); - - $this->deployment->project->notify(new DeploymentSuccessful($this->deployment, $this->github_deployment_id)); - - // dispatch(new PingJob($this->deployment)); - - return $this; - } - - public function failed() - { - $this->deployment->status = 'error'; - $this->deployment->ended_at = now(); - $this->deployment->save(); - - $this->deployment->project->notify(new DeploymentFailed($this->deployment, $this->github_deployment_id)); - } - - protected function appendToOutput($type, $line, $name) - { - $raw_output = $this->deployment->raw_output; - - if (! ($raw_output[$name] ?? null)) { - $raw_output[$name] = ''; - } - - $raw_output[$name] .= $line; - $this->deployment->raw_output = $raw_output; - - $this->deployment->save(); - } } diff --git a/app/Jobs/ProcessDeploymentTaskJob.php b/app/Jobs/ProcessDeploymentTaskJob.php new file mode 100644 index 0000000..54e05d7 --- /dev/null +++ b/app/Jobs/ProcessDeploymentTaskJob.php @@ -0,0 +1,102 @@ +batch()->cancelled()) { + return; + } + + $this + ->before() + ->runTask() + ->after(); + } + + protected function before() + { + $this->deployment = $this->task->deployment; + $this->server = $this->deployment->server; + + $this->ssh = SSH::create( + $this->server->ssh_user, + $this->server->ssh_host, + $this->server->ssh_port, + ) + ->usePrivateKey(Storage::path('keys/' . $this->server->id)) + ->disableStrictHostKeyChecking(); + + $this->ssh->onOutput(fn ($type, $line) => $this->appendToOutput($type, $line)); + + $this->task->status = 'in_progress'; + $this->task->started_at = now(); + $this->task->save(); + + return $this; + } + + protected function runTask() + { + if (! $this->task->commands) { + return $this; + } + + $process = $this->ssh->execute($this->task->commands); + + if (! $process->isSuccessful()) { + throw new ProcessFailedException($process); + } + + return $this; + } + + protected function after() + { + $this->task->status = 'success'; + $this->task->ended_at = now(); + $this->task->save(); + + return $this; + } + + public function failed() + { + $this->task->status = 'error'; + $this->task->ended_at = now(); + $this->task->save(); + } + + protected function appendToOutput($type, $line) + { + $this->task->output .= $line; + + $this->task->save(); + } +} diff --git a/app/Models/Deployment.php b/app/Models/Deployment.php index 88e940e..692d66a 100644 --- a/app/Models/Deployment.php +++ b/app/Models/Deployment.php @@ -39,6 +39,13 @@ public function server() return $this->belongsTo(Server::class); } + public function tasks() + { + return $this + ->hasMany(DeploymentTask::class) + ->orderBy('created_at'); + } + public function getDurationAttribute() { return rescue(fn () => $this->ended_at->diff($this->started_at)->format('%i minutes %s seconds'), 'N/A', false); diff --git a/app/Models/DeploymentTask.php b/app/Models/DeploymentTask.php new file mode 100644 index 0000000..5aec6d6 --- /dev/null +++ b/app/Models/DeploymentTask.php @@ -0,0 +1,33 @@ + 'json', + ]; + + protected $hidden = [ + 'commands', + 'output', + ]; + + public function deployment() + { + return $this->belongsTo(Deployment::class); + } + + public function server() + { + return $this->belongsTo(Server::class); + } +} diff --git a/database/migrations/2023_03_20_074952_create_jobs_table.php b/database/migrations/2023_03_20_074952_create_jobs_table.php index 7a5df54..aa49918 100644 --- a/database/migrations/2023_03_20_074952_create_jobs_table.php +++ b/database/migrations/2023_03_20_074952_create_jobs_table.php @@ -5,9 +5,6 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { - /** - * Run the migrations. - */ public function up(): void { Schema::create('jobs', function (Blueprint $table) { @@ -20,12 +17,4 @@ public function up(): void $table->unsignedInteger('created_at'); }); } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('jobs'); - } }; diff --git a/database/migrations/2023_04_04_134137_create_deployment_tasks_table.php b/database/migrations/2023_04_04_134137_create_deployment_tasks_table.php new file mode 100644 index 0000000..f5251d7 --- /dev/null +++ b/database/migrations/2023_04_04_134137_create_deployment_tasks_table.php @@ -0,0 +1,24 @@ +char('id', 26)->primary(); + $table->char('deployment_id', 26)->nullable(); + $table->char('server_id', 26)->nullable(); + $table->string('name'); + $table->string('status')->default('pending'); + $table->json('commands')->nullable(); + $table->longText('output')->nullable(); + $table->datetime('started_at')->nullable(); + $table->datetime('ended_at')->nullable(); + + $table->timestamps(); + }); + } +}; diff --git a/database/migrations/2023_04_04_141413_create_job_batches_table.php b/database/migrations/2023_04_04_141413_create_job_batches_table.php new file mode 100644 index 0000000..b503ac8 --- /dev/null +++ b/database/migrations/2023_04_04_141413_create_job_batches_table.php @@ -0,0 +1,23 @@ +string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + } +}; diff --git a/resources/js/Pages/Projects/Deployments/Index.vue b/resources/js/Pages/Projects/Deployments/Index.vue index f710edd..4359049 100644 --- a/resources/js/Pages/Projects/Deployments/Index.vue +++ b/resources/js/Pages/Projects/Deployments/Index.vue @@ -8,10 +8,10 @@ - Release - Started - Duration - Commit + Release + Started + Duration + Commit diff --git a/resources/js/Pages/Projects/Deployments/Show.vue b/resources/js/Pages/Projects/Deployments/Show.vue index 355efb4..b60fc48 100644 --- a/resources/js/Pages/Projects/Deployments/Show.vue +++ b/resources/js/Pages/Projects/Deployments/Show.vue @@ -59,65 +59,48 @@ -
-
-

Process output

-
- -
-
- - - - -

🏃 start

-
-
-
{{ deployment.raw_output.start }}
- -
-
- - - - -

🚚 provision

-
-
-
{{ deployment.raw_output.provision }}
- -
-
- - - - -

📦 build

-
-
-
{{ deployment.raw_output.build }}
- -
-
- - - - -

🚀 publish

-
-
-
{{ deployment.raw_output.publish }}
- -
-
- - - - -

🗑️ finish

-
-
-
{{ deployment.raw_output.finish }}
+
+ + + + + + + + + + + + + +
+ StepServerStartedEnded +
@@ -130,33 +113,19 @@ export default { props: { project: Object, deployment: Object, + tasks: Object, }, data() { return { - visible: { - start: false, - provision: false, - build: false, - publish: false, - finish: false, + taskLabels: { + start: '🏃 Start', + provision: '🚚 Provision', + build: '📦 Build', + publish: '🚀 Publish', + finish: '🗑️ Finish', }, } }, - - mounted() { - if (!this.deployment.raw_output) return - if (this.deployment.raw_output.start && !this.deployment.raw_output.provision) this.visible.start = true - if (this.deployment.raw_output.provision && !this.deployment.raw_output.build) this.visible.provision = true - if (this.deployment.raw_output.build && !this.deployment.raw_output.publish) this.visible.build = true - if (this.deployment.raw_output.publish && !this.deployment.raw_output.finish) this.visible.publish = true - if (this.deployment.raw_output.finish && !this.deployment.status == 'success') this.visible.finish = true - }, - - methods: { - toggle(name) { - this.visible[name] = !this.visible[name] - }, - }, } diff --git a/resources/js/Pages/Projects/Deployments/TaskOutput.vue b/resources/js/Pages/Projects/Deployments/TaskOutput.vue new file mode 100644 index 0000000..3b5ba23 --- /dev/null +++ b/resources/js/Pages/Projects/Deployments/TaskOutput.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/resources/js/Pages/Projects/Index.vue b/resources/js/Pages/Projects/Index.vue index e45233c..8f7a802 100644 --- a/resources/js/Pages/Projects/Index.vue +++ b/resources/js/Pages/Projects/Index.vue @@ -17,10 +17,10 @@ - - - - + + + + diff --git a/resources/js/Shared/Modal.vue b/resources/js/Shared/Modal.vue index ca5c8ec..1b87145 100644 --- a/resources/js/Shared/Modal.vue +++ b/resources/js/Shared/Modal.vue @@ -3,6 +3,13 @@ import { TransitionRoot, TransitionChild, Dialog, DialogPanel } from '@headlessu import { useModal } from 'momentum-modal' const { show, close, redirect } = useModal() + +defineProps({ + dialogPanelClasses: { + type: String, + default: 'w-full max-w-lg p-6 overflow-hidden text-left align-middle transition-all transform bg-white rounded-lg shadow-xl', + }, +})
NameRepositoryBranchLast deploymentNameRepositoryBranchLast deployment