From 5b0e55c4b29368d5e95fab37d1f057ae9034f606 Mon Sep 17 00:00:00 2001 From: jon brookes Date: Sun, 15 Feb 2026 20:10:18 +0000 Subject: [PATCH] feat: implement Change resource with CRUD functionality and migration update image tags to v0.0.8 in build scripts and docker-compose files --- .woodpecker/share-lt-build.yaml | 10 +-- .../Resources/Changes/ChangeResource.php | 53 +++++++++++++ .../Resources/Changes/Pages/CreateChange.php | 18 +++++ .../Resources/Changes/Pages/EditChange.php | 16 ++++ .../Resources/Changes/Pages/ListChanges.php | 19 +++++ .../Resources/Changes/Schemas/ChangeForm.php | 28 +++++++ .../Resources/Changes/Tables/ChangesTable.php | 34 ++++++++ app/Jobs/ProcessChangeUpdate.php | 36 +++++++++ app/Jobs/ProcessEntryUpdate.php | 79 ++----------------- app/Models/Change.php | 19 +++++ app/Models/User.php | 11 ++- app/Observers/ChangeObserver.php | 49 ++++++++++++ app/Providers/AppServiceProvider.php | 3 + app/Services/ProcessUpdateService.php | 79 +++++++++++++++++++ cmd/build_prod_container.sh | 2 +- ...2026_02_15_192829_create_changes_table.php | 30 +++++++ docs/docker/create_nats_stream.sh | 4 +- docs/docker/docker-compose-cloudflard.yaml | 2 +- docs/docker/docker-compose-nats.yaml | 2 +- docs/docker/docker-compose-prod.yaml | 2 +- 20 files changed, 408 insertions(+), 88 deletions(-) create mode 100644 app/Filament/Resources/Changes/ChangeResource.php create mode 100644 app/Filament/Resources/Changes/Pages/CreateChange.php create mode 100644 app/Filament/Resources/Changes/Pages/EditChange.php create mode 100644 app/Filament/Resources/Changes/Pages/ListChanges.php create mode 100644 app/Filament/Resources/Changes/Schemas/ChangeForm.php create mode 100644 app/Filament/Resources/Changes/Tables/ChangesTable.php create mode 100644 app/Jobs/ProcessChangeUpdate.php create mode 100644 app/Models/Change.php create mode 100644 app/Observers/ChangeObserver.php create mode 100644 app/Services/ProcessUpdateService.php create mode 100644 database/migrations/2026_02_15_192829_create_changes_table.php diff --git a/.woodpecker/share-lt-build.yaml b/.woodpecker/share-lt-build.yaml index 5054aa6..f1963ca 100644 --- a/.woodpecker/share-lt-build.yaml +++ b/.woodpecker/share-lt-build.yaml @@ -14,10 +14,10 @@ steps: - docker pull quay.io/marshyon/share-lt:latest || true - echo "Building image for testing (amd64 only for CI compatibility)..." - docker build --platform linux/amd64 --cache-from=quay.io/marshyon/share-lt:latest -t share-lt:test . - - echo "Tagging test image as quay.io/marshyon/share-lt:v0.0.7..." - - docker tag share-lt:test quay.io/marshyon/share-lt:v0.0.7 + - echo "Tagging test image as quay.io/marshyon/share-lt:v0.0.8..." + - docker tag share-lt:test quay.io/marshyon/share-lt:v0.0.8 - echo "Generating SBOM..." - - docker run --rm -v /var/run/docker.sock:/var/run/docker.sock anchore/syft:latest scan quay.io/marshyon/share-lt:v0.0.7 -o cyclonedx-json > sbom.json + - docker run --rm -v /var/run/docker.sock:/var/run/docker.sock anchore/syft:latest scan quay.io/marshyon/share-lt:v0.0.8 -o cyclonedx-json > sbom.json scan-vulnerabilities: image: aquasec/trivy:0.67.2 volumes: @@ -43,7 +43,7 @@ steps: repo: quay.io/marshyon/share-lt platforms: linux/amd64 tags: - - v0.0.7 + - v0.0.8 - latest username: from_secret: QUAY_USERNAME @@ -59,6 +59,6 @@ steps: COSIGN_REGISTRY_PASSWORD: from_secret: QUAY_PASSWORD commands: - - cosign attach sbom --sbom sbom.json quay.io/marshyon/share-lt:v0.0.7 || echo "SBOM attach failed" + - cosign attach sbom --sbom sbom.json quay.io/marshyon/share-lt:v0.0.8 || echo "SBOM attach failed" - echo "Done - trivy report saved to workspace for manual review" diff --git a/app/Filament/Resources/Changes/ChangeResource.php b/app/Filament/Resources/Changes/ChangeResource.php new file mode 100644 index 0000000..5fb9dcf --- /dev/null +++ b/app/Filament/Resources/Changes/ChangeResource.php @@ -0,0 +1,53 @@ + ListChanges::route('/'), + 'create' => CreateChange::route('/create'), + 'edit' => EditChange::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/Changes/Pages/CreateChange.php b/app/Filament/Resources/Changes/Pages/CreateChange.php new file mode 100644 index 0000000..25d1806 --- /dev/null +++ b/app/Filament/Resources/Changes/Pages/CreateChange.php @@ -0,0 +1,18 @@ +id(); + + return $data; + } +} diff --git a/app/Filament/Resources/Changes/Pages/EditChange.php b/app/Filament/Resources/Changes/Pages/EditChange.php new file mode 100644 index 0000000..93e57a0 --- /dev/null +++ b/app/Filament/Resources/Changes/Pages/EditChange.php @@ -0,0 +1,16 @@ +components([ + Textarea::make('note') + ->label('Note') + ->required(), + FormSelect::make('type') + ->label('Type') + ->options([ + 'go-live' => 'Go Live', + 'general' => 'General', + ]) + ->default('go-live') + ->required(), + ]); + } +} diff --git a/app/Filament/Resources/Changes/Tables/ChangesTable.php b/app/Filament/Resources/Changes/Tables/ChangesTable.php new file mode 100644 index 0000000..f8b99dc --- /dev/null +++ b/app/Filament/Resources/Changes/Tables/ChangesTable.php @@ -0,0 +1,34 @@ +columns([ + TextColumn::make('note') + ->label('Note') + ->wrap(), + TextColumn::make('type') + ->label('Type'), + TextColumn::make('user.name') + ->label('User'), + ]) + ->filters([ + // + ]) + ->recordActions([]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Jobs/ProcessChangeUpdate.php b/app/Jobs/ProcessChangeUpdate.php new file mode 100644 index 0000000..4f937ed --- /dev/null +++ b/app/Jobs/ProcessChangeUpdate.php @@ -0,0 +1,36 @@ +changeId}, action={$this->action}"); + (new ProcessUpdateService)->processUpdate($this->changeId, $this->action, $this->type); + } +} diff --git a/app/Jobs/ProcessEntryUpdate.php b/app/Jobs/ProcessEntryUpdate.php index 2c13762..f97ea07 100644 --- a/app/Jobs/ProcessEntryUpdate.php +++ b/app/Jobs/ProcessEntryUpdate.php @@ -2,16 +2,12 @@ namespace App\Jobs; -use App\Models\Entry; +use App\Services\ProcessUpdateService; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Queue\Queueable; -use Illuminate\Support\Facades\Log; - use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Foundation\Queue\Queueable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Process; -use Symfony\Component\Console\Exception\RuntimeException; class ProcessEntryUpdate implements ShouldQueue { @@ -20,11 +16,10 @@ class ProcessEntryUpdate implements ShouldQueue /** * Create a new job instance. */ - - public function __construct( public int $entryId, - public string $action + public string $action, + public string $type = 'entry_update_' ) { // } @@ -34,70 +29,6 @@ class ProcessEntryUpdate implements ShouldQueue */ public function handle(): void { - - $subject = env('NATS_SUBJECT', 'entry_update'); - $appUrl = env('APP_URL', 'http://localhost'); - - $incoming = [ - 'id' => $this->entryId, - 'action' => $this->action, - 'subject' => $subject, - 'app_url' => $appUrl, - ]; - - $jsonData = json_encode($incoming, JSON_THROW_ON_ERROR); - $tempFile = tempnam(sys_get_temp_dir(), 'entry_update_'); - file_put_contents($tempFile, $jsonData); - - $scriptPath = env('HANDLE_ENTRY_UPDATES_SCRIPT', base_path('cmd/handle_cms_updates.sh')); - - try { - // Log what we're about to execute - Log::info("Executing script: {$scriptPath} with action: {$this->action}", [ - 'entry_id' => $this->entryId, - 'temp_file' => $tempFile, - 'script_exists' => file_exists($scriptPath), - 'script_executable' => is_executable($scriptPath), - ]); - - $result = Process::run([ - 'bash', - $scriptPath, - $this->action, - $tempFile - ]); - - if ($result->failed()) { - $errorDetails = [ - 'exit_code' => $result->exitCode(), - 'stdout' => $result->output(), - 'stderr' => $result->errorOutput(), - 'command' => ['bash', $scriptPath, $this->action, $tempFile], - 'script_path' => $scriptPath, - 'script_exists' => file_exists($scriptPath), - 'temp_file_exists' => file_exists($tempFile), - 'temp_file_contents' => file_exists($tempFile) ? file_get_contents($tempFile) : 'N/A', - ]; - - Log::error("Script execution failed", $errorDetails); - - throw new RuntimeException( - "Script execution failed with exit code {$result->exitCode()}. " . - "STDOUT: " . ($result->output() ?: 'empty') . " " . - "STDERR: " . ($result->errorOutput() ?: 'empty') - ); - } - - Log::info("Script executed successfully", [ - 'stdout' => $result->output(), - 'entry_id' => $this->entryId, - 'action' => $this->action, - ]); - } finally { - // Clean up temp file - if (file_exists($tempFile)) { - unlink($tempFile); - } - } + (new ProcessUpdateService)->processUpdate($this->entryId, $this->action, $this->type); } } diff --git a/app/Models/Change.php b/app/Models/Change.php new file mode 100644 index 0000000..98a741f --- /dev/null +++ b/app/Models/Change.php @@ -0,0 +1,19 @@ +belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index a57eaa1..de4961a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,11 +6,9 @@ namespace App\Models; use Filament\Models\Contracts\FilamentUser; use Filament\Panel; -use Illuminate\Container\Attributes\Log; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -use Illuminate\Support\Facades\Log as FacadesLog; use Illuminate\Support\Str; use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Sanctum\HasApiTokens; @@ -18,7 +16,7 @@ use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable implements FilamentUser { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable, TwoFactorAuthenticatable, HasApiTokens; + use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable; /** * The attributes that are mass assignable. @@ -64,7 +62,7 @@ class User extends Authenticatable implements FilamentUser return Str::of($this->name) ->explode(' ') ->take(2) - ->map(fn($word) => Str::substr($word, 0, 1)) + ->map(fn ($word) => Str::substr($word, 0, 1)) ->implode(''); } @@ -75,4 +73,9 @@ class User extends Authenticatable implements FilamentUser { return $this->email === config('app.admin_email') || $this->role === 'admin'; } + + public function changes() + { + return $this->hasMany(Change::class); + } } diff --git a/app/Observers/ChangeObserver.php b/app/Observers/ChangeObserver.php new file mode 100644 index 0000000..e86990b --- /dev/null +++ b/app/Observers/ChangeObserver.php @@ -0,0 +1,49 @@ +id, 'created'); + } + + /** + * Handle the Change "updated" event. + */ + public function updated(Change $change): void + { + ProcessChangeUpdate::dispatch($change->id, 'updated'); + } + + /** + * Handle the Change "deleted" event. + */ + public function deleted(Change $change): void + { + // + } + + /** + * Handle the Change "restored" event. + */ + public function restored(Change $change): void + { + // + } + + /** + * Handle the Change "force deletecURLd" event. + */ + public function forceDeleted(Change $change): void + { + // + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index c508e75..0fad18d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,8 @@ namespace App\Providers; use App\Models\Entry; use App\Observers\EntryObserver; +use App\Models\Change; +use App\Observers\ChangeObserver; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\File; @@ -29,5 +31,6 @@ class AppServiceProvider extends ServiceProvider } Entry::observe(EntryObserver::class); + Change::observe(ChangeObserver::class); } } diff --git a/app/Services/ProcessUpdateService.php b/app/Services/ProcessUpdateService.php new file mode 100644 index 0000000..4082ad5 --- /dev/null +++ b/app/Services/ProcessUpdateService.php @@ -0,0 +1,79 @@ + $entryId, + 'action' => $action, + 'subject' => $subject, + 'app_url' => $appUrl, + 'type' => $type, + ]; + + $jsonData = json_encode($incoming, JSON_THROW_ON_ERROR); + $tempFile = tempnam(sys_get_temp_dir(), $type); + file_put_contents($tempFile, $jsonData); + + $scriptPath = env('HANDLE_ENTRY_UPDATES_SCRIPT', base_path('cmd/handle_cms_updates.sh')); + + try { + // Log what we're about to execute + Log::info("Executing script: {$scriptPath} with action: {$action}", [ + 'entry_id' => $entryId, + 'temp_file' => $tempFile, + 'script_exists' => file_exists($scriptPath), + 'script_executable' => is_executable($scriptPath), + ]); + + $result = Process::run([ + 'bash', + $scriptPath, + $action, + $tempFile, + ]); + + if ($result->failed()) { + $errorDetails = [ + 'exit_code' => $result->exitCode(), + 'stdout' => $result->output(), + 'stderr' => $result->errorOutput(), + 'command' => ['bash', $scriptPath, $action, $tempFile], + 'script_path' => $scriptPath, + 'script_exists' => file_exists($scriptPath), + 'temp_file_exists' => file_exists($tempFile), + 'temp_file_contents' => file_exists($tempFile) ? file_get_contents($tempFile) : 'N/A', + ]; + + Log::error('Script execution failed', $errorDetails); + + throw new RuntimeException( + "Script execution failed with exit code {$result->exitCode()}. " . + 'STDOUT: ' . ($result->output() ?: 'empty') . ' ' . + 'STDERR: ' . ($result->errorOutput() ?: 'empty') + ); + } + + Log::info('Script executed successfully', [ + 'stdout' => $result->output(), + 'entry_id' => $entryId, + 'action' => $action, + ]); + } finally { + // Clean up temp file + if (file_exists($tempFile)) { + unlink($tempFile); + } + } + } +} diff --git a/cmd/build_prod_container.sh b/cmd/build_prod_container.sh index 49bc2b7..c33bddc 100755 --- a/cmd/build_prod_container.sh +++ b/cmd/build_prod_container.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash LARAVEL_CONTAINER_NAME="quay.io/marshyon/share-lt" -CONTAINER_LABEL="v0.0.7" +CONTAINER_LABEL="v0.0.8" CACHE="--no-cache" # CACHE="" diff --git a/database/migrations/2026_02_15_192829_create_changes_table.php b/database/migrations/2026_02_15_192829_create_changes_table.php new file mode 100644 index 0000000..cc5bc8f --- /dev/null +++ b/database/migrations/2026_02_15_192829_create_changes_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('note'); + $table->integer('user_id'); + $table->string('type')->default('general'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('changes'); + } +}; diff --git a/docs/docker/create_nats_stream.sh b/docs/docker/create_nats_stream.sh index 53312f6..f0973a2 100755 --- a/docs/docker/create_nats_stream.sh +++ b/docs/docker/create_nats_stream.sh @@ -30,7 +30,9 @@ fi nats \ stream add "$NATS_STREAM" \ --user ruser --password $NATS_PASSWORD \ - --subjects "$NATS_SUBJECT" \ + --subjects NATS_SUBJECT1 \ + --subjects NATS_SUBJECT2 \ + --subjects NATS_SUBJECT3 \ --storage file \ --dupe-window="2m" \ --replicas=1 \ diff --git a/docs/docker/docker-compose-cloudflard.yaml b/docs/docker/docker-compose-cloudflard.yaml index 8f5c26a..3fa6484 100644 --- a/docs/docker/docker-compose-cloudflard.yaml +++ b/docs/docker/docker-compose-cloudflard.yaml @@ -25,7 +25,7 @@ services: # - nats app: - image: quay.io/marshyon/share-lt:v0.0.7 + image: quay.io/marshyon/share-lt:v0.0.8 restart: unless-stopped tty: false working_dir: /var/www diff --git a/docs/docker/docker-compose-nats.yaml b/docs/docker/docker-compose-nats.yaml index 0f2e56c..9c85e62 100644 --- a/docs/docker/docker-compose-nats.yaml +++ b/docs/docker/docker-compose-nats.yaml @@ -19,4 +19,4 @@ volumes: networks: nats: - external: true \ No newline at end of file + external: true diff --git a/docs/docker/docker-compose-prod.yaml b/docs/docker/docker-compose-prod.yaml index 1ce7b09..5e3d93c 100644 --- a/docs/docker/docker-compose-prod.yaml +++ b/docs/docker/docker-compose-prod.yaml @@ -1,6 +1,6 @@ services: app: - image: quay.io/marshyon/share-lt:v0.0.7 + image: quay.io/marshyon/share-lt:v0.0.8 restart: unless-stopped tty: false working_dir: /var/www