瀏覽代碼

create incoming webhook handler

Mohammed Nafees 4 年之前
父節點
當前提交
3ac3f09871

+ 50 - 0
api/server/handlers/environment/create.go

@@ -1,6 +1,7 @@
 package environment
 
 import (
+	"fmt"
 	"net/http"
 	"strconv"
 
@@ -58,6 +59,9 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		Name:              request.Name,
 		GitRepoOwner:      owner,
 		GitRepoName:       name,
+		Mode:              request.Mode,
+		PRCount:           0,
+		LastPRStatus:      "",
 	})
 
 	if err != nil {
@@ -73,6 +77,52 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
+	hooks, _, err := client.Repositories.ListHooks(
+		r.Context(), owner, name, &github.ListOptions{},
+	)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	webhookURL := fmt.Sprintf("%s/api/github/incoming_webhook", c.Config().ServerConf.ServerURL)
+
+	for _, hook := range hooks {
+		if hook.GetURL() == webhookURL {
+			// if a previous webhook exists then we should delete it
+			// this ensures that an updated webhook secret is maintained
+			_, err = client.Repositories.DeleteHook(
+				r.Context(), owner, name, hook.GetID(),
+			)
+
+			if err != nil {
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				return
+			}
+
+			break
+		}
+	}
+
+	// create incoming webhook
+	_, _, err = client.Repositories.CreateHook(
+		r.Context(), owner, name, &github.Hook{
+			Config: map[string]interface{}{
+				"url":          webhookURL,
+				"content_type": "json",
+				"secret":       c.Config().ServerConf.GithubIncomingWebhookSecret,
+			},
+			Events: []string{"pull_requests"},
+			Active: github.Bool(false),
+		},
+	)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	// generate porter jwt token
 	jwt, err := token.GetTokenForAPI(user.ID, project.ID)
 

+ 2 - 0
api/server/handlers/environment/create_deployment.go

@@ -84,6 +84,8 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		RepoName:       request.GitHubMetadata.RepoName,
 		PRName:         request.GitHubMetadata.PRName,
 		CommitSHA:      request.GitHubMetadata.CommitSHA,
+		PRBranchFrom:   request.GitHubMetadata.PRBranchFrom,
+		PRBranchInto:   request.GitHubMetadata.PRBranchInto,
 	})
 
 	if err != nil {

+ 1 - 1
api/server/handlers/environment/update_deployment.go

@@ -86,7 +86,7 @@ func (c *UpdateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		return
 	}
 
-	// create the deployment
+	// update the deployment
 	depl, err = c.Repo().Environment().UpdateDeployment(depl)
 
 	if err != nil {

+ 133 - 0
api/server/handlers/webhook/github_incoming.go

@@ -0,0 +1,133 @@
+package webhook
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/bradleyfalzon/ghinstallation/v2"
+	"github.com/google/go-github/v41/github"
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type GithubIncomingWebhookHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewGithubIncomingWebhookHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GithubIncomingWebhookHandler {
+	return &GithubIncomingWebhookHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *GithubIncomingWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	payload, err := github.ValidatePayload(r, []byte(c.Config().ServerConf.GithubIncomingWebhookSecret))
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	event, err := github.ParseWebHook(github.WebHookType(r), payload)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	switch event := event.(type) {
+	case *github.PullRequestEvent:
+		err = c.processPullRequestEvent(event, r)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+}
+
+func (c *GithubIncomingWebhookHandler) processPullRequestEvent(event *github.PullRequestEvent, r *http.Request) error {
+	owner := event.GetOrganization().GetName()
+	repo := event.GetRepo().GetName()
+
+	env, err := c.Repo().Environment().ReadEnvironmentByOwnerRepoName(owner, repo)
+
+	if err != nil {
+		return err
+	}
+
+	// create deployment on GitHub API
+	client, err := getGithubClientFromEnvironment(c.Config(), env)
+
+	if err != nil {
+		return err
+	}
+
+	if env.Mode == "auto" && event.GetAction() == "opened" {
+		_, err := client.Actions.CreateWorkflowDispatchEventByFileName(
+			r.Context(), owner, repo, fmt.Sprintf("porter_%s_env.yml", env.Name),
+			github.CreateWorkflowDispatchEventRequest{
+				Ref: event.PullRequest.GetHead().GetRef(),
+			},
+		)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	depl, err := c.Repo().Environment().ReadDeploymentByGitDetails(
+		env.ID, owner, repo, uint(event.GetPullRequest().GetNumber()),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	if depl.Status != "disabled" {
+		_, err := client.Actions.CreateWorkflowDispatchEventByFileName(
+			r.Context(), owner, repo, fmt.Sprintf("porter_%s_env.yml", env.Name),
+			github.CreateWorkflowDispatchEventRequest{
+				Ref: event.PullRequest.GetHead().GetRef(),
+			},
+		)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func getGithubClientFromEnvironment(config *config.Config, env *models.Environment) (*github.Client, error) {
+	// get the github app client
+	ghAppId, err := strconv.Atoi(config.ServerConf.GithubAppID)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// authenticate as github app installation
+	itr, err := ghinstallation.NewKeyFromFile(
+		http.DefaultTransport,
+		int64(ghAppId),
+		int64(env.GitInstallationID),
+		config.ServerConf.GithubAppSecretPath,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return github.NewClient(&http.Client{Transport: itr}), nil
+}

+ 30 - 0
api/server/router/base.go

@@ -9,6 +9,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/metadata"
 	"github.com/porter-dev/porter/api/server/handlers/release"
 	"github.com/porter-dev/porter/api/server/handlers/user"
+	"github.com/porter-dev/porter/api/server/handlers/webhook"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
@@ -536,5 +537,34 @@ func GetBaseRoutes(
 		Router:   r,
 	})
 
+	if config.ServerConf.GithubIncomingWebhookSecret != "" {
+
+		// POST /api/github/incoming_webhook -> webhook.NewGithubIncomingWebhook
+		githubIncomingWebhookEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbCreate,
+				Method: types.HTTPVerbPost,
+				Path: &types.Path{
+					Parent:       basePath,
+					RelativePath: "/github/incoming_webhook",
+				},
+				Scopes: []types.PermissionScope{},
+			},
+		)
+
+		githubIncomingWebhookHandler := webhook.NewGithubIncomingWebhookHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &Route{
+			Endpoint: githubIncomingWebhookEndpoint,
+			Handler:  githubIncomingWebhookHandler,
+			Router:   r,
+		})
+
+	}
+
 	return routes
 }

+ 86 - 82
api/server/router/cluster.go

@@ -288,91 +288,95 @@ func getClusterRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/environments -> environment.NewListEnvironmentHandler
-	listEnvEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbGet,
-			Method: types.HTTPVerbGet,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/environments",
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.ClusterScope,
-			},
-		},
-	)
-
-	listEnvHandler := environment.NewListEnvironmentHandler(
-		config,
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &Route{
-		Endpoint: listEnvEndpoint,
-		Handler:  listEnvHandler,
-		Router:   r,
-	})
-
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/deployments -> environment.NewListDeploymentsByClusterHandler
-	listDeploymentsEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbGet,
-			Method: types.HTTPVerbGet,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/deployments",
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.ClusterScope,
-			},
-		},
-	)
-
-	listDeploymentsHandler := environment.NewListDeploymentsByClusterHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &Route{
-		Endpoint: listDeploymentsEndpoint,
-		Handler:  listDeploymentsHandler,
-		Router:   r,
-	})
+	if config.ServerConf.GithubIncomingWebhookSecret != "" {
+
+		// GET /api/projects/{project_id}/clusters/{cluster_id}/environments -> environment.NewListEnvironmentHandler
+		listEnvEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbGet,
+				Method: types.HTTPVerbGet,
+				Path: &types.Path{
+					Parent:       basePath,
+					RelativePath: relPath + "/environments",
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		listEnvHandler := environment.NewListEnvironmentHandler(
+			config,
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &Route{
+			Endpoint: listEnvEndpoint,
+			Handler:  listEnvHandler,
+			Router:   r,
+		})
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/{environment_id}/deployment -> environment.NewGetDeploymentByClusterHandler
-	getDeploymentEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbGet,
-			Method: types.HTTPVerbGet,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/{environment_id}/deployment",
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.ClusterScope,
-			},
-		},
-	)
+		// GET /api/projects/{project_id}/clusters/{cluster_id}/deployments -> environment.NewListDeploymentsByClusterHandler
+		listDeploymentsEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbGet,
+				Method: types.HTTPVerbGet,
+				Path: &types.Path{
+					Parent:       basePath,
+					RelativePath: relPath + "/deployments",
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		listDeploymentsHandler := environment.NewListDeploymentsByClusterHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &Route{
+			Endpoint: listDeploymentsEndpoint,
+			Handler:  listDeploymentsHandler,
+			Router:   r,
+		})
 
