Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/Actions/Application/StopApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ public function handle(Application $application, bool $previewDeployments = fals
: getCurrentApplicationContainerStatus($server, $application->id, 0);

$containersToStop = $containers->pluck('Names')->toArray();
$timeout = $application->settings->stop_grace_period ?? DEFAULT_STOP_GRACE_PERIOD_SECONDS;

foreach ($containersToStop as $containerName) {
instant_remote_process(command: [
"docker stop --time=30 $containerName",
"docker stop --time=$timeout $containerName",
"docker rm -f $containerName",
], server: $server, throwError: false);
}
Expand Down
4 changes: 3 additions & 1 deletion app/Actions/Application/StopApplicationOneServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ public function handle(Application $application, Server $server)
}
try {
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
$timeout = $application->settings->stop_grace_period ?? DEFAULT_STOP_GRACE_PERIOD_SECONDS;

if ($containers->count() > 0) {
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
instant_remote_process(
[
"docker stop --time=30 $containerName",
"docker stop --time=$timeout $containerName",
"docker rm -f $containerName",
],
$server
Expand Down
7 changes: 6 additions & 1 deletion app/Jobs/ApplicationDeploymentJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -2988,7 +2988,12 @@ private function build_image()
private function graceful_shutdown_container(string $containerName)
{
try {
$timeout = isDev() ? 1 : 30;
if (isDev()) {
$timeout = 1;
} else {
$timeout = $this->application->settings->stop_grace_period ?? DEFAULT_STOP_GRACE_PERIOD_SECONDS;
}

$this->execute_remote_command(
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
Expand Down
35 changes: 35 additions & 0 deletions app/Livewire/Project/Application/Advanced.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ class Advanced extends Component
#[Validate(['string', 'nullable'])]
public ?string $gpuOptions = null;

#[Validate(['string', 'nullable'])]
public ?string $stopGracePeriod = null;

#[Validate(['boolean'])]
public bool $isBuildServerEnabled = false;

Expand Down Expand Up @@ -135,6 +138,10 @@ public function syncData(bool $toModel = false)
$this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network;
$this->disableBuildCache = $this->application->settings->disable_build_cache;
}

// Load stop_grace_period separately since it has its own save handler
// Convert null to empty string to prevent dirty detection issues
$this->stopGracePeriod = $this->application->settings->stop_grace_period ?? '';
}
Comment on lines +142 to 145
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix type mismatch when loading stop grace period
stop_grace_period is cast to integer on the model, but here you assign it directly to public ?string $stopGracePeriod. When the stored value is, say, 60, Livewire will throw TypeError: Cannot assign int to property App\Livewire\Project\Application\Advanced::$stopGracePeriod of type ?string. Cast the value to string (or change the property type) before assignment, e.g.:

-        $this->stopGracePeriod = $this->application->settings->stop_grace_period ?? '';
+        $value = $this->application->settings->stop_grace_period;
+        $this->stopGracePeriod = $value === null ? '' : (string) $value;
🤖 Prompt for AI Agents
In app/Livewire/Project/Application/Advanced.php around lines 142 to 145,
assigning the integer model field stop_grace_period directly to the property
typed as ?string causes a TypeError in Livewire; convert the loaded value to a
string (e.g. (string)$this->application->settings->stop_grace_period) before
assigning, or alternatively change the property type to allow int, so that the
assignment matches the declared property type.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into this. I think it was working without a cast as I could refresh the page with a number configured and it worked.

The reason I use a string here is the user cannot set the current value to null after they have entered any content. So to fix problems with the change indicator incorrectly identifying an empty string as a change from the previous value of null I found it easiest to convert to string and back again after. Happy to adjust if someone has a suggestion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Jackman3005, you're right to be concerned about dirty detection, and your approach of converting null'' is sound for that use case. However, the explicit cast is still necessary for type safety.

What's likely happening is that PHP or Livewire is performing implicit type coercion in your current setup, which can work in many cases but isn't guaranteed and can break with strict type checking enabled. Since stop_grace_period is cast to integer on the model, when the value is, say, 60, you're assigning int(60) to a ?string property, which should trigger a TypeError in strict PHP 8+.

The bot's suggestion actually preserves your dirty detection fix while adding the necessary type safety:

$value = $this->application->settings->stop_grace_period;
$this->stopGracePeriod = $value === null ? '' : (string) $value;

This still converts null to '' (preventing dirty detection issues), but also explicitly casts non-null integers to strings (e.g., 60'60'), ensuring the assignment matches your ?string property type. Your saveStopGracePeriod() method already handles the reverse conversion ((int) $this->stopGracePeriod), so the round-trip behavior remains intact.

I'd recommend accepting the bot's suggestion—it maintains your intended behavior while being more robust.


private function resetDefaultLabels()
Expand Down Expand Up @@ -242,6 +249,34 @@ public function saveCustomName()
}
}

public function saveStopGracePeriod()
{
try {
$this->authorize('update', $this->application);

// Convert empty string to null, otherwise cast to integer
$value = ($this->stopGracePeriod === '' || $this->stopGracePeriod === null)
? null
: (int) $this->stopGracePeriod;

// Validate the integer value
if ($value !== null && ($value < 1 || $value > 3600)) {
$this->dispatch('error', 'Stop grace period must be between 1 and 3600 seconds.');

return;
}

// Save to model
$this->application->settings->stop_grace_period = $value;
$this->application->settings->save();

// User feedback
$this->dispatch('success', 'Stop grace period updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}

public function render()
{
return view('livewire.project.application.advanced');
Expand Down
3 changes: 2 additions & 1 deletion app/Livewire/Project/Application/Previews.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,11 @@ protected function setDeploymentUuid()
private function stopContainers(array $containers, $server)
{
$containersToStop = collect($containers)->pluck('Names')->toArray();
$timeout = $this->application->settings->stop_grace_period ?? DEFAULT_STOP_GRACE_PERIOD_SECONDS;

foreach ($containersToStop as $containerName) {
instant_remote_process(command: [
"docker stop --time=30 $containerName",
"docker stop --time=$timeout $containerName",
"docker rm -f $containerName",
], server: $server, throwError: false);
}
Expand Down
1 change: 1 addition & 0 deletions app/Models/ApplicationSetting.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class ApplicationSetting extends Model
'is_git_submodules_enabled' => 'boolean',
'is_git_lfs_enabled' => 'boolean',
'is_git_shallow_clone_enabled' => 'boolean',
'stop_grace_period' => 'integer',
];

protected $guarded = [];
Expand Down
1 change: 1 addition & 0 deletions bootstrap/helpers/constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'@yearly' => '0 0 1 1 *',
];
const RESTART_MODE = 'unless-stopped';
const DEFAULT_STOP_GRACE_PERIOD_SECONDS = 30;

const DATABASE_DOCKER_IMAGES = [
'bitnami/mariadb',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?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::table('application_settings', function (Blueprint $table) {
$table->integer('stop_grace_period')
->nullable()
->after('use_build_secrets')
->comment('Seconds to wait for graceful shutdown before forcing container stop (1-3600). Null uses default of 30 seconds.');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('stop_grace_period');
});
}
};
15 changes: 15 additions & 0 deletions resources/views/livewire/project/application/advanced.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@
helper="Readonly labels are disabled. You need to set the labels in the labels section." disabled
instantSave id="isStripprefixEnabled" label="Strip Prefixes" canGate="update" :canResource="$application" />
@endif
<h3 class="pt-4">Operations</h3>
<form class="flex items-end gap-2" wire:submit.prevent='saveStopGracePeriod'>
<x-forms.input
type="number"
id="stopGracePeriod"
label="Stop Grace Period (seconds)"
placeholder="{{ DEFAULT_STOP_GRACE_PERIOD_SECONDS }}"
helper="How long to wait for graceful shutdown during rolling updates, manual stops, and restarts. Applies to all containers for this application. Default: {{ DEFAULT_STOP_GRACE_PERIOD_SECONDS }} seconds. Range: 1-3600 seconds (1 hour)."
min="1"
max="3600"
canGate="update"
:canResource="$application"
/>
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
</form>
@if ($application->build_pack === 'dockercompose')
<h3>Docker Compose</h3>
<x-forms.checkbox instantSave id="isRawComposeDeploymentEnabled" label="Raw Compose Deployment"
Expand Down
23 changes: 23 additions & 0 deletions tests/Unit/ApplicationSettingStaticCastTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,26 @@
->and($casts[$field])->toBe('boolean');
}
});

it('casts stop_grace_period to integer', function () {
$setting = new ApplicationSetting;
$casts = $setting->getCasts();

expect($casts)->toHaveKey('stop_grace_period')
->and($casts['stop_grace_period'])->toBe('integer');
});

it('handles null stop_grace_period for default behavior', function () {
$setting = new ApplicationSetting;
$setting->stop_grace_period = null;

expect($setting->stop_grace_period)->toBeNull();
});

it('casts stop_grace_period from string to integer', function () {
$setting = new ApplicationSetting;
$setting->stop_grace_period = '60';

expect($setting->stop_grace_period)->toBe(60)
->and($setting->stop_grace_period)->toBeInt();
});