diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php
index 8872754e5..dd35a17dd 100644
--- a/app/Http/Controllers/Webhook/Github.php
+++ b/app/Http/Controllers/Webhook/Github.php
@@ -78,6 +78,7 @@ class Github extends Controller
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
$branch = data_get($payload, 'pull_request.head.ref');
$base_branch = data_get($payload, 'pull_request.base.ref');
+ $author_association = data_get($payload, 'pull_request.author_association');
}
if (! $branch) {
return response('Nothing to do. No branch found in the request.');
@@ -170,6 +171,19 @@ class Github extends Controller
if ($x_github_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) {
+ // Check if PR deployments from public contributors are restricted
+ if (! $application->settings->is_pr_deployments_public_enabled) {
+ $trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR'];
+ if (! in_array($author_association, $trustedAssociations)) {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'failed',
+ 'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association,
+ ]);
+
+ continue;
+ }
+ }
$deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
@@ -327,6 +341,7 @@ class Github extends Controller
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
$branch = data_get($payload, 'pull_request.head.ref');
$base_branch = data_get($payload, 'pull_request.base.ref');
+ $author_association = data_get($payload, 'pull_request.author_association');
}
if (! $id || ! $branch) {
return response('Nothing to do. No id or branch found.');
@@ -400,6 +415,19 @@ class Github extends Controller
if ($x_github_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) {
+ // Check if PR deployments from public contributors are restricted
+ if (! $application->settings->is_pr_deployments_public_enabled) {
+ $trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR'];
+ if (! in_array($author_association, $trustedAssociations)) {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'failed',
+ 'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association,
+ ]);
+
+ continue;
+ }
+ }
$deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php
index 862dc20d8..ed15ab258 100644
--- a/app/Livewire/Project/Application/Advanced.php
+++ b/app/Livewire/Project/Application/Advanced.php
@@ -28,6 +28,9 @@ class Advanced extends Component
#[Validate(['boolean'])]
public bool $isPreviewDeploymentsEnabled = false;
+ #[Validate(['boolean'])]
+ public bool $isPrDeploymentsPublicEnabled = false;
+
#[Validate(['boolean'])]
public bool $isAutoDeployEnabled = true;
@@ -91,6 +94,7 @@ class Advanced extends Component
$this->application->settings->is_git_lfs_enabled = $this->isGitLfsEnabled;
$this->application->settings->is_git_shallow_clone_enabled = $this->isGitShallowCloneEnabled;
$this->application->settings->is_preview_deployments_enabled = $this->isPreviewDeploymentsEnabled;
+ $this->application->settings->is_pr_deployments_public_enabled = $this->isPrDeploymentsPublicEnabled;
$this->application->settings->is_auto_deploy_enabled = $this->isAutoDeployEnabled;
$this->application->settings->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->application->settings->is_gpu_enabled = $this->isGpuEnabled;
@@ -117,6 +121,7 @@ class Advanced extends Component
$this->isGitLfsEnabled = $this->application->settings->is_git_lfs_enabled;
$this->isGitShallowCloneEnabled = $this->application->settings->is_git_shallow_clone_enabled ?? false;
$this->isPreviewDeploymentsEnabled = $this->application->settings->is_preview_deployments_enabled;
+ $this->isPrDeploymentsPublicEnabled = $this->application->settings->is_pr_deployments_public_enabled ?? false;
$this->isAutoDeployEnabled = $this->application->settings->is_auto_deploy_enabled;
$this->isGpuEnabled = $this->application->settings->is_gpu_enabled;
$this->gpuDriver = $this->application->settings->gpu_driver;
diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php
index d05081d21..4b03c69e1 100644
--- a/app/Models/ApplicationSetting.php
+++ b/app/Models/ApplicationSetting.php
@@ -13,6 +13,7 @@ class ApplicationSetting extends Model
'is_force_https_enabled' => 'boolean',
'is_debug_enabled' => 'boolean',
'is_preview_deployments_enabled' => 'boolean',
+ 'is_pr_deployments_public_enabled' => 'boolean',
'is_git_submodules_enabled' => 'boolean',
'is_git_lfs_enabled' => 'boolean',
'is_git_shallow_clone_enabled' => 'boolean',
diff --git a/database/migrations/2025_09_05_142446_add_pr_deployments_public_enabled_to_application_settings.php b/database/migrations/2025_09_05_142446_add_pr_deployments_public_enabled_to_application_settings.php
new file mode 100644
index 000000000..5d84ce42d
--- /dev/null
+++ b/database/migrations/2025_09_05_142446_add_pr_deployments_public_enabled_to_application_settings.php
@@ -0,0 +1,28 @@
+boolean('is_pr_deployments_public_enabled')->default(false)->after('is_preview_deployments_enabled');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('application_settings', function (Blueprint $table) {
+ $table->dropColumn('is_pr_deployments_public_enabled');
+ });
+ }
+};
diff --git a/resources/views/livewire/project/application/advanced.blade.php b/resources/views/livewire/project/application/advanced.blade.php
index 6dd5c872c..62d4380e9 100644
--- a/resources/views/livewire/project/application/advanced.blade.php
+++ b/resources/views/livewire/project/application/advanced.blade.php
@@ -13,6 +13,12 @@
helper="Allow to automatically deploy Preview Deployments for all opened PR's.
Closing a PR will delete Preview Deployments."
instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" canGate="update"
:canResource="$application" />
+ @if ($isPreviewDeploymentsEnabled)
+
+ @endif
@endif