From bc9427ac33f0b3f8a303c5c62d72a2611d6579c6 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Fri, 24 Jul 2026 13:57:56 +0100 Subject: [PATCH] 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 --- Modules/CloudDevOps/Config/config.php | 9 ++ .../Commands/CloudDevOpsHealthCommand.php | 6 + .../Commands/CloudDevOpsPruneCommand.php | 5 + ...7_24_180000_create_cloud_devops_tables.php | 121 ++++++++++++++++++ .../Controllers/CloudDevOpsApiController.php | 7 + .../Controllers/CloudDevOpsController.php | 47 +++++++ .../Models/DeploymentEnvironment.php | 16 +++ .../Models/DeploymentHealthCheck.php | 4 + .../CloudDevOps/Models/DeploymentPipeline.php | 4 + .../CloudDevOps/Models/DeploymentRelease.php | 6 + .../CloudDevOps/Models/DeploymentRollback.php | 4 + .../CloudDevOps/Models/DeploymentSecret.php | 4 + Modules/CloudDevOps/Models/DeploymentStep.php | 4 + .../CloudDevOps/Models/DeploymentTarget.php | 4 + .../Providers/CloudDevOpsServiceProvider.php | 6 + .../Providers/RouteServiceProvider.php | 5 + .../Resources/views/index.blade.php | 44 +++++++ Modules/CloudDevOps/Routes/api.php | 4 + Modules/CloudDevOps/Routes/web.php | 14 ++ .../Services/DeploymentManager.php | 39 ++++++ .../Services/HealthCheckRunner.php | 5 + .../Tests/Feature/CloudDevOpsModuleTest.php | 6 + Modules/CloudDevOps/module.json | 9 ++ .../Database/Seeders/RolePermissionSeeder.php | 1 + modules_statuses.json | 3 +- .../components/layouts/app/sidebar.blade.php | 1 + 26 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 Modules/CloudDevOps/Config/config.php create mode 100644 Modules/CloudDevOps/Console/Commands/CloudDevOpsHealthCommand.php create mode 100644 Modules/CloudDevOps/Console/Commands/CloudDevOpsPruneCommand.php create mode 100644 Modules/CloudDevOps/Database/Migrations/2026_07_24_180000_create_cloud_devops_tables.php create mode 100644 Modules/CloudDevOps/Http/Controllers/CloudDevOpsApiController.php create mode 100644 Modules/CloudDevOps/Http/Controllers/CloudDevOpsController.php create mode 100644 Modules/CloudDevOps/Models/DeploymentEnvironment.php create mode 100644 Modules/CloudDevOps/Models/DeploymentHealthCheck.php create mode 100644 Modules/CloudDevOps/Models/DeploymentPipeline.php create mode 100644 Modules/CloudDevOps/Models/DeploymentRelease.php create mode 100644 Modules/CloudDevOps/Models/DeploymentRollback.php create mode 100644 Modules/CloudDevOps/Models/DeploymentSecret.php create mode 100644 Modules/CloudDevOps/Models/DeploymentStep.php create mode 100644 Modules/CloudDevOps/Models/DeploymentTarget.php create mode 100644 Modules/CloudDevOps/Providers/CloudDevOpsServiceProvider.php create mode 100644 Modules/CloudDevOps/Providers/RouteServiceProvider.php create mode 100644 Modules/CloudDevOps/Resources/views/index.blade.php create mode 100644 Modules/CloudDevOps/Routes/api.php create mode 100644 Modules/CloudDevOps/Routes/web.php create mode 100644 Modules/CloudDevOps/Services/DeploymentManager.php create mode 100644 Modules/CloudDevOps/Services/HealthCheckRunner.php create mode 100644 Modules/CloudDevOps/Tests/Feature/CloudDevOpsModuleTest.php create mode 100644 Modules/CloudDevOps/module.json diff --git a/Modules/CloudDevOps/Config/config.php b/Modules/CloudDevOps/Config/config.php new file mode 100644 index 0000000..8278777 --- /dev/null +++ b/Modules/CloudDevOps/Config/config.php @@ -0,0 +1,9 @@ + 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'], +]; diff --git a/Modules/CloudDevOps/Console/Commands/CloudDevOpsHealthCommand.php b/Modules/CloudDevOps/Console/Commands/CloudDevOpsHealthCommand.php new file mode 100644 index 0000000..2f0b3e8 --- /dev/null +++ b/Modules/CloudDevOps/Console/Commands/CloudDevOpsHealthCommand.php @@ -0,0 +1,6 @@ +each(fn($check)=>$runner->run($check)); $this->info('Health checks completed.'); return self::SUCCESS; } } diff --git a/Modules/CloudDevOps/Console/Commands/CloudDevOpsPruneCommand.php b/Modules/CloudDevOps/Console/Commands/CloudDevOpsPruneCommand.php new file mode 100644 index 0000000..c709989 --- /dev/null +++ b/Modules/CloudDevOps/Console/Commands/CloudDevOpsPruneCommand.php @@ -0,0 +1,5 @@ +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; } } diff --git a/Modules/CloudDevOps/Database/Migrations/2026_07_24_180000_create_cloud_devops_tables.php b/Modules/CloudDevOps/Database/Migrations/2026_07_24_180000_create_cloud_devops_tables.php new file mode 100644 index 0000000..abcfa5f --- /dev/null +++ b/Modules/CloudDevOps/Database/Migrations/2026_07_24_180000_create_cloud_devops_tables.php @@ -0,0 +1,121 @@ +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'); + } +}; diff --git a/Modules/CloudDevOps/Http/Controllers/CloudDevOpsApiController.php b/Modules/CloudDevOps/Http/Controllers/CloudDevOpsApiController.php new file mode 100644 index 0000000..806ac3d --- /dev/null +++ b/Modules/CloudDevOps/Http/Controllers/CloudDevOpsApiController.php @@ -0,0 +1,7 @@ +json(['data'=>DeploymentEnvironment::withCount(['targets','pipelines','releases'])->get()]); } public function releases(): JsonResponse { return response()->json(['data'=>DeploymentRelease::with(['environment','steps'])->latest()->paginate()]); } } diff --git a/Modules/CloudDevOps/Http/Controllers/CloudDevOpsController.php b/Modules/CloudDevOps/Http/Controllers/CloudDevOpsController.php new file mode 100644 index 0000000..ac3b1bc --- /dev/null +++ b/Modules/CloudDevOps/Http/Controllers/CloudDevOpsController.php @@ -0,0 +1,47 @@ + 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.'); + } +} diff --git a/Modules/CloudDevOps/Models/DeploymentEnvironment.php b/Modules/CloudDevOps/Models/DeploymentEnvironment.php new file mode 100644 index 0000000..bc6afd0 --- /dev/null +++ b/Modules/CloudDevOps/Models/DeploymentEnvironment.php @@ -0,0 +1,16 @@ + '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'); } +} diff --git a/Modules/CloudDevOps/Models/DeploymentHealthCheck.php b/Modules/CloudDevOps/Models/DeploymentHealthCheck.php new file mode 100644 index 0000000..ec35a73 --- /dev/null +++ b/Modules/CloudDevOps/Models/DeploymentHealthCheck.php @@ -0,0 +1,4 @@ + 'datetime']; } } diff --git a/Modules/CloudDevOps/Models/DeploymentPipeline.php b/Modules/CloudDevOps/Models/DeploymentPipeline.php new file mode 100644 index 0000000..904bfc2 --- /dev/null +++ b/Modules/CloudDevOps/Models/DeploymentPipeline.php @@ -0,0 +1,4 @@ + 'array', 'requires_approval' => 'boolean', 'is_active' => 'boolean']; } } diff --git a/Modules/CloudDevOps/Models/DeploymentRelease.php b/Modules/CloudDevOps/Models/DeploymentRelease.php new file mode 100644 index 0000000..4faee10 --- /dev/null +++ b/Modules/CloudDevOps/Models/DeploymentRelease.php @@ -0,0 +1,6 @@ + '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'); } } diff --git a/Modules/CloudDevOps/Models/DeploymentRollback.php b/Modules/CloudDevOps/Models/DeploymentRollback.php new file mode 100644 index 0000000..370b8cb --- /dev/null +++ b/Modules/CloudDevOps/Models/DeploymentRollback.php @@ -0,0 +1,4 @@ + 'datetime']; } } diff --git a/Modules/CloudDevOps/Models/DeploymentStep.php b/Modules/CloudDevOps/Models/DeploymentStep.php new file mode 100644 index 0000000..514faaa --- /dev/null +++ b/Modules/CloudDevOps/Models/DeploymentStep.php @@ -0,0 +1,4 @@ + 'datetime', 'completed_at' => 'datetime']; } } diff --git a/Modules/CloudDevOps/Models/DeploymentTarget.php b/Modules/CloudDevOps/Models/DeploymentTarget.php new file mode 100644 index 0000000..86efbd3 --- /dev/null +++ b/Modules/CloudDevOps/Models/DeploymentTarget.php @@ -0,0 +1,4 @@ + 'encrypted:array', 'is_active' => 'boolean']; } } diff --git a/Modules/CloudDevOps/Providers/CloudDevOpsServiceProvider.php b/Modules/CloudDevOps/Providers/CloudDevOpsServiceProvider.php new file mode 100644 index 0000000..0f0dd67 --- /dev/null +++ b/Modules/CloudDevOps/Providers/CloudDevOpsServiceProvider.php @@ -0,0 +1,6 @@ +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]); } } } diff --git a/Modules/CloudDevOps/Providers/RouteServiceProvider.php b/Modules/CloudDevOps/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..8c0792c --- /dev/null +++ b/Modules/CloudDevOps/Providers/RouteServiceProvider.php @@ -0,0 +1,5 @@ +group(module_path('CloudDevOps','Routes/web.php')); Route::middleware('api')->prefix('api')->group(module_path('CloudDevOps','Routes/api.php')); } } diff --git a/Modules/CloudDevOps/Resources/views/index.blade.php b/Modules/CloudDevOps/Resources/views/index.blade.php new file mode 100644 index 0000000..cefb0ce --- /dev/null +++ b/Modules/CloudDevOps/Resources/views/index.blade.php @@ -0,0 +1,44 @@ + +
+
+ Cloud & DevOps + Manage environments, deployment releases, pipelines and service health. +
+ + @if (session('status')) + {{ session('status') }} + @endif + +
+

