diff --git a/app/Http/Livewire/Project/Shared/Webhooks.php b/app/Http/Livewire/Project/Shared/Webhooks.php
index a943347b1..d425b51df 100644
--- a/app/Http/Livewire/Project/Shared/Webhooks.php
+++ b/app/Http/Livewire/Project/Shared/Webhooks.php
@@ -8,9 +8,27 @@ class Webhooks extends Component
{
public $resource;
public ?string $deploywebhook = null;
+ public ?string $githubManualWebhook = null;
+ public ?string $gitlabManualWebhook = null;
+ protected $rules = [
+ 'resource.manual_webhook_secret_github' => 'nullable|string',
+ 'resource.manual_webhook_secret_gitlab' => 'nullable|string',
+ ];
+ public function saveSecret()
+ {
+ try {
+ $this->validate();
+ $this->resource->save();
+ $this->emit('success','Secret Saved.');
+ } catch (\Exception $e) {
+ return handleError($e, $this);
+ }
+ }
public function mount()
{
$this->deploywebhook = generateDeployWebhook($this->resource);
+ $this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
+ $this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
}
public function render()
{
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index ca01b8d50..86566c534 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -53,6 +53,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private StandaloneDocker|SwarmDocker $destination;
private Server $server;
private ?ApplicationPreview $preview = null;
+ private ?string $git_type = null;
private string $container_name;
private ?string $currently_running_container_name = null;
@@ -99,6 +100,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->force_rebuild = $this->application_deployment_queue->force_rebuild;
$this->restart_only = $this->application_deployment_queue->restart_only;
+ $this->git_type = data_get($this->application_deployment_queue, 'git_type');
+
$source = data_get($this->application, 'source');
if ($source) {
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
@@ -656,7 +659,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
if ($this->pull_request_id !== 0) {
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
- $commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name"));
+ $commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin $this->branch && git checkout $pr_branch_name"));
}
return $commands->implode(' && ');
}
@@ -668,14 +671,28 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
throw new Exception('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
- $git_clone_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}";
- $git_clone_command = $this->set_git_import_settings($git_clone_command);
+ $git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}";
+ $git_clone_command = $this->set_git_import_settings($git_clone_command_base);
$commands = collect([
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"),
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa"),
- executeInDocker($this->deployment_uuid, $git_clone_command)
]);
+ if ($this->pull_request_id !== 0) {
+ ray($this->git_type);
+ if ($this->git_type === 'gitlab') {
+ $this->branch = "merge-requests/{$this->pull_request_id}/head:$pr_branch_name";
+ $commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
+ $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name";
+ }
+ if ($this->git_type === 'github') {
+ $this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
+ $commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
+ $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name";
+ }
+ }
+
+ $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
return $commands->implode(' && ');
}
if ($this->application->deploymentType() === 'other') {
diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php
index d78d19992..fce225dc5 100644
--- a/bootstrap/helpers/applications.php
+++ b/bootstrap/helpers/applications.php
@@ -4,7 +4,7 @@ use App\Jobs\ApplicationDeploymentJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
-function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false)
+function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null)
{
$deployment = ApplicationDeploymentQueue::create([
'application_id' => $application_id,
@@ -14,6 +14,7 @@ function queue_application_deployment(int $application_id, string $deployment_uu
'is_webhook' => $is_webhook,
'restart_only' => $restart_only,
'commit' => $commit,
+ 'git_type' => $git_type
]);
$queued_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'queued')->get()->sortByDesc('created_at');
$running_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'in_progress')->get()->sortByDesc('created_at');
diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php
index 16ac3671a..60ff6d9c9 100644
--- a/bootstrap/helpers/github.php
+++ b/bootstrap/helpers/github.php
@@ -50,8 +50,11 @@ function generate_github_jwt_token(GithubApp $source)
return $issuedToken;
}
-function githubApi(GithubApp|GitlabApp $source, string $endpoint, string $method = 'get', array|null $data = null, bool $throwError = true)
+function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', array|null $data = null, bool $throwError = true)
{
+ if (is_null($source)) {
+ throw new \Exception('Not implemented yet.');
+ }
if ($source->getMorphClass() == 'App\Models\GithubApp') {
if ($source->is_public) {
$response = Http::github($source->api_url)->$method($endpoint);
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index d6766a8d0..1abb90ce5 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -510,6 +510,14 @@ function generateDeployWebhook($resource)
$url = $api . $endpoint . "?uuid=$uuid&force=false";
return $url;
}
+function generateGitManualWebhook($resource, $type) {
+ if ($resource->getMorphClass() === 'App\Models\Application') {
+ $baseUrl = base_url();
+ $api = Url::fromString($baseUrl) . "/webhooks/source/$type/events/manual";
+ return $api;
+ }
+ return null;
+}
function removeAnsiColors($text)
{
return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text);
diff --git a/database/migrations/2023_11_14_103450_add_manual_webhook_secret.php b/database/migrations/2023_11_14_103450_add_manual_webhook_secret.php
new file mode 100644
index 000000000..3d1f37d38
--- /dev/null
+++ b/database/migrations/2023_11_14_103450_add_manual_webhook_secret.php
@@ -0,0 +1,30 @@
+string('manual_webhook_secret_github')->nullable();
+ $table->string('manual_webhook_secret_gitlab')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('applications', function (Blueprint $table) {
+ $table->dropColumn('manual_webhook_secret_github');
+ $table->dropColumn('manual_webhook_secret_gitlab');
+ });
+ }
+};
diff --git a/database/migrations/2023_11_14_121416_add_git_type.php b/database/migrations/2023_11_14_121416_add_git_type.php
new file mode 100644
index 000000000..1e2a307fe
--- /dev/null
+++ b/database/migrations/2023_11_14_121416_add_git_type.php
@@ -0,0 +1,34 @@
+string('git_type')->nullable();
+ });
+ Schema::table('application_deployment_queues', function (Blueprint $table) {
+ $table->string('git_type')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('application_previews', function (Blueprint $table) {
+ $table->dropColumn('git_type');
+ });
+ Schema::table('application_deployment_queues', function (Blueprint $table) {
+ $table->dropColumn('git_type');
+ });
+ }
+};
diff --git a/resources/views/livewire/project/shared/webhooks.blade.php b/resources/views/livewire/project/shared/webhooks.blade.php
index ba0461bdb..b47aba097 100644
--- a/resources/views/livewire/project/shared/webhooks.blade.php
+++ b/resources/views/livewire/project/shared/webhooks.blade.php
@@ -1,10 +1,31 @@
-
+
diff --git a/routes/webhooks.php b/routes/webhooks.php
index bb2c55a3d..8ca16dd8c 100644
--- a/routes/webhooks.php
+++ b/routes/webhooks.php
@@ -63,6 +63,245 @@ Route::get('/source/github/install', function () {
return handleError($e);
}
});
+Route::post('/source/gitlab/events/manual', function () {
+ try {
+ $payload = request()->collect();
+ $headers = request()->headers->all();
+ ray($payload, $headers);
+ $x_gitlab_token = data_get($headers, 'x-gitlab-token.0');
+ $x_gitlab_event = data_get($payload, 'object_kind');
+ if ($x_gitlab_event === 'push') {
+ $branch = data_get($payload, 'ref');
+ $full_name = data_get($payload, 'project.path_with_namespace');
+ if (Str::isMatch('/refs\/heads\/*/', $branch)) {
+ $branch = Str::after($branch, 'refs/heads/');
+ }
+ if (!$branch) {
+ return response('Nothing to do. No branch found in the request.');
+ }
+ ray('Manual Webhook GitLab Push Event with branch: ' . $branch);
+ }
+ if ($x_gitlab_event === 'merge_request') {
+ $action = data_get($payload, 'object_attributes.action');
+ ray($action);
+ $branch = data_get($payload, 'object_attributes.source_branch');
+ $base_branch = data_get($payload, 'object_attributes.target_branch');
+ $full_name = data_get($payload, 'project.path_with_namespace');
+ $pull_request_id = data_get($payload, 'object_attributes.iid');
+ $pull_request_html_url = data_get($payload, 'object_attributes.url');
+ if (!$branch) {
+ return response('Nothing to do. No branch found in the request.');
+ }
+ ray('Webhook GitHub Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id);
+ }
+ $applications = Application::whereNotNull('private_key_id')->where('git_repository', 'like', "%$full_name%");
+ if ($x_gitlab_event === 'push') {
+ $applications = $applications->where('git_branch', $branch)->get();
+ if ($applications->isEmpty()) {
+ return response("Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.");
+ }
+ }
+ if ($x_gitlab_event === 'merge_request') {
+ $applications = $applications->where('git_branch', $base_branch)->get();
+ if ($applications->isEmpty()) {
+ return response("Nothing to do. No applications found with branch '$base_branch'.");
+ }
+ }
+ foreach ($applications as $application) {
+ $webhook_secret = data_get($application, 'manual_webhook_secret_gitlab');
+ if ($webhook_secret !== $x_gitlab_token) {
+ ray('Invalid signature');
+ continue;
+ }
+ $isFunctional = $application->destination->server->isFunctional();
+ if (!$isFunctional) {
+ ray('Server is not functional: ' . $application->destination->server->name);
+ continue;
+ }
+ if ($x_gitlab_event === 'push') {
+ if ($application->isDeployable()) {
+ ray('Deploying ' . $application->name . ' with branch ' . $branch);
+ $deployment_uuid = new Cuid2(7);
+ queue_application_deployment(
+ application_id: $application->id,
+ deployment_uuid: $deployment_uuid,
+ force_rebuild: false,
+ is_webhook: true
+ );
+ } else {
+ ray('Deployments disabled for ' . $application->name);
+ }
+ }
+ if ($x_gitlab_event === 'merge_request') {
+ if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') {
+ if ($application->isPRDeployable()) {
+ $deployment_uuid = new Cuid2(7);
+ $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
+ if (!$found) {
+ ApplicationPreview::create([
+ 'git_type' => 'gitlab',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ ]);
+ }
+ queue_application_deployment(
+ application_id: $application->id,
+ pull_request_id: $pull_request_id,
+ deployment_uuid: $deployment_uuid,
+ force_rebuild: false,
+ is_webhook: true,
+ git_type: 'gitlab'
+ );
+ ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id);
+ return response('Preview Deployment queued.');
+ } else {
+ ray('Preview deployments disabled for ' . $application->name);
+ return response('Nothing to do. Preview Deployments disabled.');
+ }
+ }
+ if ($action === 'closed') {
+ $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
+ if ($found) {
+ $found->delete();
+ $container_name = generateApplicationContainerName($application, $pull_request_id);
+ // ray('Stopping container: ' . $container_name);
+ instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
+ return response('Preview Deployment closed.');
+ }
+ return response('Nothing to do. No Preview Deployment found');
+ }
+ }
+ }
+ } catch (Exception $e) {
+ ray($e->getMessage());
+ return handleError($e);
+ }
+});
+Route::post('/source/github/events/manual', function () {
+ try {
+ $x_github_event = Str::lower(request()->header('X-GitHub-Event'));
+ $x_hub_signature_256 = Str::after(request()->header('X-Hub-Signature-256'), 'sha256=');
+ $content_type = request()->header('Content-Type');
+ $payload = request()->collect();
+ if ($x_github_event === 'ping') {
+ // Just pong
+ return response('pong');
+ }
+
+ if ($content_type !== 'application/json') {
+ $payload = json_decode(data_get($payload, 'payload'), true);
+ }
+ ray($payload);
+ if ($x_github_event === 'push') {
+ $branch = data_get($payload, 'ref');
+ $full_name = data_get($payload, 'repository.full_name');
+ if (Str::isMatch('/refs\/heads\/*/', $branch)) {
+ $branch = Str::after($branch, 'refs/heads/');
+ }
+ ray('Manual Webhook GitHub Push Event with branch: ' . $branch);
+ }
+ if ($x_github_event === 'pull_request') {
+ $action = data_get($payload, 'action');
+ $full_name = data_get($payload, 'repository.full_name');
+ $pull_request_id = data_get($payload, 'number');
+ $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');
+ ray('Webhook GitHub Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id);
+ }
+ if (!$branch) {
+ return response('Nothing to do. No branch found in the request.');
+ }
+ $applications = Application::whereNotNull('private_key_id')->where('git_repository', 'like', "%$full_name%");
+
+ if ($x_github_event === 'push') {
+ $applications = $applications->where('git_branch', $branch)->get();
+ if ($applications->isEmpty()) {
+ return response("Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.");
+ }
+ }
+ if ($x_github_event === 'pull_request') {
+ $applications = $applications->where('git_branch', $base_branch)->get();
+ if ($applications->isEmpty()) {
+ return response("Nothing to do. No applications found with branch '$base_branch'.");
+ }
+ }
+ foreach ($applications as $application) {
+ ray($application);
+ $webhook_secret = data_get($application, 'manual_webhook_secret_github');
+ ray($webhook_secret);
+ $hmac = hash_hmac('sha256', request()->getContent(), $webhook_secret);
+ ray($hmac, $x_hub_signature_256);
+ if (!hash_equals($x_hub_signature_256, $hmac)) {
+ ray('Invalid signature');
+ continue;
+ }
+ $isFunctional = $application->destination->server->isFunctional();
+ if (!$isFunctional) {
+ ray('Server is not functional: ' . $application->destination->server->name);
+ continue;
+ }
+ if ($x_github_event === 'push') {
+ if ($application->isDeployable()) {
+ ray('Deploying ' . $application->name . ' with branch ' . $branch);
+ $deployment_uuid = new Cuid2(7);
+ queue_application_deployment(
+ application_id: $application->id,
+ deployment_uuid: $deployment_uuid,
+ force_rebuild: false,
+ is_webhook: true
+ );
+ } else {
+ ray('Deployments disabled for ' . $application->name);
+ }
+ }
+ if ($x_github_event === 'pull_request') {
+ if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
+ if ($application->isPRDeployable()) {
+ $deployment_uuid = new Cuid2(7);
+ $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
+ if (!$found) {
+ ApplicationPreview::create([
+ 'git_type' => 'github',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ ]);
+ }
+ queue_application_deployment(
+ application_id: $application->id,
+ pull_request_id: $pull_request_id,
+ deployment_uuid: $deployment_uuid,
+ force_rebuild: false,
+ is_webhook: true,
+ git_type: 'github'
+ );
+ ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id);
+ return response('Preview Deployment queued.');
+ } else {
+ ray('Preview deployments disabled for ' . $application->name);
+ return response('Nothing to do. Preview Deployments disabled.');
+ }
+ }
+ if ($action === 'closed') {
+ $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
+ if ($found) {
+ $found->delete();
+ $container_name = generateApplicationContainerName($application, $pull_request_id);
+ // ray('Stopping container: ' . $container_name);
+ instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
+ return response('Preview Deployment closed.');
+ }
+ return response('Nothing to do. No Preview Deployment found');
+ }
+ }
+ }
+ } catch (Exception $e) {
+ ray($e->getMessage());
+ return handleError($e);
+ }
+});
Route::post('/source/github/events', function () {
try {
$id = null;
@@ -150,6 +389,7 @@ Route::post('/source/github/events', function () {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (!$found) {
ApplicationPreview::create([
+ 'git_type' => 'github',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,
@@ -160,7 +400,8 @@ Route::post('/source/github/events', function () {
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
- is_webhook: true
+ is_webhook: true,
+ git_type: 'github'
);
ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id);
return response('Preview Deployment queued.');
@@ -169,7 +410,7 @@ Route::post('/source/github/events', function () {
return response('Nothing to do. Preview Deployments disabled.');
}
}
- if ($action === 'closed') {
+ if ($action === 'closed' || $action === 'close') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
$found->delete();