Просмотр исходного кода

Merge pull request #2223 from porter-dev/nafees/pr-env-toggle

[POR-512] Disable new comments for preview environments
abelanger5 3 лет назад
Родитель
Сommit
375c32d1cf

+ 9 - 8
api/server/handlers/environment/create.go

@@ -64,14 +64,15 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	}
 	}
 
 
 	env, err := c.Repo().Environment().CreateEnvironment(&models.Environment{
 	env, err := c.Repo().Environment().CreateEnvironment(&models.Environment{
-		ProjectID:         project.ID,
-		ClusterID:         cluster.ID,
-		GitInstallationID: uint(ga.InstallationID),
-		Name:              request.Name,
-		GitRepoOwner:      owner,
-		GitRepoName:       name,
-		Mode:              request.Mode,
-		WebhookID:         string(webhookUID),
+		ProjectID:           project.ID,
+		ClusterID:           cluster.ID,
+		GitInstallationID:   uint(ga.InstallationID),
+		Name:                request.Name,
+		GitRepoOwner:        owner,
+		GitRepoName:         name,
+		Mode:                request.Mode,
+		WebhookID:           string(webhookUID),
+		NewCommentsDisabled: false,
 	})
 	})
 
 
 	if err != nil {
 	if err != nil {

+ 108 - 7
api/server/handlers/environment/finalize_deployment.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"strings"
 
 
 	"github.com/google/go-github/v41/github"
 	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -14,6 +15,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/repository"
 )
 )
 
 
 type FinalizeDeploymentHandler struct {
 type FinalizeDeploymentHandler struct {
@@ -146,20 +148,119 @@ func (c *FinalizeDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 		}
 		}
 	}
 	}
 
 
-	_, _, err = client.Issues.CreateComment(
+	err = createOrUpdateComment(client, c.Repo(), env.NewCommentsDisabled, depl, github.String(commentBody))
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, depl.ToDeploymentType())
+}
+
+func createOrUpdateComment(
+	client *github.Client,
+	repo repository.Repository,
+	newCommentsDisabled bool,
+	depl *models.Deployment,
+	commentBody *string,
+) error {
+	// when updating a PR comment, we have to handle several cases:
+	//   1. when a Porter environment has deployment status repeat-comments enabled
+	//      - nothing special here, simply create a new comment in the PR
+	//   2. when a Porter environment has deployment status repeat-comments disabled
+	//      - when a Porter deployment has Github comment ID saved in the DB
+	//        - try to update the comment using the Github comment ID
+	//        - if the above fails, try creating a new comment and save the new comment ID in the DB
+	//      - when a Porter deployment does not have a Github comment ID saved in the DB
+	//        - create a new comment and save the Github comment ID in the DB
+
+	if newCommentsDisabled {
+		if depl.GHPRCommentID == 0 {
+			// create a new comment
+			err := createGithubComment(client, repo, depl, commentBody)
+
+			if err != nil {
+				return err
+			}
+		} else {
+			err := updateGithubComment(
+				client, depl.RepoOwner, depl.RepoName, depl.GHPRCommentID, commentBody,
+			)
+
+			if err != nil {
+				if strings.Contains(err.Error(), "404") {
+					// perhaps a deleted comment?
+					// create a new comment
+					err := createGithubComment(client, repo, depl, commentBody)
+
+					if err != nil {
+						return fmt.Errorf("invalid github comment ID for deployment with ID: %d. Error creating "+
+							"new comment: %w", depl.ID, err)
+					}
+				}
+
+				return err
+			}
+		}
+	} else {
+		err := createGithubComment(client, repo, depl, commentBody)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func createGithubComment(
+	client *github.Client,
+	repo repository.Repository,
+	depl *models.Deployment,
+	body *string,
+) error {
+	ghResp, _, err := client.Issues.CreateComment(
 		context.Background(),
 		context.Background(),
-		env.GitRepoOwner,
-		env.GitRepoName,
+		depl.RepoOwner,
+		depl.RepoName,
 		int(depl.PullRequestID),
 		int(depl.PullRequestID),
 		&github.IssueComment{
 		&github.IssueComment{
-			Body: github.String(commentBody),
+			Body: body,
 		},
 		},
 	)
 	)
 
 
 	if err != nil {
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+		return fmt.Errorf("error creating new github comment for owner: %s repo %s prNumber: %d. Error: %w",
+			depl.RepoOwner, depl.RepoName, depl.PullRequestID, err)
 	}
 	}
 
 
-	c.WriteResult(w, r, depl.ToDeploymentType())
+	depl.GHPRCommentID = ghResp.GetID()
+
+	_, err = repo.Environment().UpdateDeployment(depl)
+
+	if err != nil {
+		return fmt.Errorf("error updating deployment with ID: %d. Error: %w", depl.ID, err)
+	}
+
+	return nil
+}
+
+func updateGithubComment(
+	client *github.Client,
+	owner, repo string,
+	commentID int64,
+	body *string,
+) error {
+	_, _, err := client.Issues.EditComment(
+		context.Background(),
+		owner,
+		repo,
+		commentID,
+		&github.IssueComment{
+			Body: body,
+		},
+	)
+
+	return err
 }
 }