-	getDeploymentHandler := environment.NewGetDeploymentByClusterHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
+		// GET /api/projects/{project_id}/clusters/{cluster_id}/{environment_id}/deployment -> environment.NewGetDeploymentByClusterHandler
+		getDeploymentEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbGet,
+				Method: types.HTTPVerbGet,
+				Path: &types.Path{
+					Parent:       basePath,
+					RelativePath: relPath + "/{environment_id}/deployment",
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		getDeploymentHandler := environment.NewGetDeploymentByClusterHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &Route{
+			Endpoint: getDeploymentEndpoint,
+			Handler:  getDeploymentHandler,
+			Router:   r,
+		})
 
-	routes = append(routes, &Route{
-		Endpoint: getDeploymentEndpoint,
-		Handler:  getDeploymentHandler,
-		Router:   r,
-	})
+	}
 
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces -> cluster.NewClusterListNamespacesHandler
 	listNamespacesEndpoint := factory.NewAPIEndpoint(

+ 318 - 314
api/server/router/git_installation.go

@@ -112,329 +112,333 @@ func getGitInstallationRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id} ->
-	// environment.NewCreateEnvironmentHandler
-	createEnvironmentEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbCreate,
-			Method: types.HTTPVerbPost,
-			Path: &types.Path{
-				Parent: basePath,
-				RelativePath: fmt.Sprintf(
-					"%s/{%s}/{%s}/clusters/{cluster_id}/environment",
-					relPath,
-					types.URLParamGitRepoOwner,
-					types.URLParamGitRepoName,
-				),
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.GitInstallationScope,
-				types.ClusterScope,
-			},
-		},
-	)
-
-	createEnvironmentHandler := environment.NewCreateEnvironmentHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &Route{
-		Endpoint: createEnvironmentEndpoint,
-		Handler:  createEnvironmentHandler,
-		Router:   r,
-	})
-
-	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment ->
-	// environment.NewCreateDeploymentHandler
-	createDeploymentEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbCreate,
-			Method: types.HTTPVerbPost,
-			Path: &types.Path{
-				Parent: basePath,
-				RelativePath: fmt.Sprintf(
-					"%s/{%s}/{%s}/clusters/{cluster_id}/deployment",
-					relPath,
-					types.URLParamGitRepoOwner,
-					types.URLParamGitRepoName,
-				),
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.GitInstallationScope,
-				types.ClusterScope,
-			},
-		},
-	)
-
-	createDeploymentHandler := environment.NewCreateDeploymentHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &Route{
-		Endpoint: createDeploymentEndpoint,
-		Handler:  createDeploymentHandler,
-		Router:   r,
-	})
-
-	// GET /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment ->
-	// environment.NewCreateDeploymentHandler
-	getDeploymentEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbGet,
-			Method: types.HTTPVerbGet,
-			Path: &types.Path{
-				Parent: basePath,
-				RelativePath: fmt.Sprintf(
-					"%s/{%s}/{%s}/clusters/{cluster_id}/deployment",
-					relPath,
-					types.URLParamGitRepoOwner,
-					types.URLParamGitRepoName,
-				),
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.GitInstallationScope,
-				types.ClusterScope,
-			},
-		},
-	)
-
-	getDeploymentHandler := environment.NewGetDeploymentHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &Route{
-		Endpoint: getDeploymentEndpoint,
-		Handler:  getDeploymentHandler,
-		Router:   r,
-	})
-
-	// GET /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployments ->
-	// environment.NewCreateDeploymentHandler
-	listDeploymentsEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbGet,
-			Method: types.HTTPVerbGet,
-			Path: &types.Path{
-				Parent: basePath,
-				RelativePath: fmt.Sprintf(
-					"%s/{%s}/{%s}/clusters/{cluster_id}/deployments",
-					relPath,
-					types.URLParamGitRepoOwner,
-					types.URLParamGitRepoName,
-				),
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.GitInstallationScope,
-				types.ClusterScope,
-			},
-		},
-	)
-
-	listDeploymentsHandler := environment.NewListDeploymentsHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &Route{
-		Endpoint: listDeploymentsEndpoint,
-		Handler:  listDeploymentsHandler,
-		Router:   r,
-	})
-
-	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment/finalize ->
-	// environment.NewFinalizeDeploymentHandler
-	finalizeDeploymentEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbCreate,
-			Method: types.HTTPVerbPost,
-			Path: &types.Path{
-				Parent: basePath,
-				RelativePath: fmt.Sprintf(
-					"%s/{%s}/{%s}/clusters/{cluster_id}/deployment/finalize",
-					relPath,
-					types.URLParamGitRepoOwner,
-					types.URLParamGitRepoName,
-				),
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.GitInstallationScope,
-				types.ClusterScope,
-			},
-		},
-	)
-
-	finalizeDeploymentHandler := environment.NewFinalizeDeploymentHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &Route{
-		Endpoint: finalizeDeploymentEndpoint,
-		Handler:  finalizeDeploymentHandler,
-		Router:   r,
-	})
-
-	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment/update ->
-	// environment.NewFinalizeDeploymentHandler
-	updateDeploymentEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbUpdate,
-			Method: types.HTTPVerbPost,
-			Path: &types.Path{
-				Parent: basePath,
-				RelativePath: fmt.Sprintf(
-					"%s/{%s}/{%s}/clusters/{cluster_id}/deployment/update",
-					relPath,
-					types.URLParamGitRepoOwner,
-					types.URLParamGitRepoName,
-				),
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.GitInstallationScope,
-				types.ClusterScope,
-			},
-		},
-	)
-
-	updateDeploymentHandler := environment.NewUpdateDeploymentHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &Route{
-		Endpoint: updateDeploymentEndpoint,
-		Handler:  updateDeploymentHandler,
-		Router:   r,
-	})
+	if config.ServerConf.GithubIncomingWebhookSecret != "" {
+
+		// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id} ->
+		// environment.NewCreateEnvironmentHandler
+		createEnvironmentEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbCreate,
+				Method: types.HTTPVerbPost,
+				Path: &types.Path{
+					Parent: basePath,
+					RelativePath: fmt.Sprintf(
+						"%s/{%s}/{%s}/clusters/{cluster_id}/environment",
+						relPath,
+						types.URLParamGitRepoOwner,
+						types.URLParamGitRepoName,
+					),
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.GitInstallationScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		createEnvironmentHandler := environment.NewCreateEnvironmentHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &Route{
+			Endpoint: createEnvironmentEndpoint,
+			Handler:  createEnvironmentHandler,
+			Router:   r,
+		})
 
-	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment/update/status ->
-	// environment.NewUpdateDeploymentStatusHandler
-	updateDeploymentStatusEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbUpdate,
-			Method: types.HTTPVerbPost,
-			Path: &types.Path{
-				Parent: basePath,
-				RelativePath: fmt.Sprintf(
-					"%s/{%s}/{%s}/clusters/{cluster_id}/deployment/update/status",
-					relPath,
-					types.URLParamGitRepoOwner,
-					types.URLParamGitRepoName,
-				),
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.GitInstallationScope,
-				types.ClusterScope,
-			},
-		},
-	)
+		// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment ->
+		// environment.NewCreateDeploymentHandler
+		createDeploymentEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbCreate,
+				Method: types.HTTPVerbPost,
+				Path: &types.Path{
+					Parent: basePath,
+					RelativePath: fmt.Sprintf(
+						"%s/{%s}/{%s}/clusters/{cluster_id}/deployment",
+						relPath,
+						types.URLParamGitRepoOwner,
+						types.URLParamGitRepoName,
+					),
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.GitInstallationScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		createDeploymentHandler := environment.NewCreateDeploymentHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &Route{
+			Endpoint: createDeploymentEndpoint,
+			Handler:  createDeploymentHandler,
+			Router:   r,
+		})
 
