Merge branch 'next' into shadow/add-shared-server-env

This commit is contained in:
ShadowArcanist
2026-01-14 05:26:06 +01:00
committed by GitHub
227 changed files with 9960 additions and 15827 deletions
+3 -1
View File
@@ -131,10 +131,10 @@ function sharedDataApplications()
'manual_webhook_secret_gitea' => 'string|nullable',
'docker_compose_location' => 'string',
'docker_compose' => 'string|nullable',
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
'is_container_label_escape_enabled' => 'boolean',
];
}
@@ -179,4 +179,6 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('is_static');
$request->offsetUnset('force_domain_override');
$request->offsetUnset('autogenerate_domain');
$request->offsetUnset('is_container_label_escape_enabled');
$request->offsetUnset('docker_compose_raw');
}
+1 -1
View File
@@ -72,7 +72,7 @@ const SUPPORTED_OS = [
const NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK = [
'pgadmin',
'postgresus',
'databasus',
'redis-insight',
];
const NEEDS_TO_DISABLE_GZIP = [
+2 -2
View File
@@ -129,8 +129,8 @@ function format_docker_envs_to_json($rawOutput)
}
function checkMinimumDockerEngineVersion($dockerVersion)
{
$majorDockerVersion = str($dockerVersion)->before('.')->value();
$requiredDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.')->value();
$majorDockerVersion = (int) str($dockerVersion)->before('.')->value();
$requiredDockerVersion = (int) str(config('constants.docker.minimum_required_version'))->before('.')->value();
if ($majorDockerVersion < $requiredDockerVersion) {
$dockerVersion = null;
}
+51
View File
@@ -162,3 +162,54 @@ function loadRepositoryByPage(GithubApp $source, string $token, int $page)
'repositories' => $json['repositories'],
];
}
function getGithubCommitRangeFiles(?GithubApp $source, string $owner, string $repo, string $beforeSha, string $afterSha): array
{
try {
if (! $source) {
// Manual webhooks don't have GitHub App authentication
// Return empty array so watch paths are ignored (current behavior)
return [];
}
$endpoint = "/repos/{$owner}/{$repo}/compare/{$beforeSha}...{$afterSha}";
$response = githubApi($source, $endpoint, 'get', null, false);
if (! $response) {
return [];
}
$files = collect(data_get($response, 'data.files', []));
return $files->pluck('filename')->filter()->values()->toArray();
} catch (Exception $e) {
ray('Error fetching GitHub commit range files: '.$e->getMessage());
return [];
}
}
function getGithubPullRequestFiles(?GithubApp $source, string $owner, string $repo, int $pullRequestId): array
{
try {
if (! $source) {
// Manual webhooks don't have GitHub App authentication
// Return empty array so watch paths are ignored (current behavior)
return [];
}
$endpoint = "/repos/{$owner}/{$repo}/pulls/{$pullRequestId}/files";
$response = githubApi($source, $endpoint, 'get', null, false);
if (! $response) {
return [];
}
$files = collect(data_get($response, 'data', []));
return $files->pluck('filename')->filter()->values()->toArray();
} catch (Exception $e) {
ray('Error fetching GitHub PR files: '.$e->getMessage());
return [];
}
}
+2 -2
View File
@@ -175,7 +175,7 @@ function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
throw new \RuntimeException($errorMessage, $exitCode);
}
function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection
function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null, bool $includeAll = false): Collection
{
if (is_null($application_deployment_queue)) {
return collect([]);
@@ -216,7 +216,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
$seenCommands = collect();
$formatted = collect($decoded);
if (! $is_debug_enabled) {
if (! $is_debug_enabled && ! $includeAll) {
$formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);
}
+126 -5
View File
@@ -182,8 +182,11 @@ function refreshSession(?Team $team = null): void
$team = User::find(Auth::id())->teams->first();
}
}
// Clear old cache key format for backwards compatibility
Cache::forget('team:'.Auth::id());
Cache::remember('team:'.Auth::id(), 3600, function () use ($team) {
// Use new cache key format that includes team ID
Cache::forget('user:'.Auth::id().':team:'.$team->id);
Cache::remember('user:'.Auth::id().':team:'.$team->id, 3600, function () use ($team) {
return $team;
});
session(['currentTeam' => $team]);
@@ -384,7 +387,7 @@ function base_url(bool $withPort = true): string
function isSubscribed()
{
return isSubscriptionActive() || auth()->user()->isInstanceAdmin();
return isSubscriptionActive();
}
function isProduction(): bool
@@ -548,7 +551,21 @@ function getResourceByUuid(string $uuid, ?int $teamId = null)
return null;
}
$resource = queryResourcesByUuid($uuid);
if (! is_null($resource) && $resource->environment->project->team_id === $teamId) {
if (is_null($resource)) {
return null;
}
// ServiceDatabase has a different relationship path: service->environment->project->team_id
if ($resource instanceof \App\Models\ServiceDatabase) {
if ($resource->service?->environment?->project?->team_id === $teamId) {
return $resource;
}
return null;
}
// Standard resources: environment->project->team_id
if ($resource->environment->project->team_id === $teamId) {
return $resource;
}
@@ -635,6 +652,12 @@ function queryResourcesByUuid(string $uuid)
return $clickhouse;
}
// Check for ServiceDatabase by its own UUID
$serviceDatabase = ServiceDatabase::whereUuid($uuid)->first();
if ($serviceDatabase) {
return $serviceDatabase;
}
return $resource;
}
function generateTagDeployWebhook($tag_name)
@@ -966,6 +989,9 @@ function generateEnvValue(string $command, Service|Application|null $service = n
case 'USER':
$generatedValue = Str::random(16);
break;
case 'LOWERCASEUSER':
$generatedValue = Str::lower(Str::random(16));
break;
case 'SUPABASEANON':
$signingKey = $service->environment_variables()->where('key', 'SERVICE_PASSWORD_JWT')->first();
if (is_null($signingKey)) {
@@ -1164,7 +1190,7 @@ function get_public_ips()
$ipv4 = $first->output();
if ($ipv4) {
$ipv4 = trim($ipv4);
$validate_ipv4 = filter_var($ipv4, FILTER_VALIDATE_IP);
$validate_ipv4 = filter_var($ipv4, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
if ($validate_ipv4 == false) {
echo "Invalid ipv4: $ipv4\n";
@@ -1179,7 +1205,7 @@ function get_public_ips()
$ipv6 = $second->output();
if ($ipv6) {
$ipv6 = trim($ipv6);
$validate_ipv6 = filter_var($ipv6, FILTER_VALIDATE_IP);
$validate_ipv6 = filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
if ($validate_ipv6 == false) {
echo "Invalid ipv6: $ipv6\n";
@@ -2934,6 +2960,23 @@ function wireNavigate(): string
}
}
/**
* Redirect to a named route with SPA navigation support.
* Automatically uses wire:navigate when is_wire_navigate_enabled is true.
*/
function redirectRoute(Livewire\Component $component, string $name, array $parameters = []): mixed
{
$navigate = true;
try {
$navigate = instanceSettings()->is_wire_navigate_enabled ?? true;
} catch (\Exception $e) {
$navigate = true;
}
return $component->redirectRoute($name, $parameters, navigate: $navigate);
}
function getHelperVersion(): string
{
$settings = instanceSettings();
@@ -3399,3 +3442,81 @@ function verifyPasswordConfirmation(mixed $password, ?Livewire\Component $compon
return true;
}
/**
* Downsample metrics using the Largest-Triangle-Three-Buckets (LTTB) algorithm.
* This preserves the visual shape of the data better than simple averaging.
*
* @param array $data Array of [timestamp, value] pairs
* @param int $threshold Target number of points
* @return array Downsampled data
*/
function downsampleLTTB(array $data, int $threshold): array
{
$dataLength = count($data);
// Return unchanged if threshold >= data length, or if threshold <= 2
// (threshold <= 2 would cause division by zero in bucket calculation)
if ($threshold >= $dataLength || $threshold <= 2) {
return $data;
}
$sampled = [];
$sampled[] = $data[0]; // Always keep first point
$bucketSize = ($dataLength - 2) / ($threshold - 2);
$a = 0; // Index of previous selected point
for ($i = 0; $i < $threshold - 2; $i++) {
// Calculate bucket range
$bucketStart = (int) floor(($i + 1) * $bucketSize) + 1;
$bucketEnd = (int) floor(($i + 2) * $bucketSize) + 1;
$bucketEnd = min($bucketEnd, $dataLength - 1);
// Calculate average point for next bucket (used as reference)
$nextBucketStart = (int) floor(($i + 2) * $bucketSize) + 1;
$nextBucketEnd = (int) floor(($i + 3) * $bucketSize) + 1;
$nextBucketEnd = min($nextBucketEnd, $dataLength - 1);
$avgX = 0;
$avgY = 0;
$nextBucketCount = $nextBucketEnd - $nextBucketStart + 1;
if ($nextBucketCount > 0) {
for ($j = $nextBucketStart; $j <= $nextBucketEnd; $j++) {
$avgX += $data[$j][0];
$avgY += $data[$j][1];
}
$avgX /= $nextBucketCount;
$avgY /= $nextBucketCount;
}
// Find point in current bucket with largest triangle area
$maxArea = -1;
$maxAreaIndex = $bucketStart;
$pointAX = $data[$a][0];
$pointAY = $data[$a][1];
for ($j = $bucketStart; $j <= $bucketEnd; $j++) {
// Triangle area calculation
$area = abs(
($pointAX - $avgX) * ($data[$j][1] - $pointAY) -
($pointAX - $data[$j][0]) * ($avgY - $pointAY)
) * 0.5;
if ($area > $maxArea) {
$maxArea = $area;
$maxAreaIndex = $j;
}
}
$sampled[] = $data[$maxAreaIndex];
$a = $maxAreaIndex;
}
$sampled[] = $data[$dataLength - 1]; // Always keep last point
return $sampled;
}
+4
View File
@@ -13,6 +13,10 @@ function isSubscriptionActive()
if (! $team) {
return false;
}
// Root team (id=0) doesn't require subscription
if ($team->id === 0) {
return true;
}
$subscription = $team?->subscription;
if (is_null($subscription)) {