+ 2 - 12
api/server/handlers/environment/finalize_deployment_with_errors.go

@@ -145,20 +145,10 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 		commentBody += fmt.Sprintf("<details>\n  <summary><code>%s</code></summary>\n\n  **Error:** %s\n</details>\n", res, err)
 		commentBody += fmt.Sprintf("<details>\n  <summary><code>%s</code></summary>\n\n  **Error:** %s\n</details>\n", res, err)
 	}
 	}
 
 
-	_, _, err = client.Issues.CreateComment(
-		context.Background(),
-		env.GitRepoOwner,
-		env.GitRepoName,
-		int(depl.PullRequestID),
-		&github.IssueComment{
-			Body: github.String(commentBody),
-		},
-	)
+	err = createOrUpdateComment(client, c.Repo(), env.NewCommentsDisabled, depl, github.String(commentBody))
 
 
 	if err != nil {
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("error creating github comment: %w", err), http.StatusConflict,
-		))
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
 
 

+ 55 - 0
api/server/handlers/environment/get_environment.go

@@ -0,0 +1,55 @@
+package environment
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"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/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type GetEnvironmentHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewGetEnvironmentHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *GetEnvironmentHandler {
+	return &GetEnvironmentHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *GetEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	envID, reqErr := requestutils.GetURLParamUint(r, "environment_id")
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	env, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, envID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("no such environment with ID: %d", envID)))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error reading environment with ID: %d. Error: %w", envID, err)))
+		return
+	}
+
+	c.WriteResult(w, r, env.ToEnvironmentType())
+}

+ 74 - 0
api/server/handlers/environment/toggle_new_comment.go

@@ -0,0 +1,74 @@
+package environment
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"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/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type ToggleNewCommentHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewToggleNewCommentHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ToggleNewCommentHandler {
+	return &ToggleNewCommentHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *ToggleNewCommentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	environmentID, reqErr := requestutils.GetURLParamUint(r, "environment_id")
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	request := &types.ToggleNewCommentRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	env, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, environmentID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("no such environment with ID: %d", environmentID)))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if env.NewCommentsDisabled != request.Disable {
+		env.NewCommentsDisabled = request.Disable
+
+		_, err = c.Repo().Environment().UpdateEnvironment(env)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+}

+ 57 - 0
api/server/router/cluster.go

@@ -319,6 +319,63 @@ func getClusterRoutes(
 			Router:   r,
 			Router:   r,
 		})
 		})
 
 