-	updateDeploymentStatusHandler := environment.NewUpdateDeploymentStatusHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
+		// GET /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment ->
+		// environment.NewCreateDeploymentHandler
+		getDeploymentEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbGet,
+				Method: types.HTTPVerbGet,
+				Path: &types.Path{
+					Parent: basePath,
+					RelativePath: fmt.Sprintf(
+						"%s/{%s}/{%s}/clusters/{cluster_id}/deployment",
+						relPath,
+						types.URLParamGitRepoOwner,
+						types.URLParamGitRepoName,
+					),
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.GitInstallationScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		getDeploymentHandler := environment.NewGetDeploymentHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &Route{
+			Endpoint: getDeploymentEndpoint,
+			Handler:  getDeploymentHandler,
+			Router:   r,
+		})
 
-	routes = append(routes, &Route{
-		Endpoint: updateDeploymentStatusEndpoint,
-		Handler:  updateDeploymentStatusHandler,
-		Router:   r,
-	})
+		// GET /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployments ->
+		// environment.NewCreateDeploymentHandler
+		listDeploymentsEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbGet,
+				Method: types.HTTPVerbGet,
+				Path: &types.Path{
+					Parent: basePath,
+					RelativePath: fmt.Sprintf(
+						"%s/{%s}/{%s}/clusters/{cluster_id}/deployments",
+						relPath,
+						types.URLParamGitRepoOwner,
+						types.URLParamGitRepoName,
+					),
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.GitInstallationScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		listDeploymentsHandler := environment.NewListDeploymentsHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &Route{
+			Endpoint: listDeploymentsEndpoint,
+			Handler:  listDeploymentsHandler,
+			Router:   r,
+		})
 