Environments

{{ $environments->count() }}

+

Recent releases

{{ $releases->count() }}

+

Healthy checks

{{ $checks->where('status', 'healthy')->count() }}

+
+ + @can('cloud-devops.manage') +
+ @csrf + + + +
Create environment
+ + @endcan + +
+ Environments +
+ @forelse ($environments as $environment) +
+

{{ $environment->name }}

{{ ucfirst($environment->type) }} ยท {{ ucfirst($environment->status) }}

@if ($environment->is_protected)Protected@endif
+
Targets

{{ $environment->targets_count }}

Pipelines

{{ $environment->pipelines_count }}

Releases

{{ $environment->releases_count }}

+
+ @empty +
No deployment environments configured.
+ @endforelse +
+
+ +
Recent releases
@forelse ($releases as $release)@empty@endforelse
VersionEnvironmentStatusCreated
{{ $release->version }}{{ $release->environment?->name }}{{ ucfirst(str_replace('_', ' ', $release->status)) }}{{ $release->created_at?->diffForHumans() }}
No releases yet.
+
+
diff --git a/Modules/CloudDevOps/Routes/api.php b/Modules/CloudDevOps/Routes/api.php new file mode 100644 index 0000000..c9a14aa --- /dev/null +++ b/Modules/CloudDevOps/Routes/api.php @@ -0,0 +1,4 @@ +prefix('v1/cloud-devops')->group(function (): void { Route::get('/environments',[CloudDevOpsApiController::class,'environments']); Route::get('/releases',[CloudDevOpsApiController::class,'releases']); }); diff --git a/Modules/CloudDevOps/Routes/web.php b/Modules/CloudDevOps/Routes/web.php new file mode 100644 index 0000000..fb5ebf6 --- /dev/null +++ b/Modules/CloudDevOps/Routes/web.php @@ -0,0 +1,14 @@ +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'); + }); diff --git a/Modules/CloudDevOps/Services/DeploymentManager.php b/Modules/CloudDevOps/Services/DeploymentManager.php new file mode 100644 index 0000000..6f12688 --- /dev/null +++ b/Modules/CloudDevOps/Services/DeploymentManager.php @@ -0,0 +1,39 @@ +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(); + } +} diff --git a/Modules/CloudDevOps/Services/HealthCheckRunner.php b/Modules/CloudDevOps/Services/HealthCheckRunner.php new file mode 100644 index 0000000..f967f9c --- /dev/null +++ b/Modules/CloudDevOps/Services/HealthCheckRunner.php @@ -0,0 +1,5 @@ +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(); } } diff --git a/Modules/CloudDevOps/Tests/Feature/CloudDevOpsModuleTest.php b/Modules/CloudDevOps/Tests/Feature/CloudDevOpsModuleTest.php new file mode 100644 index 0000000..511ef18 --- /dev/null +++ b/Modules/CloudDevOps/Tests/Feature/CloudDevOpsModuleTest.php @@ -0,0 +1,6 @@ +'Production','slug'=>'production','type'=>'production']); $this->assertDatabaseHas('deployment_environments',['id'=>$environment->id,'slug'=>'production']); } } diff --git a/Modules/CloudDevOps/module.json b/Modules/CloudDevOps/module.json new file mode 100644 index 0000000..5a14f70 --- /dev/null +++ b/Modules/CloudDevOps/module.json @@ -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": [] +} diff --git a/Modules/Roles/Database/Seeders/RolePermissionSeeder.php b/Modules/Roles/Database/Seeders/RolePermissionSeeder.php index ea2284d..9322669 100644 --- a/Modules/Roles/Database/Seeders/RolePermissionSeeder.php +++ b/Modules/Roles/Database/Seeders/RolePermissionSeeder.php @@ -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) { diff --git a/modules_statuses.json b/modules_statuses.json index 83eb280..a1d1713 100644 --- a/modules_statuses.json +++ b/modules_statuses.json @@ -43,5 +43,6 @@ "AI": true, "Analytics": true, "Builder": true, - "Marketplace": true + "Marketplace": true, + "CloudDevOps": true } diff --git a/resources/themes/common/views/components/layouts/app/sidebar.blade.php b/resources/themes/common/views/components/layouts/app/sidebar.blade.php index d29a7af..a5ef366 100644 --- a/resources/themes/common/views/components/layouts/app/sidebar.blade.php +++ b/resources/themes/common/views/components/layouts/app/sidebar.blade.php @@ -42,6 +42,7 @@ {{ __('Review Queue') }} {{ __('Users') }}{{ __('Roles & Permissions') }}{{ __('Media') }}{{ __('Search') }} {{ __('Localisation') }}{{ __('SEO') }}{{ __('Settings') }}{{ __('Modules') }}{{ __('Audit Log') }} +{{ __('Cloud & DevOps') }}