feat: implement Change resource with CRUD functionality and migration

update image tags to v0.0.8 in build scripts and docker-compose files
This commit is contained in:
jon brookes 2026-02-15 20:10:18 +00:00
parent 64a7d1d2f4
commit 5b0e55c4b2
20 changed files with 408 additions and 88 deletions

View file

@ -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"

View file

@ -0,0 +1,53 @@
<?php
namespace App\Filament\Resources\Changes;
use App\Filament\Resources\Changes\Pages\CreateChange;
use App\Filament\Resources\Changes\Pages\EditChange;
use App\Filament\Resources\Changes\Pages\ListChanges;
use App\Filament\Resources\Changes\Schemas\ChangeForm;
use App\Filament\Resources\Changes\Tables\ChangesTable;
use App\Models\Change as ModelsChange;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class ChangeResource extends Resource
{
protected static ?string $model = ModelsChange::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::ArrowUpOnSquareStack;
public static function getNavigationGroup(): ?string
{
return 'Settings';
}
public static function form(Schema $schema): Schema
{
return ChangeForm::configure($schema);
}
public static function table(Table $table): Table
{
return ChangesTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListChanges::route('/'),
'create' => CreateChange::route('/create'),
'edit' => EditChange::route('/{record}/edit'),
];
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Filament\Resources\Changes\Pages;
use App\Filament\Resources\Changes\ChangeResource;
use Filament\Resources\Pages\CreateRecord;
class CreateChange extends CreateRecord
{
protected static string $resource = ChangeResource::class;
protected function mutateFormDataBeforeCreate(array $data): array
{
$data['user_id'] = auth()->id();
return $data;
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Filament\Resources\Changes\Pages;
use App\Filament\Resources\Changes\ChangeResource;
use Filament\Resources\Pages\EditRecord;
class EditChange extends EditRecord
{
protected static string $resource = ChangeResource::class;
protected function getHeaderActions(): array
{
return [];
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Changes\Pages;
use App\Filament\Resources\Changes\ChangeResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListChanges extends ListRecords
{
protected static string $resource = ChangeResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Filament\Resources\Changes\Schemas;
use Filament\Forms\Components\Select as FormSelect;
use Filament\Forms\Components\Textarea;
use Filament\Schemas\Schema;
class ChangeForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Textarea::make('note')
->label('Note')
->required(),
FormSelect::make('type')
->label('Type')
->options([
'go-live' => 'Go Live',
'general' => 'General',
])
->default('go-live')
->required(),
]);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Filament\Resources\Changes\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class ChangesTable
{
public static function configure(Table $table): Table
{
return $table
->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(),
]),
]);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Jobs;
use App\Services\ProcessUpdateService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ProcessChangeUpdate implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(
public int $changeId,
public string $action,
public string $type = 'change_update_'
) {
//
}
/**
* Execute the job.
*/
public function handle(): void
{
Log::info("Processing change update: changeId={$this->changeId}, action={$this->action}");
(new ProcessUpdateService)->processUpdate($this->changeId, $this->action, $this->type);
}
}

View file

@ -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);
}
}

19
app/Models/Change.php Normal file
View file

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Change extends Model
{
protected $fillable = [
'note',
'user_id',
'type',
];
public function user()
{
return $this->belongsTo(User::class);
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace App\Observers;
use App\Jobs\ProcessChangeUpdate;
use App\Models\Change;
class ChangeObserver
{
/**
* Handle the Change "created" event.
*/
public function created(Change $change): void
{
ProcessChangeUpdate::dispatch($change->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
{
//
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Symfony\Component\Console\Exception\RuntimeException;
class ProcessUpdateService
{
public function processUpdate(int $entryId, string $action, string $type = 'entry_update_'): void
{
$subject = env('NATS_SUBJECT', 'entry_update');
$appUrl = env('APP_URL', 'http://localhost');
$incoming = [
'id' => $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);
}
}
}
}

View file

@ -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=""

View file

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('changes', function (Blueprint $table) {
$table->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');
}
};

View file

@ -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 \

View file

@ -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

View file

@ -19,4 +19,4 @@ volumes:
networks:
nats:
external: true
external: true

View file

@ -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