-	// DELETE /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/environment ->
-	// environment.NewDeleteEnvironmentHandler
-	deleteEnvironmentEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbDelete,
-			Method: types.HTTPVerbDelete,
-			Path: &types.Path{
-				Parent: basePath,
-				RelativePath: fmt.Sprintf(
-					"%s/{%s}/{%s}/clusters/{cluster_id}/environment",
-					relPath,
-					types.URLParamGitRepoOwner,
-					types.URLParamGitRepoName,
-				),
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.GitInstallationScope,
-				types.ClusterScope,
-			},
-		},
-	)
+		// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment/finalize ->
+		// environment.NewFinalizeDeploymentHandler
+		finalizeDeploymentEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbCreate,
+				Method: types.HTTPVerbPost,
+				Path: &types.Path{
+					Parent: basePath,
+					RelativePath: fmt.Sprintf(
+						"%s/{%s}/{%s}/clusters/{cluster_id}/deployment/finalize",
+						relPath,
+						types.URLParamGitRepoOwner,
+						types.URLParamGitRepoName,
+					),
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.GitInstallationScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		finalizeDeploymentHandler := environment.NewFinalizeDeploymentHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &Route{
+			Endpoint: finalizeDeploymentEndpoint,
+			Handler:  finalizeDeploymentHandler,
+			Router:   r,
+		})
 
