Merge branch 'next' into feat/api/tag-management

This commit is contained in:
Michael Jathe authored and GitHub committed 2026-03-30 10:04:00 +02:00
commit 2b573ef5ec
62 files changed
+1577 -244

No files matched your search

+97 -12
View File
@@ -118,7 +118,92 @@ class Application extends BaseModel
private static $parserVersion = '5';
protected $guarded = [];
protected $fillable = [
'name',
'description',
'fqdn',
'git_repository',
'git_branch',
'git_commit_sha',
'git_full_url',
'docker_registry_image_name',
'docker_registry_image_tag',
'build_pack',
'static_image',
'install_command',
'build_command',
'start_command',
'ports_exposes',
'ports_mappings',
'base_directory',
'publish_directory',
'health_check_enabled',
'health_check_path',
'health_check_port',
'health_check_host',
'health_check_method',
'health_check_return_code',
'health_check_scheme',
'health_check_response_text',
'health_check_interval',
'health_check_timeout',
'health_check_retries',
'health_check_start_period',
'health_check_type',
'health_check_command',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'status',
'preview_url_template',
'dockerfile',
'dockerfile_location',
'dockerfile_target_build',
'custom_labels',
'custom_docker_run_options',
'post_deployment_command',
'post_deployment_command_container',
'pre_deployment_command',
'pre_deployment_command_container',
'manual_webhook_secret_github',
'manual_webhook_secret_gitlab',
'manual_webhook_secret_bitbucket',
'manual_webhook_secret_gitea',
'docker_compose_location',
'docker_compose_pr_location',
'docker_compose',
'docker_compose_pr',
'docker_compose_raw',
'docker_compose_pr_raw',
'docker_compose_domains',
'docker_compose_custom_start_command',
'docker_compose_custom_build_command',
'swarm_replicas',
'swarm_placement_constraints',
'watch_paths',
'redirect',
'compose_parsing_version',
'custom_nginx_configuration',
'custom_network_aliases',
'custom_healthcheck_found',
'nixpkgsarchive',
'is_http_basic_auth_enabled',
'http_basic_auth_username',
'http_basic_auth_password',
'connect_to_docker_network',
'force_domain_override',
'is_container_label_escape_enabled',
'use_build_server',
'config_hash',
'last_online_at',
'restart_count',
'last_restart_at',
'last_restart_type',
];
protected $appends = ['server_status'];
@@ -1145,7 +1230,7 @@ class Application extends BaseModel
'is_accessible' => true,
'error' => null,
];
} catch (\RuntimeException $ex) {
} catch (RuntimeException $ex) {
return [
'is_accessible' => false,
'error' => $ex->getMessage(),
@@ -1202,7 +1287,7 @@ class Application extends BaseModel
];
}
if ($this->source->getMorphClass() === \App\Models\GitlabApp::class) {
if ($this->source->getMorphClass() === GitlabApp::class) {
$gitlabSource = $this->source;
$private_key = data_get($gitlabSource, 'privateKey.private_key');
@@ -1354,7 +1439,7 @@ class Application extends BaseModel
$source_html_url_host = $url['host'];
$source_html_url_scheme = $url['scheme'];
if ($this->source->getMorphClass() === \App\Models\GithubApp::class) {
if ($this->source->getMorphClass() === GithubApp::class) {
if ($this->source->is_public) {
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$escapedRepoUrl = escapeshellarg("{$this->source->html_url}/{$customRepository}");
@@ -1409,7 +1494,7 @@ class Application extends BaseModel
];
}
if ($this->source->getMorphClass() === \App\Models\GitlabApp::class) {
if ($this->source->getMorphClass() === GitlabApp::class) {
$gitlabSource = $this->source;
$private_key = data_get($gitlabSource, 'privateKey.private_key');
@@ -1600,7 +1685,7 @@ class Application extends BaseModel
try {
$yaml = Yaml::parse($this->docker_compose_raw);
} catch (\Exception $e) {
throw new \RuntimeException($e->getMessage());
throw new RuntimeException($e->getMessage());
}
$services = data_get($yaml, 'services');
@@ -1682,7 +1767,7 @@ class Application extends BaseModel
$fileList = collect([".$workdir$composeFile"]);
$gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid);
if (! $gitRemoteStatus['is_accessible']) {
throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
throw new RuntimeException('Failed to read Git source. Please verify repository access and try again.');
}
$getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false);
$gitVersion = str($getGitVersion)->explode(' ')->last();
@@ -1732,15 +1817,15 @@ class Application extends BaseModel
$this->save();
if (str($e->getMessage())->contains('No such file')) {
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
throw new RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
if (str($e->getMessage())->contains('fatal: repository') && str($e->getMessage())->contains('does not exist')) {
if ($this->deploymentType() === 'deploy_key') {
throw new \RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.');
throw new RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.');
}
throw new \RuntimeException('Repository does not exist. Please check your repository URL and try again.');
throw new RuntimeException('Repository does not exist. Please check your repository URL and try again.');
}
throw new \RuntimeException($e->getMessage());
throw new RuntimeException('Failed to read the Docker Compose file from the repository.');
} finally {
// Cleanup only - restoration happens in catch block
$commands = collect([
@@ -1793,7 +1878,7 @@ class Application extends BaseModel
$this->base_directory = $initialBaseDirectory;
$this->save();
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
throw new RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
}
-2
View File
@@ -265,8 +265,6 @@ class Server extends BaseModel
'server_metadata',
];
protected $guarded = [];
use HasSafeStringAttribute;
public function setValidationLogsAttribute($value): void
+1
View File
@@ -56,6 +56,7 @@ class ServerSetting extends Model
protected $guarded = [];
protected $casts = [
'force_disabled' => 'boolean',
'force_docker_cleanup' => 'boolean',
'docker_cleanup_threshold' => 'integer',
'sentinel_token' => 'encrypted',
+12 -2
View File
@@ -15,6 +15,7 @@ use Illuminate\Support\Facades\Storage;
use OpenApi\Attributes as OA;
use Spatie\Activitylog\Models\Activity;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
@@ -47,7 +48,16 @@ class Service extends BaseModel
private static $parserVersion = '5';
protected $guarded = [];
protected $fillable = [
'name',
'description',
'docker_compose_raw',
'docker_compose',
'connect_to_docker_network',
'service_type',
'config_hash',
'compose_parsing_version',
];
protected $appends = ['server_status', 'status'];
@@ -1552,7 +1562,7 @@ class Service extends BaseModel
// Generate SERVICE_NAME_* environment variables from docker-compose services
if ($this->docker_compose) {
try {
$dockerCompose = \Symfony\Component\Yaml\Yaml::parse($this->docker_compose);
$dockerCompose = Yaml::parse($this->docker_compose);
$services = data_get($dockerCompose, 'services', []);
foreach ($services as $serviceName => $_) {
$envs->push('SERVICE_NAME_'.str($serviceName)->replace('-', '_')->replace('.', '_')->upper().'='.$serviceName);
+26 -2
View File
@@ -13,12 +13,36 @@ class StandaloneClickhouse extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'clickhouse_admin_user',
'clickhouse_admin_password',
'is_log_drain_enabled',
'is_include_timestamps',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
'clickhouse_password' => 'encrypted',
'clickhouse_admin_password' => 'encrypted',
'public_port_timeout' => 'integer',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
+12 -1
View File
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Jobs\ConnectProxyToNetworksJob;
use App\Support\ValidationPatterns;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -18,13 +19,23 @@ class StandaloneDocker extends BaseModel
parent::boot();
static::created(function ($newStandaloneDocker) {
$server = $newStandaloneDocker->server;
$safeNetwork = escapeshellarg($newStandaloneDocker->network);
instant_remote_process([
"docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null",
"docker network inspect {$safeNetwork} >/dev/null 2>&1 || docker network create --driver overlay --attachable {$safeNetwork} >/dev/null",
], $server, false);
ConnectProxyToNetworksJob::dispatchSync($server);
});
}
public function setNetworkAttribute(string $value): void
{
if (! ValidationPatterns::isValidDockerNetwork($value)) {
throw new \InvalidArgumentException('Invalid Docker network name. Must start with alphanumeric and contain only alphanumeric characters, dots, hyphens, and underscores.');
}
$this->attributes['network'] = $value;
}
public function applications()
{
return $this->morphMany(Application::class, 'destination');
+24 -1
View File
@@ -13,7 +13,30 @@ class StandaloneDragonfly extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'dragonfly_password',
'is_log_drain_enabled',
'is_include_timestamps',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+25 -1
View File
@@ -13,7 +13,31 @@ class StandaloneKeydb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'keydb_password',
'keydb_conf',
'is_log_drain_enabled',
'is_include_timestamps',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
];
protected $appends = ['internal_db_url', 'external_db_url', 'server_status'];
+26 -1
View File
@@ -14,7 +14,32 @@ class StandaloneMariadb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'mariadb_root_password',
'mariadb_user',
'mariadb_password',
'mariadb_database',
'mariadb_conf',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+25 -1
View File
@@ -13,7 +13,31 @@ class StandaloneMongodb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'mongo_conf',
'mongo_initdb_root_username',
'mongo_initdb_root_password',
'mongo_initdb_database',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+26 -1
View File
@@ -13,7 +13,32 @@ class StandaloneMysql extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'mysql_root_password',
'mysql_user',
'mysql_password',
'mysql_database',
'mysql_conf',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+28 -1
View File
@@ -13,7 +13,34 @@ class StandalonePostgresql extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'postgres_user',
'postgres_password',
'postgres_db',
'postgres_initdb_args',
'postgres_host_auth_method',
'postgres_conf',
'init_scripts',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+22 -1
View File
@@ -13,7 +13,28 @@ class StandaloneRedis extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'redis_conf',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+11
View File
@@ -2,10 +2,21 @@
namespace App\Models;
use App\Support\ValidationPatterns;
class SwarmDocker extends BaseModel
{
protected $guarded = [];
public function setNetworkAttribute(string $value): void
{
if (! ValidationPatterns::isValidDockerNetwork($value)) {
throw new \InvalidArgumentException('Invalid Docker network name. Must start with alphanumeric and contain only alphanumeric characters, dots, hyphens, and underscores.');
}
$this->attributes['network'] = $value;
}
public function applications()
{
return $this->morphMany(Application::class, 'destination');
+7 -1
View File
@@ -40,7 +40,13 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
{
use HasFactory, HasNotificationSettings, HasSafeStringAttribute, Notifiable;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'personal_team',
'show_boarding',
'custom_server_limit',
];
protected $casts = [
'personal_team' => 'boolean',
+16 -8
View File
@@ -4,7 +4,9 @@ namespace App\Models;
use App\Jobs\UpdateStripeCustomerEmailJob;
use App\Notifications\Channels\SendsEmail;
use App\Notifications\TransactionalEmails\EmailChangeVerification;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
use App\Services\ChangelogService;
use App\Traits\DeletesUserSessions;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -41,7 +43,13 @@ class User extends Authenticatable implements SendsEmail
{
use DeletesUserSessions, HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
protected $guarded = [];
protected $fillable = [
'name',
'email',
'password',
'force_password_reset',
'marketing_emails',
];
protected $hidden = [
'password',
@@ -87,7 +95,7 @@ class User extends Authenticatable implements SendsEmail
$team['id'] = 0;
$team['name'] = 'Root Team';
}
$new_team = Team::create($team);
$new_team = Team::forceCreate($team);
$user->teams()->attach($new_team, ['role' => 'owner']);
});
@@ -190,7 +198,7 @@ class User extends Authenticatable implements SendsEmail
$team['id'] = 0;
$team['name'] = 'Root Team';
}
$new_team = Team::create($team);
$new_team = Team::forceCreate($team);
$this->teams()->attach($new_team, ['role' => 'owner']);
return $new_team;
@@ -228,7 +236,7 @@ class User extends Authenticatable implements SendsEmail
public function getUnreadChangelogCount(): int
{
return app(\App\Services\ChangelogService::class)->getUnreadCountForUser($this);
return app(ChangelogService::class)->getUnreadCountForUser($this);
}
public function getRecipients(): array
@@ -239,7 +247,7 @@ class User extends Authenticatable implements SendsEmail
public function sendVerificationEmail()
{
$mail = new MailMessage;
$url = Url::temporarySignedRoute(
$url = URL::temporarySignedRoute(
'verify.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
@@ -401,14 +409,14 @@ class User extends Authenticatable implements SendsEmail
$expiryMinutes = config('constants.email_change.verification_code_expiry_minutes', 10);
$expiresAt = Carbon::now()->addMinutes($expiryMinutes);
$this->update([
$this->forceFill([
'pending_email' => $newEmail,
'email_change_code' => $code,
'email_change_code_expires_at' => $expiresAt,
]);
])->save();
// Send verification email to new address
$this->notify(new \App\Notifications\TransactionalEmails\EmailChangeVerification($this, $code, $newEmail, $expiresAt));
$this->notify(new EmailChangeVerification($this, $code, $newEmail, $expiresAt));
}
public function isEmailChangeCodeValid(string $code): bool