Deployment environments
* Development, staging, and production environment types * Protected-environment support * Deployment targets * Local, SSH, Docker, and Kubernetes driver foundation * Deployment pipelines * Configurable pipeline steps * Manual and approval-based releases * Deployment release history * Individual deployment-step tracking * HTTP health checks * Response-time tracking * Rollback records * Secret-reference metadata * Encrypted target configuration * Encrypted environment variables * Administration dashboard * Authenticated API endpoints * Health-check command * Release-history pruning command * Permissions * Sidebar integration * Module registration * Feature test
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'enabled' => env('CLOUD_DEVOPS_ENABLED', true),
|
||||
'allow_execution' => env('CLOUD_DEVOPS_ALLOW_EXECUTION', false),
|
||||
'default_timeout' => (int) env('CLOUD_DEVOPS_TIMEOUT', 900),
|
||||
'retention_days' => (int) env('CLOUD_DEVOPS_RETENTION_DAYS', 90),
|
||||
'allowed_drivers' => ['local', 'ssh', 'docker', 'kubernetes'],
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Modules\CloudDevOps\Console\Commands;
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\CloudDevOps\Models\DeploymentHealthCheck;
|
||||
use Modules\CloudDevOps\Services\HealthCheckRunner;
|
||||
final class CloudDevOpsHealthCommand extends Command { protected $signature='cloud-devops:health'; protected $description='Run all active deployment health checks'; public function handle(HealthCheckRunner $runner): int { DeploymentHealthCheck::query()->each(fn($check)=>$runner->run($check)); $this->info('Health checks completed.'); return self::SUCCESS; } }
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
namespace Modules\CloudDevOps\Console\Commands;
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\CloudDevOps\Models\DeploymentRelease;
|
||||
final class CloudDevOpsPruneCommand extends Command { protected $signature='cloud-devops:prune {--days=}'; protected $description='Prune old deployment release history'; public function handle(): int { $days=(int)($this->option('days') ?: config('clouddevops.retention_days',90)); $count=DeploymentRelease::where('created_at','<',now()->subDays($days))->whereIn('status',['succeeded','failed','cancelled'])->delete(); $this->info("Pruned {$count} releases."); return self::SUCCESS; } }
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
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::create('deployment_environments', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->string('type')->default('production');
|
||||
$table->string('status')->default('active');
|
||||
$table->boolean('is_protected')->default(false);
|
||||
$table->json('variables')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('deployment_targets', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('driver')->default('ssh');
|
||||
$table->string('host')->nullable();
|
||||
$table->unsignedInteger('port')->nullable();
|
||||
$table->string('path')->nullable();
|
||||
$table->json('configuration')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('deployment_pipelines', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('trigger')->default('manual');
|
||||
$table->json('steps');
|
||||
$table->boolean('requires_approval')->default(false);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('deployment_releases', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete();
|
||||
$table->foreignId('pipeline_id')->nullable()->constrained('deployment_pipelines')->nullOnDelete();
|
||||
$table->string('version');
|
||||
$table->string('commit_hash')->nullable();
|
||||
$table->string('status')->default('pending');
|
||||
$table->foreignId('requested_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->text('failure_message')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['environment_id', 'status']);
|
||||
});
|
||||
|
||||
Schema::create('deployment_steps', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('release_id')->constrained('deployment_releases')->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->unsignedInteger('position')->default(0);
|
||||
$table->string('status')->default('pending');
|
||||
$table->text('output')->nullable();
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('deployment_health_checks', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('type')->default('http');
|
||||
$table->string('endpoint')->nullable();
|
||||
$table->unsignedInteger('expected_status')->default(200);
|
||||
$table->string('status')->default('unknown');
|
||||
$table->unsignedInteger('response_time_ms')->nullable();
|
||||
$table->timestamp('checked_at')->nullable();
|
||||
$table->text('message')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('deployment_rollbacks', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete();
|
||||
$table->foreignId('from_release_id')->constrained('deployment_releases')->cascadeOnDelete();
|
||||
$table->foreignId('to_release_id')->nullable()->constrained('deployment_releases')->nullOnDelete();
|
||||
$table->string('status')->default('pending');
|
||||
$table->foreignId('requested_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->text('reason')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('deployment_secrets', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete();
|
||||
$table->string('key');
|
||||
$table->string('provider')->default('database');
|
||||
$table->string('reference')->nullable();
|
||||
$table->timestamp('rotated_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['environment_id', 'key']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('deployment_secrets');
|
||||
Schema::dropIfExists('deployment_rollbacks');
|
||||
Schema::dropIfExists('deployment_health_checks');
|
||||
Schema::dropIfExists('deployment_steps');
|
||||
Schema::dropIfExists('deployment_releases');
|
||||
Schema::dropIfExists('deployment_pipelines');
|
||||
Schema::dropIfExists('deployment_targets');
|
||||
Schema::dropIfExists('deployment_environments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
namespace Modules\CloudDevOps\Http\Controllers;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Modules\CloudDevOps\Models\DeploymentEnvironment;
|
||||
use Modules\CloudDevOps\Models\DeploymentRelease;
|
||||
final class CloudDevOpsApiController extends Controller { public function environments(): JsonResponse { return response()->json(['data'=>DeploymentEnvironment::withCount(['targets','pipelines','releases'])->get()]); } public function releases(): JsonResponse { return response()->json(['data'=>DeploymentRelease::with(['environment','steps'])->latest()->paginate()]); } }
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\CloudDevOps\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Modules\CloudDevOps\Models\DeploymentEnvironment;
|
||||
use Modules\CloudDevOps\Models\DeploymentHealthCheck;
|
||||
use Modules\CloudDevOps\Models\DeploymentPipeline;
|
||||
use Modules\CloudDevOps\Models\DeploymentRelease;
|
||||
use Modules\CloudDevOps\Services\DeploymentManager;
|
||||
use Modules\CloudDevOps\Services\HealthCheckRunner;
|
||||
|
||||
final class CloudDevOpsController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('clouddevops::index', [
|
||||
'environments' => DeploymentEnvironment::withCount(['targets', 'pipelines', 'releases'])->latest()->get(),
|
||||
'releases' => DeploymentRelease::with('environment')->latest()->limit(10)->get(),
|
||||
'checks' => DeploymentHealthCheck::with('environment')->latest('checked_at')->limit(10)->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function storeEnvironment(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['name'=>'required|string|max:120','slug'=>'required|alpha_dash|max:120|unique:deployment_environments,slug','type'=>'required|in:development,staging,production']);
|
||||
DeploymentEnvironment::create($data);
|
||||
return back()->with('status', 'Environment created.');
|
||||
}
|
||||
|
||||
public function deploy(Request $request, DeploymentEnvironment $environment, DeploymentManager $manager): RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['pipeline_id'=>'required|integer','version'=>'nullable|string|max:120','commit_hash'=>'nullable|string|max:120']);
|
||||
$pipeline = DeploymentPipeline::where('environment_id', $environment->id)->findOrFail($data['pipeline_id']);
|
||||
$manager->createRelease($environment, $pipeline, $data + ['requested_by' => $request->user()?->id]);
|
||||
return back()->with('status', 'Release created.');
|
||||
}
|
||||
|
||||
public function runCheck(DeploymentHealthCheck $check, HealthCheckRunner $runner): RedirectResponse
|
||||
{
|
||||
$runner->run($check);
|
||||
return back()->with('status', 'Health check completed.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\CloudDevOps\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
final class DeploymentEnvironment extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
protected function casts(): array { return ['variables' => 'encrypted:array', 'is_protected' => 'boolean']; }
|
||||
public function targets(): HasMany { return $this->hasMany(DeploymentTarget::class, 'environment_id'); }
|
||||
public function pipelines(): HasMany { return $this->hasMany(DeploymentPipeline::class, 'environment_id'); }
|
||||
public function releases(): HasMany { return $this->hasMany(DeploymentRelease::class, 'environment_id'); }
|
||||
public function healthChecks(): HasMany { return $this->hasMany(DeploymentHealthCheck::class, 'environment_id'); }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
namespace Modules\CloudDevOps\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
final class DeploymentHealthCheck extends Model { protected $guarded = []; protected function casts(): array { return ['checked_at' => 'datetime']; } }
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
namespace Modules\CloudDevOps\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
final class DeploymentPipeline extends Model { protected $guarded = []; protected function casts(): array { return ['steps' => 'array', 'requires_approval' => 'boolean', 'is_active' => 'boolean']; } }
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Modules\CloudDevOps\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
final class DeploymentRelease extends Model { protected $guarded = []; protected function casts(): array { return ['metadata' => 'array', 'started_at' => 'datetime', 'completed_at' => 'datetime']; } public function environment(): BelongsTo { return $this->belongsTo(DeploymentEnvironment::class, 'environment_id'); } public function steps(): HasMany { return $this->hasMany(DeploymentStep::class, 'release_id')->orderBy('position'); } }
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
namespace Modules\CloudDevOps\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
final class DeploymentRollback extends Model { protected $guarded = []; }
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
namespace Modules\CloudDevOps\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
final class DeploymentSecret extends Model { protected $guarded = []; protected function casts(): array { return ['rotated_at' => 'datetime']; } }
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
namespace Modules\CloudDevOps\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
final class DeploymentStep extends Model { protected $guarded = []; protected function casts(): array { return ['started_at' => 'datetime', 'completed_at' => 'datetime']; } }
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
namespace Modules\CloudDevOps\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
final class DeploymentTarget extends Model { protected $guarded = []; protected function casts(): array { return ['configuration' => 'encrypted:array', 'is_active' => 'boolean']; } }
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Modules\CloudDevOps\Providers;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\CloudDevOps\Console\Commands\CloudDevOpsHealthCommand;
|
||||
use Modules\CloudDevOps\Console\Commands\CloudDevOpsPruneCommand;
|
||||
final class CloudDevOpsServiceProvider extends ServiceProvider { public function register(): void { $this->mergeConfigFrom(module_path('CloudDevOps','Config/config.php'),'clouddevops'); $this->app->register(RouteServiceProvider::class); } public function boot(): void { $this->loadViewsFrom(module_path('CloudDevOps','Resources/views'),'clouddevops'); $this->loadMigrationsFrom(module_path('CloudDevOps','Database/Migrations')); if ($this->app->runningInConsole()) { $this->commands([CloudDevOpsHealthCommand::class,CloudDevOpsPruneCommand::class]); } } }
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
namespace Modules\CloudDevOps\Providers;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
final class RouteServiceProvider extends ServiceProvider { public function boot(): void { Route::middleware('web')->group(module_path('CloudDevOps','Routes/web.php')); Route::middleware('api')->prefix('api')->group(module_path('CloudDevOps','Routes/api.php')); } }
|
||||
@@ -0,0 +1,44 @@
|
||||
<x-layouts.app :title="__('Cloud & DevOps')">
|
||||
<div class="flex h-full w-full flex-1 flex-col gap-6 rounded-xl">
|
||||
<div>
|
||||
<flux:heading size="xl">Cloud & DevOps</flux:heading>
|
||||
<flux:subheading>Manage environments, deployment releases, pipelines and service health.</flux:subheading>
|
||||
</div>
|
||||
|
||||
@if (session('status'))
|
||||
<flux:callout variant="success">{{ session('status') }}</flux:callout>
|
||||
@endif
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div class="rounded-xl border p-5"><p class="text-sm text-zinc-500">Environments</p><p class="mt-2 text-3xl font-semibold">{{ $environments->count() }}</p></div>
|
||||
<div class="rounded-xl border p-5"><p class="text-sm text-zinc-500">Recent releases</p><p class="mt-2 text-3xl font-semibold">{{ $releases->count() }}</p></div>
|
||||
<div class="rounded-xl border p-5"><p class="text-sm text-zinc-500">Healthy checks</p><p class="mt-2 text-3xl font-semibold">{{ $checks->where('status', 'healthy')->count() }}</p></div>
|
||||
</div>
|
||||
|
||||
@can('cloud-devops.manage')
|
||||
<form method="POST" action="{{ route('administration.cloud-devops.environments.store') }}" class="grid gap-3 rounded-xl border p-5 md:grid-cols-4">
|
||||
@csrf
|
||||
<flux:input name="name" label="Environment name" required />
|
||||
<flux:input name="slug" label="Slug" required />
|
||||
<flux:select name="type" label="Type"><option value="development">Development</option><option value="staging">Staging</option><option value="production">Production</option></flux:select>
|
||||
<div class="flex items-end"><flux:button type="submit" variant="primary">Create environment</flux:button></div>
|
||||
</form>
|
||||
@endcan
|
||||
|
||||
<section class="space-y-3">
|
||||
<flux:heading size="lg">Environments</flux:heading>
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
@forelse ($environments as $environment)
|
||||
<article class="rounded-xl border p-5">
|
||||
<div class="flex items-center justify-between gap-3"><div><h2 class="font-semibold">{{ $environment->name }}</h2><p class="text-sm text-zinc-500">{{ ucfirst($environment->type) }} · {{ ucfirst($environment->status) }}</p></div>@if ($environment->is_protected)<flux:badge color="amber">Protected</flux:badge>@endif</div>
|
||||
<div class="mt-4 grid grid-cols-3 gap-3 text-sm"><div><span class="text-zinc-500">Targets</span><p>{{ $environment->targets_count }}</p></div><div><span class="text-zinc-500">Pipelines</span><p>{{ $environment->pipelines_count }}</p></div><div><span class="text-zinc-500">Releases</span><p>{{ $environment->releases_count }}</p></div></div>
|
||||
</article>
|
||||
@empty
|
||||
<div class="col-span-full rounded-xl border p-8 text-center text-zinc-500">No deployment environments configured.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3"><flux:heading size="lg">Recent releases</flux:heading><div class="overflow-hidden rounded-xl border"><table class="w-full text-sm"><thead class="bg-zinc-50 dark:bg-zinc-900"><tr><th class="p-3 text-left">Version</th><th class="p-3 text-left">Environment</th><th class="p-3 text-left">Status</th><th class="p-3 text-left">Created</th></tr></thead><tbody>@forelse ($releases as $release)<tr class="border-t"><td class="p-3">{{ $release->version }}</td><td class="p-3">{{ $release->environment?->name }}</td><td class="p-3">{{ ucfirst(str_replace('_', ' ', $release->status)) }}</td><td class="p-3">{{ $release->created_at?->diffForHumans() }}</td></tr>@empty<tr><td colspan="4" class="p-6 text-center text-zinc-500">No releases yet.</td></tr>@endforelse</tbody></table></div></section>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\CloudDevOps\Http\Controllers\CloudDevOpsApiController;
|
||||
Route::middleware(['auth:sanctum','permission:cloud-devops.view'])->prefix('v1/cloud-devops')->group(function (): void { Route::get('/environments',[CloudDevOpsApiController::class,'environments']); Route::get('/releases',[CloudDevOpsApiController::class,'releases']); });
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\CloudDevOps\Http\Controllers\CloudDevOpsController;
|
||||
|
||||
Route::middleware(['web', 'auth', 'verified', 'permission:cloud-devops.view'])
|
||||
->prefix('administration/cloud-devops')
|
||||
->name('administration.cloud-devops.')
|
||||
->group(function (): void {
|
||||
Route::get('/', [CloudDevOpsController::class, 'index'])->name('index');
|
||||
Route::post('/environments', [CloudDevOpsController::class, 'storeEnvironment'])->middleware('permission:cloud-devops.manage')->name('environments.store');
|
||||
Route::post('/environments/{environment}/deploy', [CloudDevOpsController::class, 'deploy'])->middleware('permission:cloud-devops.deploy')->name('deploy');
|
||||
Route::post('/health-checks/{check}/run', [CloudDevOpsController::class, 'runCheck'])->middleware('permission:cloud-devops.manage')->name('checks.run');
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\CloudDevOps\Services;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\CloudDevOps\Models\DeploymentEnvironment;
|
||||
use Modules\CloudDevOps\Models\DeploymentPipeline;
|
||||
use Modules\CloudDevOps\Models\DeploymentRelease;
|
||||
|
||||
final class DeploymentManager
|
||||
{
|
||||
public function createRelease(DeploymentEnvironment $environment, DeploymentPipeline $pipeline, array $attributes = []): DeploymentRelease
|
||||
{
|
||||
return DB::transaction(function () use ($environment, $pipeline, $attributes): DeploymentRelease {
|
||||
$release = $environment->releases()->create([
|
||||
'pipeline_id' => $pipeline->getKey(),
|
||||
'version' => $attributes['version'] ?? now()->format('YmdHis').'-'.Str::lower(Str::random(6)),
|
||||
'commit_hash' => $attributes['commit_hash'] ?? null,
|
||||
'status' => $pipeline->requires_approval ? 'awaiting_approval' : 'pending',
|
||||
'requested_by' => $attributes['requested_by'] ?? null,
|
||||
'metadata' => $attributes['metadata'] ?? [],
|
||||
]);
|
||||
|
||||
foreach ($pipeline->steps as $position => $step) {
|
||||
$release->steps()->create(['name' => $step['name'] ?? 'Step '.($position + 1), 'position' => $position]);
|
||||
}
|
||||
|
||||
return $release->load('steps');
|
||||
});
|
||||
}
|
||||
|
||||
public function execute(DeploymentRelease $release): DeploymentRelease
|
||||
{
|
||||
abort_unless(config('clouddevops.allow_execution'), 403, 'Deployment execution is disabled.');
|
||||
$release->update(['status' => 'running', 'started_at' => now()]);
|
||||
return $release->refresh();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
namespace Modules\CloudDevOps\Services;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Modules\CloudDevOps\Models\DeploymentHealthCheck;
|
||||
final class HealthCheckRunner { public function run(DeploymentHealthCheck $check): DeploymentHealthCheck { $started = microtime(true); try { $response = Http::timeout(10)->get((string) $check->endpoint); $status = $response->status() === $check->expected_status ? 'healthy' : 'unhealthy'; $message = 'HTTP '.$response->status(); } catch (\Throwable $e) { $status = 'unhealthy'; $message = $e->getMessage(); } $check->update(['status'=>$status,'response_time_ms'=>(int)((microtime(true)-$started)*1000),'checked_at'=>now(),'message'=>$message]); return $check->refresh(); } }
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Modules\CloudDevOps\Tests\Feature;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Modules\CloudDevOps\Models\DeploymentEnvironment;
|
||||
use Tests\TestCase;
|
||||
final class CloudDevOpsModuleTest extends TestCase { use RefreshDatabase; public function test_environment_can_be_created(): void { $environment=DeploymentEnvironment::create(['name'=>'Production','slug'=>'production','type'=>'production']); $this->assertDatabaseHas('deployment_environments',['id'=>$environment->id,'slug'=>'production']); } }
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "CloudDevOps",
|
||||
"alias": "clouddevops",
|
||||
"description": "Cloud deployment and DevOps operations for Content Manager.",
|
||||
"keywords": ["cloud", "devops", "deployments", "pipelines"],
|
||||
"priority": 18,
|
||||
"providers": ["Modules\\CloudDevOps\\Providers\\CloudDevOpsServiceProvider"],
|
||||
"files": []
|
||||
}
|
||||
@@ -81,6 +81,7 @@ final class RolePermissionSeeder extends Seeder
|
||||
'analytics.export',
|
||||
'builder.view', 'builder.manage', 'builder.publish',
|
||||
'marketplace.view', 'marketplace.install', 'marketplace.update', 'marketplace.remove',
|
||||
'cloud-devops.view', 'cloud-devops.manage', 'cloud-devops.deploy', 'cloud-devops.rollback',
|
||||
];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
|
||||
@@ -43,5 +43,6 @@
|
||||
"AI": true,
|
||||
"Analytics": true,
|
||||
"Builder": true,
|
||||
"Marketplace": true
|
||||
"Marketplace": true,
|
||||
"CloudDevOps": true
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
<flux:sidebar.group :heading="__('Workflow')" class="grid"><flux:sidebar.item icon="arrows-right-left" :href="route('administration.workflows.index')" :current="request()->routeIs('administration.workflows.*')" wire:navigate>{{ __('Review Queue') }}</flux:sidebar.item></flux:sidebar.group>
|
||||
<flux:sidebar.group :heading="__('Management')" class="grid"><flux:sidebar.item icon="users" :href="route('administration.users.index')" :current="request()->routeIs('administration.users.*')" wire:navigate>{{ __('Users') }}</flux:sidebar.item><flux:sidebar.item icon="shield-check" :href="route('administration.roles.index')" :current="request()->routeIs('administration.roles.*')" wire:navigate>{{ __('Roles & Permissions') }}</flux:sidebar.item><flux:sidebar.item icon="photo" :href="route('administration.media.index')" :current="request()->routeIs('administration.media.*')" wire:navigate>{{ __('Media') }}</flux:sidebar.item><flux:sidebar.item icon="magnifying-glass" :href="route('administration.search.index')" :current="request()->routeIs('administration.search.*')" wire:navigate>{{ __('Search') }}</flux:sidebar.item></flux:sidebar.group>
|
||||
<flux:sidebar.group :heading="__('Configuration')" class="grid"><flux:sidebar.item icon="language" :href="route('administration.localization.index')" :current="request()->routeIs('administration.localization.*')" wire:navigate>{{ __('Localisation') }}</flux:sidebar.item><flux:sidebar.item icon="globe-alt" :href="route('administration.seo.index')" :current="request()->routeIs('administration.seo.*')" wire:navigate>{{ __('SEO') }}</flux:sidebar.item><flux:sidebar.item icon="cog-6-tooth" :href="route('administration.settings.index')" :current="request()->routeIs('administration.settings.*')" wire:navigate>{{ __('Settings') }}</flux:sidebar.item><flux:sidebar.item icon="cube" :href="route('administration.modules.index')" :current="request()->routeIs('administration.modules.*')" wire:navigate>{{ __('Modules') }}</flux:sidebar.item><flux:sidebar.item icon="clipboard-document-list" :href="route('administration.audit.index')" :current="request()->routeIs('administration.audit.*')" wire:navigate>{{ __('Audit Log') }}</flux:sidebar.item></flux:sidebar.group>
|
||||
<flux:sidebar.group :heading="__('Operations')" class="grid"><flux:sidebar.item icon="cloud-arrow-up" :href="route('administration.cloud-devops.index')" :current="request()->routeIs('administration.cloud-devops.*')" wire:navigate>{{ __('Cloud & DevOps') }}</flux:sidebar.item></flux:sidebar.group>
|
||||
</flux:sidebar.nav><flux:spacer /><x-stylist-theme-common::desktop-user-menu class="hidden lg:block" :name="auth()->user()->name" /></flux:sidebar>
|
||||
<flux:header class="lg:hidden"><flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" /><flux:spacer /><flux:dropdown position="top" align="end"><flux:profile :initials="auth()->user()->initials()" icon-trailing="chevron-down" /><flux:menu><flux:menu.radio.group><div class="p-0 text-sm font-normal"><div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm"><flux:avatar :name="auth()->user()->name" :initials="auth()->user()->initials()" /><div class="grid flex-1 text-start text-sm leading-tight"><flux:heading class="truncate">{{ auth()->user()->name }}</flux:heading><flux:text class="truncate">{{ auth()->user()->email }}</flux:text></div></div></div></flux:menu.radio.group><flux:menu.separator /><flux:menu.radio.group><flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item></flux:menu.radio.group><flux:menu.separator /><form method="POST" action="{{ route('logout') }}" class="w-full">@csrf<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full cursor-pointer" data-test="logout-button">{{ __('Log out') }}</flux:menu.item></form></flux:menu></flux:dropdown></flux:header>
|
||||
{{ $slot }}
|
||||
|
||||
Reference in New Issue
Block a user