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 @@
-
-
-
-
-
{{ deployment.raw_output.start }}
-
-
-
{{ deployment.raw_output.provision }}
-
-
-
{{ deployment.raw_output.build }}
-
-
-
{{ deployment.raw_output.publish }}
-
-
-
{{ deployment.raw_output.finish }}
+
+
+
+
+ |
+ Step |
+ Server |
+ Started |
+ Ended |
+ |
+
+
+
+
+
+
+ |
+
+
+
+ |
+ {{ taskLabels[name] }} |
+ {{ task.server.name }} |
+
+
+ {{ moment(task.started_at).format('L') }} {{ moment(task.started_at).format('LTS') }}
+
+ N/A
+ |
+
+
+ {{ moment(task.ended_at).format('L') }} {{ moment(task.ended_at).format('LTS') }}
+
+ N/A
+ |
+
+
+ |
+
+
+
+
@@ -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 @@
+
+
+
+
+ {{ output }}
+
+
+
+
+
+
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 @@
- | Name |
- Repository |
- Branch |
- Last deployment |
+ Name |
+ Repository |
+ Branch |
+ Last deployment |
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',
+ },
+})
@@ -15,7 +22,7 @@ const { show, close, redirect } = useModal()
-
+
diff --git a/routes/web.php b/routes/web.php
index 855609c..de3e07b 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -62,5 +62,6 @@
Route::post('/{project}/deployments', [ProjectDeploymentController::class, 'store'])->name('deployments.store');
Route::get('/{project}/deployments', [ProjectDeploymentController::class, 'index'])->name('deployments.index');
Route::get('/{project}/deployments/{deployment}', [ProjectDeploymentController::class, 'show'])->name('deployments.show');
+ Route::get('/{project}/deployments/{deployment}/tasks/{task}/output', [ProjectDeploymentController::class, 'showTaskOutput'])->name('deployments.tasks.show-output');
});
});