-	deleteEnvironmentHandler := environment.NewDeleteEnvironmentHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
+		// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment/update ->
+		// environment.NewFinalizeDeploymentHandler
+		updateDeploymentEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbUpdate,
+				Method: types.HTTPVerbPost,
+				Path: &types.Path{
+					Parent: basePath,
+					RelativePath: fmt.Sprintf(
+						"%s/{%s}/{%s}/clusters/{cluster_id}/deployment/update",
+						relPath,
+						types.URLParamGitRepoOwner,
+						types.URLParamGitRepoName,
+					),
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.GitInstallationScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		updateDeploymentHandler := environment.NewUpdateDeploymentHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &Route{
+			Endpoint: updateDeploymentEndpoint,
+			Handler:  updateDeploymentHandler,
+			Router:   r,
+		})
 
-	routes = append(routes, &Route{
-		Endpoint: deleteEnvironmentEndpoint,
-		Handler:  deleteEnvironmentHandler,
-		Router:   r,
-	})
+		// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment/update/status ->
+		// environment.NewUpdateDeploymentStatusHandler
+		updateDeploymentStatusEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbUpdate,
+				Method: types.HTTPVerbPost,
+				Path: &types.Path{
+					Parent: basePath,
+					RelativePath: fmt.Sprintf(
+						"%s/{%s}/{%s}/clusters/{cluster_id}/deployment/update/status",
+						relPath,
+						types.URLParamGitRepoOwner,
+						types.URLParamGitRepoName,
+					),
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.GitInstallationScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		updateDeploymentStatusHandler := environment.NewUpdateDeploymentStatusHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &Route{
+			Endpoint: updateDeploymentStatusEndpoint,
+			Handler:  updateDeploymentStatusHandler,
+			Router:   r,
+		})
 