+		// GET /api/projects/{project_id}/clusters/{cluster_id}/environments/{environment_id} -> environment.NewGetEnvironmentHandler
+		getEnvEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbGet,
+				Method: types.HTTPVerbGet,
+				Path: &types.Path{
+					Parent:       basePath,
+					RelativePath: relPath + "/environments/{environment_id}",
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		getEnvHandler := environment.NewGetEnvironmentHandler(
+			config,
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &router.Route{
+			Endpoint: getEnvEndpoint,
+			Handler:  getEnvHandler,
+			Router:   r,
+		})
+
+		// PATCH /api/projects/{project_id}/clusters/{cluster_id}/environment/{environment_id}/toggle_new_comment -> environment.NewToggleNewCommentHandler
+		toggleNewCommentEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbUpdate,
+				Method: types.HTTPVerbPatch,
+				Path: &types.Path{
+					Parent:       basePath,
+					RelativePath: relPath + "/environments/{environment_id}/toggle_new_comment",
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		toggleNewCommentHandler := environment.NewToggleNewCommentHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &router.Route{
+			Endpoint: toggleNewCommentEndpoint,
+			Handler:  toggleNewCommentHandler,
+			Router:   r,
+		})
+
 		// GET /api/projects/{project_id}/clusters/{cluster_id}/deployments -> environment.NewListDeploymentsByClusterHandler
 		// GET /api/projects/{project_id}/clusters/{cluster_id}/deployments -> environment.NewListDeploymentsByClusterHandler
 		listDeploymentsEndpoint := factory.NewAPIEndpoint(
 		listDeploymentsEndpoint := factory.NewAPIEndpoint(
 			&types.APIRequestMetadata{
 			&types.APIRequestMetadata{

+ 5 - 0
api/types/environment.go

@@ -14,6 +14,7 @@ type Environment struct {
 	Mode                 string `json:"mode"`
 	Mode                 string `json:"mode"`
 	DeploymentCount      uint   `json:"deployment_count"`
 	DeploymentCount      uint   `json:"deployment_count"`
 	LastDeploymentStatus string `json:"last_deployment_status"`
 	LastDeploymentStatus string `json:"last_deployment_status"`
+	NewCommentsDisabled  bool   `json:"new_comments_disabled"`
 }
 }
 
 
 type CreateEnvironmentRequest struct {
 type CreateEnvironmentRequest struct {
@@ -123,4 +124,8 @@ type PullRequest struct {
 	BranchInto string `json:"branch_into"`
 	BranchInto string `json:"branch_into"`
 }
 }
 
 
+type ToggleNewCommentRequest struct {
+	Disable bool `json:"disable"`
+}
+
 type ListEnvironmentsResponse []*Environment
 type ListEnvironmentsResponse []*Environment

+ 83 - 3
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentList.tsx

@@ -16,6 +16,8 @@ import PullRequestCard from "./PullRequestCard";
 import DynamicLink from "components/DynamicLink";
 import DynamicLink from "components/DynamicLink";
 import { PreviewEnvironmentsHeader } from "../components/PreviewEnvironmentsHeader";
 import { PreviewEnvironmentsHeader } from "../components/PreviewEnvironmentsHeader";
 import SearchBar from "components/SearchBar";
 import SearchBar from "components/SearchBar";
+import CheckboxRow from "components/form-components/CheckboxRow";
+import DocsHelper from "components/DocsHelper";
 
 
 const AvailableStatusFilters = [
 const AvailableStatusFilters = [
   "all",
   "all",
@@ -34,6 +36,7 @@ const DeploymentList = () => {
   const [deploymentList, setDeploymentList] = useState<PRDeployment[]>([]);
   const [deploymentList, setDeploymentList] = useState<PRDeployment[]>([]);
   const [pullRequests, setPullRequests] = useState<PullRequest[]>([]);
   const [pullRequests, setPullRequests] = useState<PullRequest[]>([]);
   const [searchValue, setSearchValue] = useState("");
   const [searchValue, setSearchValue] = useState("");
+  const [newCommentsDisabled, setNewCommentsDisabled] = useState(false);
 
 
   const [
   const [
     statusSelectorVal,
     statusSelectorVal,
@@ -66,6 +69,18 @@ const DeploymentList = () => {
     // return mockRequest();
     // return mockRequest();
   };
   };
 
 
+  const getEnvironment = () => {
+    return api.getEnvironment(
+      "<token>",
+      {},
+      {
+        project_id: currentProject.id,
+        cluster_id: currentCluster.id,
+        environment_id: Number(environment_id),
+      }
+    );
+  };
+
   useEffect(() => {
   useEffect(() => {
     const status_filter = getQueryParam("status_filter");
     const status_filter = getQueryParam("status_filter");
 
 
@@ -93,7 +108,6 @@ const DeploymentList = () => {
 
 
         setDeploymentList(data.deployments || []);
         setDeploymentList(data.deployments || []);
         setPullRequests(data.pull_requests || []);
         setPullRequests(data.pull_requests || []);
-        setIsLoading(false);
       })
       })
       .catch((err) => {
       .catch((err) => {
         console.error(err);
         console.error(err);
@@ -101,6 +115,21 @@ const DeploymentList = () => {
           setHasError(true);
           setHasError(true);
         }
         }
       });
       });
+    getEnvironment()
+      .then(({ data }) => {
+        if (!isSubscribed) {
+          return;
+        }
+
+        setNewCommentsDisabled(data.new_comments_disabled || false);
+      })
+      .catch((err) => {
+        console.error(err);
+        if (isSubscribed) {
+          setHasError(true);
+        }
+      });
+    setIsLoading(false);
 
 
     return () => {
     return () => {
       isSubscribed = false;
       isSubscribed = false;
@@ -116,9 +145,15 @@ const DeploymentList = () => {
     } catch (error) {
     } catch (error) {
       setHasError(true);
       setHasError(true);
       console.error(error);
       console.error(error);
-    } finally {
-      setIsLoading(false);
     }
     }
+    try {
+      const { data } = await getEnvironment();
+      setNewCommentsDisabled(data.new_comments_disabled || false);
+    } catch (error) {
+      setHasError(true);
+      console.error(error);
+    }
+    setIsLoading(false);
   };
   };
 
 
   const handlePreviewEnvironmentManualCreation = (pullRequest: PullRequest) => {
   const handlePreviewEnvironmentManualCreation = (pullRequest: PullRequest) => {
@@ -232,6 +267,24 @@ const DeploymentList = () => {
     setStatusSelectorVal(value);
     setStatusSelectorVal(value);
   };
   };
 
 
+  const handleToggleCommentStatus = (currentlyDisabled: boolean) => {
+    api
+      .toggleNewCommentForEnvironment(
+        "<token>",
+        {
+          disable: !currentlyDisabled,
+        },
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          environment_id: Number(environment_id),
+        }
+      )
+      .then(() => {
+        setNewCommentsDisabled(!currentlyDisabled);
+      });
+  };
+
   return (
   return (
     <>
     <>
       <PreviewEnvironmentsHeader />
       <PreviewEnvironmentsHeader />
@@ -282,6 +335,24 @@ const DeploymentList = () => {
           </StyledStatusSelector>
           </StyledStatusSelector>
         </ActionsWrapper>
         </ActionsWrapper>
       </Flex>
       </Flex>
+      <Flex>
+        <ActionsWrapper>
+          <FlexWrap>
+            <CheckboxRow
+              label="Disable new comments for deployments"
+              checked={newCommentsDisabled}
+              toggle={() => handleToggleCommentStatus(newCommentsDisabled)}
+            />
+            <Div>
+              <DocsHelper
+                disableMargin
+                tooltipText="When checked, comments for every new deployment are disabled. Instead, the most recent comment is updated each time."
+                placement="top-end"
+              />
+            </Div>
+          </FlexWrap>
+        </ActionsWrapper>
+      </Flex>
       <Container>
       <Container>
         <EventsGrid>{renderDeploymentList()}</EventsGrid>
         <EventsGrid>{renderDeploymentList()}</EventsGrid>
       </Container>
       </Container>
@@ -307,6 +378,15 @@ const Flex = styled.div`
   align-items: center;
   align-items: center;
 `;
 `;
 
 
+const Div = styled.div`
+  margin-bottom: -7px;
+`;
+
+const FlexWrap = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
 const BackButton = styled(DynamicLink)`
 const BackButton = styled(DynamicLink)`
   cursor: pointer;
   cursor: pointer;
   font-size: 24px;
   font-size: 24px;

+ 28 - 0
dashboard/src/shared/api.tsx

@@ -201,6 +201,32 @@ const listEnvironments = baseApi<
   return `/api/projects/${project_id}/clusters/${cluster_id}/environments`;
   return `/api/projects/${project_id}/clusters/${cluster_id}/environments`;
 });
 });
 
 
+const getEnvironment = baseApi<
+  {},
+  {
+    project_id: number;
+    cluster_id: number;
+    environment_id: number;
+  }
+>("GET", (pathParams) => {
+  let { project_id, cluster_id, environment_id } = pathParams;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/environments/${environment_id}`;
+});
+
+const toggleNewCommentForEnvironment = baseApi<
+  {
+    disable: boolean;
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+    environment_id: number;
+  }
+>("PATCH", (pathParams) => {
+  let { project_id, cluster_id, environment_id } = pathParams;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/environments/${environment_id}/toggle_new_comment`;
+});
+
 const createGCPIntegration = baseApi<
 const createGCPIntegration = baseApi<
   {
   {
     gcp_key_data: string;
     gcp_key_data: string;
@@ -2073,6 +2099,8 @@ export default {
   createPreviewEnvironmentDeployment,
   createPreviewEnvironmentDeployment,
   reenablePreviewEnvironmentDeployment,
   reenablePreviewEnvironmentDeployment,
   listEnvironments,
   listEnvironments,
+  getEnvironment,
+  toggleNewCommentForEnvironment,
   createGCPIntegration,
   createGCPIntegration,
   createInvite,
   createInvite,
   createNamespace,
   createNamespace,

+ 5 - 0
internal/models/environment.go

@@ -19,6 +19,8 @@ type Environment struct {
 	Name string
 	Name string
 	Mode string
 	Mode string
 
 
+	NewCommentsDisabled bool
+
 	// WebhookID uniquely identifies the environment when other fields (project, cluster)
 	// WebhookID uniquely identifies the environment when other fields (project, cluster)
 	// aren't present
 	// aren't present
 	WebhookID string `gorm:"unique"`
 	WebhookID string `gorm:"unique"`
@@ -33,6 +35,8 @@ func (e *Environment) ToEnvironmentType() *types.Environment {
 		GitRepoOwner:      e.GitRepoOwner,
 		GitRepoOwner:      e.GitRepoOwner,
 		GitRepoName:       e.GitRepoName,
 		GitRepoName:       e.GitRepoName,
 
 
+		NewCommentsDisabled: e.NewCommentsDisabled,
+
 		Name: e.Name,
 		Name: e.Name,
 		Mode: e.Mode,
 		Mode: e.Mode,
 	}
 	}
@@ -47,6 +51,7 @@ type Deployment struct {
 	Subdomain      string
 	Subdomain      string
 	PullRequestID  uint
 	PullRequestID  uint
 	GHDeploymentID int64
 	GHDeploymentID int64
+	GHPRCommentID  int64
 	PRName         string
 	PRName         string
 	RepoName       string
 	RepoName       string
 	RepoOwner      string
 	RepoOwner      string

+ 1 - 0
internal/repository/environment.go

@@ -9,6 +9,7 @@ type EnvironmentRepository interface {
 	ReadEnvironmentByOwnerRepoName(projectID, clusterID uint, owner, repo string) (*models.Environment, error)
 	ReadEnvironmentByOwnerRepoName(projectID, clusterID uint, owner, repo string) (*models.Environment, error)
 	ReadEnvironmentByWebhookIDOwnerRepoName(webhookID, owner, repo string) (*models.Environment, error)
 	ReadEnvironmentByWebhookIDOwnerRepoName(webhookID, owner, repo string) (*models.Environment, error)
 	ListEnvironments(projectID, clusterID uint) ([]*models.Environment, error)
 	ListEnvironments(projectID, clusterID uint) ([]*models.Environment, error)
+	UpdateEnvironment(environment *models.Environment) (*models.Environment, error)
 	DeleteEnvironment(env *models.Environment) (*models.Environment, error)
 	DeleteEnvironment(env *models.Environment) (*models.Environment, error)
 	CreateDeployment(deployment *models.Deployment) (*models.Deployment, error)
 	CreateDeployment(deployment *models.Deployment) (*models.Deployment, error)
 	ReadDeployment(environmentID uint, namespace string) (*models.Deployment, error)
 	ReadDeployment(environmentID uint, namespace string) (*models.Deployment, error)

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

@@ -119,6 +119,14 @@ func (repo *EnvironmentRepository) ListEnvironments(projectID, clusterID uint) (
 	return envs, nil
 	return envs, nil
 }
 }
 
 
+func (repo *EnvironmentRepository) UpdateEnvironment(environment *models.Environment) (*models.Environment, error) {
+	if err := repo.db.Save(environment).Error; err != nil {
+		return nil, err
+	}
+
+	return environment, nil
+}
+
 func (repo *EnvironmentRepository) DeleteEnvironment(env *models.Environment) (*models.Environment, error) {
 func (repo *EnvironmentRepository) DeleteEnvironment(env *models.Environment) (*models.Environment, error) {
 	if err := repo.db.Delete(&env).Error; err != nil {
 	if err := repo.db.Delete(&env).Error; err != nil {
 		return nil, err
 		return nil, err

+ 4 - 0
internal/repository/test/environment.go

@@ -42,6 +42,10 @@ func (repo *EnvironmentRepository) ListEnvironments(projectID, clusterID uint) (
 	panic("unimplemented")
 	panic("unimplemented")
 }
 }
 
 
+func (repo *EnvironmentRepository) UpdateEnvironment(environment *models.Environment) (*models.Environment, error) {
+	panic("unimplemented")
+}
+
 func (repo *EnvironmentRepository) DeleteEnvironment(env *models.Environment) (*models.Environment, error) {
 func (repo *EnvironmentRepository) DeleteEnvironment(env *models.Environment) (*models.Environment, error) {
 	panic("unimplemented")
 	panic("unimplemented")
 }
 }