v4.0.0-beta.459 (#7750)

This commit is contained in:
Andras Bacsai 2025-12-23 15:40:27 +01:00 committed by GitHub
commit b7e0f5577d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 211 additions and 39 deletions

View file

@ -0,0 +1,81 @@
<?php
namespace App\Console\Commands\Cloud;
use App\Jobs\SyncStripeSubscriptionsJob;
use Illuminate\Console\Command;
class SyncStripeSubscriptions extends Command
{
protected $signature = 'cloud:sync-stripe-subscriptions {--fix : Actually fix discrepancies (default is check only)}';
protected $description = 'Sync subscription status with Stripe. By default only checks, use --fix to apply changes.';
public function handle(): int
{
if (! isCloud()) {
$this->error('This command can only be run on Coolify Cloud.');
return 1;
}
if (! isStripe()) {
$this->error('Stripe is not configured.');
return 1;
}
$fix = $this->option('fix');
if ($fix) {
$this->warn('Running with --fix: discrepancies will be corrected.');
} else {
$this->info('Running in check mode (no changes will be made). Use --fix to apply corrections.');
}
$this->newLine();
$job = new SyncStripeSubscriptionsJob($fix);
$result = $job->handle();
if (isset($result['error'])) {
$this->error($result['error']);
return 1;
}
$this->info("Total subscriptions checked: {$result['total_checked']}");
$this->newLine();
if (count($result['discrepancies']) > 0) {
$this->warn('Discrepancies found: '.count($result['discrepancies']));
$this->newLine();
foreach ($result['discrepancies'] as $discrepancy) {
$this->line(" - Subscription ID: {$discrepancy['subscription_id']}");
$this->line(" Team ID: {$discrepancy['team_id']}");
$this->line(" Stripe ID: {$discrepancy['stripe_subscription_id']}");
$this->line(" Stripe Status: {$discrepancy['stripe_status']}");
$this->newLine();
}
if ($fix) {
$this->info('All discrepancies have been fixed.');
} else {
$this->comment('Run with --fix to correct these discrepancies.');
}
} else {
$this->info('No discrepancies found. All subscriptions are in sync.');
}
if (count($result['errors']) > 0) {
$this->newLine();
$this->error('Errors encountered: '.count($result['errors']));
foreach ($result['errors'] as $error) {
$this->line(" - Subscription {$error['subscription_id']}: {$error['error']}");
}
}
return 0;
}
}

View file

@ -371,7 +371,7 @@ public function handle(): void
try {
$this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}");
$this->graceful_shutdown_container($this->deployment_uuid);
$this->graceful_shutdown_container($this->deployment_uuid, skipRemove: true);
} catch (Exception $e) {
// Log but don't fail - container cleanup errors are expected when container is already gone
\Log::warning('Failed to shutdown container '.$this->deployment_uuid.': '.$e->getMessage());
@ -1968,7 +1968,7 @@ private function prepare_builder_image(bool $firstTry = true)
$this->application_deployment_queue->addLogEntry('Preparing container with helper image with updated envs.');
}
$this->graceful_shutdown_container($this->deployment_uuid);
$this->graceful_shutdown_container($this->deployment_uuid, skipRemove: true);
$this->execute_remote_command(
[
$runCommand,
@ -1983,8 +1983,8 @@ private function prepare_builder_image(bool $firstTry = true)
private function restart_builder_container_with_actual_commit()
{
// Stop and remove the current helper container
$this->graceful_shutdown_container($this->deployment_uuid);
// Stop the current helper container (no need for rm -f as it was started with --rm)
$this->graceful_shutdown_container($this->deployment_uuid, skipRemove: true);
// Clear cached env_args to force regeneration with actual SOURCE_COMMIT value
$this->env_args = null;
@ -3171,14 +3171,20 @@ private function build_image()
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
}
private function graceful_shutdown_container(string $containerName)
private function graceful_shutdown_container(string $containerName, bool $skipRemove = false)
{
try {
$timeout = isDev() ? 1 : 30;
$this->execute_remote_command(
["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
);
if ($skipRemove) {
$this->execute_remote_command(
["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true]
);
} else {
$this->execute_remote_command(
["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
);
}
} catch (Exception $error) {
$this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr');
}

View file

@ -0,0 +1,92 @@
<?php
namespace App\Jobs;
use App\Models\Subscription;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SyncStripeSubscriptionsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 1;
public int $timeout = 1800; // 30 minutes max
public function __construct(public bool $fix = false)
{
$this->onQueue('high');
}
public function handle(): array
{
if (! isCloud() || ! isStripe()) {
return ['error' => 'Not running on Cloud or Stripe not configured'];
}
$subscriptions = Subscription::whereNotNull('stripe_subscription_id')
->where('stripe_invoice_paid', true)
->get();
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$discrepancies = [];
$errors = [];
foreach ($subscriptions as $subscription) {
try {
$stripeSubscription = $stripe->subscriptions->retrieve(
$subscription->stripe_subscription_id
);
// Check if Stripe says cancelled but we think it's active
if (in_array($stripeSubscription->status, ['canceled', 'incomplete_expired', 'unpaid'])) {
$discrepancies[] = [
'subscription_id' => $subscription->id,
'team_id' => $subscription->team_id,
'stripe_subscription_id' => $subscription->stripe_subscription_id,
'stripe_status' => $stripeSubscription->status,
];
// Only fix if --fix flag is passed
if ($this->fix) {
$subscription->update([
'stripe_invoice_paid' => false,
'stripe_past_due' => false,
]);
if ($stripeSubscription->status === 'canceled') {
$subscription->team?->subscriptionEnded();
}
}
}
// Small delay to avoid Stripe rate limits
usleep(100000); // 100ms
} catch (\Exception $e) {
$errors[] = [
'subscription_id' => $subscription->id,
'error' => $e->getMessage(),
];
}
}
// Only notify if discrepancies found and fixed
if ($this->fix && count($discrepancies) > 0) {
send_internal_notification(
'SyncStripeSubscriptionsJob: Fixed '.count($discrepancies)." discrepancies:\n".
json_encode($discrepancies, JSON_PRETTY_PRINT)
);
}
return [
'total_checked' => $subscriptions->count(),
'discrepancies' => $discrepancies,
'errors' => $errors,
'fixed' => $this->fix,
];
}
}

View file

@ -110,26 +110,6 @@ public function restartSentinel()
}
}
public function updatedIsSentinelDebugEnabled($value)
{
try {
$this->submit();
$this->restartSentinel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function updatedIsMetricsEnabled($value)
{
try {
$this->submit();
$this->restartSentinel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function updatedIsSentinelEnabled($value)
{
try {
@ -175,6 +155,16 @@ public function submit()
}
}
public function instantSave()
{
try {
$this->syncData(true);
$this->restartSentinel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.sentinel');

View file

@ -2,7 +2,7 @@
return [
'coolify' => [
'version' => '4.0.0-beta.458',
'version' => '4.0.0-beta.459',
'helper_version' => '1.0.12',
'realtime_version' => '1.0.10',
'self_hosted' => env('SELF_HOSTED', true),

View file

@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.458"
"version": "4.0.0-beta.459"
},
"nightly": {
"version": "4.0.0-beta.459"
"version": "4.0.0-beta.460"
},
"helper": {
"version": "1.0.12"

View file

@ -22,6 +22,9 @@ services:
- DOLI_CRON=${DOLI_CRON:-0}
- DOLI_INIT_DEMO=${DOLI_INIT_DEMO:-0}
- DOLI_COMPANY_NAME=${DOLI_COMPANY_NAME:-MyBigCompany}
volumes:
- dolibarr_docs:/var/www/documents
- dolibarr_custom:/var/www/html/custom
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:80"]
interval: 2s

View file

@ -280,7 +280,7 @@ services:
config:
hide_credentials: true
supabase-studio:
image: supabase/studio:2025.06.02-sha-8f2993d
image: supabase/studio:2025.12.17-sha-43f4f7f
healthcheck:
test:
[

View file

@ -7,7 +7,7 @@
services:
superset:
image: amancevice/superset:latest
image: amancevice/superset:6.0.0
environment:
- SERVICE_URL_SUPERSET_8088
- SECRET_KEY=${SERVICE_BASE64_64_SUPERSETSECRETKEY}
@ -63,13 +63,13 @@ services:
retries: 10
postgres:
image: postgres:17-alpine
image: postgres:18
environment:
- POSTGRES_USER=${SERVICE_USER_POSTGRES}
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
- POSTGRES_DB=${POSTGRES_DB:-superset-db}
volumes:
- superset_postgres_data:/var/lib/postgresql/data
- superset_postgres_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
@ -77,7 +77,7 @@ services:
retries: 10
redis:
image: redis:8-alpine
image: redis:8
volumes:
- superset_redis_data:/data
command: redis-server --requirepass ${SERVICE_PASSWORD_REDIS}

View file

@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.458"
"version": "4.0.0-beta.459"
},
"nightly": {
"version": "4.0.0-beta.459"
"version": "4.0.0-beta.460"
},
"helper": {
"version": "1.0.12"