-	// DELETE /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment ->
-	// environment.NewDeleteDeploymentHandler
-	deleteDeploymentEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbDelete,
-			Method: types.HTTPVerbDelete,
-			Path: &types.Path{
-				Parent: basePath,
-				RelativePath: fmt.Sprintf(
-					"%s/{%s}/{%s}/clusters/{cluster_id}/deployment",
-					relPath,
-					types.URLParamGitRepoOwner,
-					types.URLParamGitRepoName,
-				),
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.GitInstallationScope,
-				types.ClusterScope,
-			},
-		},
-	)
+		// DELETE /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/environment ->
+		// environment.NewDeleteEnvironmentHandler
+		deleteEnvironmentEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbDelete,
+				Method: types.HTTPVerbDelete,
+				Path: &types.Path{
+					Parent: basePath,
+					RelativePath: fmt.Sprintf(
+						"%s/{%s}/{%s}/clusters/{cluster_id}/environment",
+						relPath,
+						types.URLParamGitRepoOwner,
+						types.URLParamGitRepoName,
+					),
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.GitInstallationScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		deleteEnvironmentHandler := environment.NewDeleteEnvironmentHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &Route{
+			Endpoint: deleteEnvironmentEndpoint,
+			Handler:  deleteEnvironmentHandler,
+			Router:   r,
+		})
 
-	deleteDeploymentHandler := environment.NewDeleteDeploymentHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
+		// DELETE /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment ->
+		// environment.NewDeleteDeploymentHandler
+		deleteDeploymentEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbDelete,
+				Method: types.HTTPVerbDelete,
+				Path: &types.Path{
+					Parent: basePath,
+					RelativePath: fmt.Sprintf(
+						"%s/{%s}/{%s}/clusters/{cluster_id}/deployment",
+						relPath,
+						types.URLParamGitRepoOwner,
+						types.URLParamGitRepoName,
+					),
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.GitInstallationScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		deleteDeploymentHandler := environment.NewDeleteDeploymentHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &Route{
+			Endpoint: deleteDeploymentEndpoint,
+			Handler:  deleteDeploymentHandler,
+			Router:   r,
+		})
 
-	routes = append(routes, &Route{
-		Endpoint: deleteDeploymentEndpoint,
-		Handler:  deleteDeploymentHandler,
-		Router:   r,
-	})
+	}
 
 	// GET /api/projects/{project_id}/gitrepos/{git_installation_id}/repos ->
 	// gitinstallation.GithubListReposHandler

+ 2 - 0
api/server/shared/config/env/envconfs.go

@@ -37,6 +37,8 @@ type ServerConf struct {
 	GithubClientSecret string `env:"GITHUB_CLIENT_SECRET"`
 	GithubLoginEnabled bool   `env:"GITHUB_LOGIN_ENABLED,default=true"`
 
+	GithubIncomingWebhookSecret string `env:"GITHUB_INCOMING_WEBHOOK_SECRET"`
+
 	GithubAppClientID      string `env:"GITHUB_APP_CLIENT_ID"`
 	GithubAppClientSecret  string `env:"GITHUB_APP_CLIENT_SECRET"`
 	GithubAppName          string `env:"GITHUB_APP_NAME"`

+ 8 - 1
api/types/environment.go

@@ -10,11 +10,15 @@ type Environment struct {
 	GitRepoOwner      string `json:"git_repo_owner"`
 	GitRepoName       string `json:"git_repo_name"`
 
-	Name string `json:"name"`
+	Name         string `json:"name"`
+	Mode         string `json:"mode"`
+	PRCount      uint   `json:"pr_count"`
+	LastPRStatus string `json:"last_pr_status"`
 }
 
 type CreateEnvironmentRequest struct {
 	Name string `json:"name" form:"required"`
+	Mode string `json:"mode" form:"required"`
 }
 
 type GitHubMetadata struct {
@@ -23,6 +27,8 @@ type GitHubMetadata struct {
 	RepoName     string `json:"gh_repo_name"`
 	RepoOwner    string `json:"gh_repo_owner"`
 	CommitSHA    string `json:"gh_commit_sha"`
+	PRBranchFrom string `json:"gh_pr_branch_from"`
+	PRBranchInto string `json:"gh_pr_branch_into"`
 }
 
 type DeploymentStatus string
@@ -32,6 +38,7 @@ const (
 	DeploymentStatusCreating DeploymentStatus = "creating"
 	DeploymentStatusInactive DeploymentStatus = "inactive"
 	DeploymentStatusFailed   DeploymentStatus = "failed"
+	DeploymentStatusDisabled DeploymentStatus = "disabled"
 )
 
 type Deployment struct {

+ 1 - 0
internal/environment/utils.go

@@ -0,0 +1 @@
+package environment

+ 15 - 2
internal/models/environment.go

@@ -5,6 +5,8 @@ import (
 	"gorm.io/gorm"
 )
 
+type EnvironmentMode uint
+
 type Environment struct {
 	gorm.Model
 
@@ -14,7 +16,10 @@ type Environment struct {
 	GitRepoOwner      string
 	GitRepoName       string
 
-	Name string
+	Name         string
+	Mode         string
+	PRCount      uint
+	LastPRStatus string
 }
 
 func (e *Environment) ToEnvironmentType() *types.Environment {
@@ -25,7 +30,11 @@ func (e *Environment) ToEnvironmentType() *types.Environment {
 		GitInstallationID: e.GitInstallationID,
 		GitRepoOwner:      e.GitRepoOwner,
 		GitRepoName:       e.GitRepoName,
-		Name:              e.Name,
+
+		Name:         e.Name,
+		Mode:         e.Mode,
+		PRCount:      e.PRCount,
+		LastPRStatus: e.LastPRStatus,
 	}
 }
 
@@ -42,6 +51,8 @@ type Deployment struct {
 	RepoName       string
 	RepoOwner      string
 	CommitSHA      string
+	PRBranchFrom   string
+	PRBranchInto   string
 }
 
 func (d *Deployment) ToDeploymentType() *types.Deployment {
@@ -52,6 +63,8 @@ func (d *Deployment) ToDeploymentType() *types.Deployment {
 		RepoName:     d.RepoName,
 		RepoOwner:    d.RepoOwner,
 		CommitSHA:    d.CommitSHA,
+		PRBranchFrom: d.PRBranchFrom,
+		PRBranchInto: d.PRBranchInto,
 	}
 
 	return &types.Deployment{

+ 2 - 0
internal/repository/environment.go

@@ -6,11 +6,13 @@ type EnvironmentRepository interface {
 	CreateEnvironment(env *models.Environment) (*models.Environment, error)
 	ReadEnvironment(projectID, clusterID, gitInstallationID uint, gitRepoOwner, gitRepoName string) (*models.Environment, error)
 	ReadEnvironmentByID(projectID, clusterID, envID uint) (*models.Environment, error)
+	ReadEnvironmentByOwnerRepoName(owner, repo string) (*models.Environment, error)
 	ListEnvironments(projectID, clusterID uint) ([]*models.Environment, error)
 	DeleteEnvironment(env *models.Environment) (*models.Environment, error)
 	CreateDeployment(deployment *models.Deployment) (*models.Deployment, error)
 	ReadDeployment(environmentID uint, namespace string) (*models.Deployment, error)
 	ReadDeploymentByCluster(projectID, clusterID uint, namespace string) (*models.Deployment, error)
+	ReadDeploymentByGitDetails(environmentID uint, owner, repo string, prNumber uint) (*models.Deployment, error)
 	ListDeploymentsByCluster(projectID, clusterID uint, states ...string) ([]*models.Deployment, error)
 	ListDeployments(environmentID uint, states ...string) ([]*models.Deployment, error)
 	UpdateDeployment(deployment *models.Deployment) (*models.Deployment, error)

+ 27 - 0
internal/repository/gorm/environment.go

@@ -51,6 +51,18 @@ func (repo *EnvironmentRepository) ReadEnvironmentByID(projectID, clusterID, env
 	return env, nil
 }
 
+func (repo *EnvironmentRepository) ReadEnvironmentByOwnerRepoName(
+	gitRepoOwner, gitRepoName string,
+) (*models.Environment, error) {
+	env := &models.Environment{}
+	if err := repo.db.Order("id desc").Where("git_repo_owner = ? AND git_repo_name = ?",
+		gitRepoOwner, gitRepoName,
+	).First(&env).Error; err != nil {
+		return nil, err
+	}
+	return env, nil
+}
+
 func (repo *EnvironmentRepository) ListEnvironments(projectID, clusterID uint) ([]*models.Environment, error) {
 	envs := make([]*models.Environment, 0)
 
@@ -105,6 +117,21 @@ func (repo *EnvironmentRepository) ReadDeploymentByCluster(projectID, clusterID
 	return depl, nil
 }
 
+func (repo *EnvironmentRepository) ReadDeploymentByGitDetails(
+	environmentID uint, gitRepoOwner, gitRepoName string, prNumber uint,
+) (*models.Deployment, error) {
+	depl := &models.Deployment{}
+
+	if err := repo.db.Order("id asc").
+		Where("environment_id = ? AND repo_owner = ? AND repo_name = ? AND pull_request_id = ?",
+			environmentID, gitRepoOwner, gitRepoName, prNumber).
+		First(&depl).Error; err != nil {
+		return nil, err
+	}
+
+	return depl, nil
+}
+
 func (repo *EnvironmentRepository) ListDeploymentsByCluster(projectID, clusterID uint, states ...string) ([]*models.Deployment, error) {
 	query := repo.db.
 		Order("deployments.updated_at desc").