Browse Source

Merge pull request #2226 from porter-dev/staging

Preview env improvements + versioning bumps -> prod
abelanger5 3 years ago
parent
commit
a4a07bc413
30 changed files with 1337 additions and 146 deletions
  1. 15 1
      .github/workflows/release.yaml
  2. 29 18
      api/server/handlers/environment/create.go
  3. 16 0
      api/server/handlers/environment/create_deployment.go
  4. 28 0
      api/server/handlers/environment/delete.go
  5. 8 2
      api/server/handlers/environment/enable_pull_request.go
  6. 144 9
      api/server/handlers/environment/finalize_deployment.go
  7. 26 19
      api/server/handlers/environment/finalize_deployment_with_errors.go
  8. 55 0
      api/server/handlers/environment/get_environment.go
  9. 28 8
      api/server/handlers/environment/reenable_deployment.go
  10. 74 0
      api/server/handlers/environment/toggle_new_comment.go
  11. 20 0
      api/server/handlers/environment/trigger_deployment_workflow.go
  12. 18 0
      api/server/handlers/environment/update_deployment.go
  13. 27 0
      api/server/handlers/environment/update_deployment_status.go
  14. 61 0
      api/server/handlers/status/github.go
  15. 3 1
      api/server/handlers/webhook/github_incoming.go
  16. 57 0
      api/server/router/cluster.go
  17. 2 1
      api/server/router/router.go
  18. 83 0
      api/server/router/status.go
  19. 5 0
      api/types/environment.go
  20. 6 0
      api/types/status.go
  21. 14 5
      cli/cmd/apply.go
  22. 80 10
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/PreviewEnvironmentsHeader.tsx
  23. 83 3
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentList.tsx
  24. 36 0
      dashboard/src/shared/api.tsx
  25. 95 69
      go.mod
  26. 304 0
      go.sum
  27. 7 0
      internal/models/environment.go
  28. 1 0
      internal/repository/environment.go
  29. 8 0
      internal/repository/gorm/environment.go
  30. 4 0
      internal/repository/test/environment.go

+ 15 - 1
.github/workflows/release.yaml

@@ -3,6 +3,20 @@ on:
     types: [released]
 name: Update binaries 
 jobs:
+  update-self-hosted-helm-registry:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Get tag name
+        id: tag_name
+        run: |
+          tag=${GITHUB_TAG/refs\/tags\//}
+          echo ::set-output name=tag::$tag
+        env:
+          GITHUB_TAG: ${{ github.ref }}
+      - name: Run workflow
+        run: gh workflow run release.yaml --repo porter-dev/porter-self-hosted -f version=${{steps.tag_name.outputs.tag}}
+        env:
+          GITHUB_TOKEN: ${{ secrets.PORTER_DEV_GITHUB_TOKEN }}  
   push-docker-server-latest:
     runs-on: ubuntu-latest
     steps:
@@ -95,4 +109,4 @@ jobs:
 
           git add Formula
           git commit -m "Update to version ${{steps.tag_name.outputs.tag}}"
-          git push origin main
+          git push origin main

+ 29 - 18
api/server/handlers/environment/create.go

@@ -63,34 +63,30 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	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),
-	})
-
-	if err != nil {
-		c.deleteEnvAndReportError(w, r, env, err)
-		return
+	env := &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),
+		NewCommentsDisabled: false,
 	}
 
 	// write Github actions files to the repo
 	client, err := getGithubClientFromEnvironment(c.Config(), env)
 
 	if err != nil {
-		c.deleteEnvAndReportError(w, r, env, err)
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	webhookURL := fmt.Sprintf("%s/api/github/incoming_webhook/%s", c.Config().ServerConf.ServerURL, string(webhookUID))
+	webhookURL := getGithubWebhookURLFromUID(c.Config().ServerConf.ServerURL, string(webhookUID))
 
 	// create incoming webhook
-	_, _, err = client.Repositories.CreateHook(
+	hook, _, err := client.Repositories.CreateHook(
 		r.Context(), owner, name, &github.Hook{
 			Config: map[string]interface{}{
 				"url":          webhookURL,
@@ -103,7 +99,18 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	)
 
 	if err != nil && !strings.Contains(err.Error(), "already exists on this repository") {
-		c.deleteEnvAndReportError(w, r, env, err)
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("error trying to create a new github repository webhook: %w", err), http.StatusConflict),
+		)
+		return
+	}
+
+	env.GithubWebhookID = hook.GetID()
+
+	env, err = c.Repo().Environment().CreateEnvironment(env)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -187,3 +194,7 @@ func getGithubClientFromEnvironment(config *config.Config, env *models.Environme
 
 	return github.NewClient(&http.Client{Transport: itr}), nil
 }
+
+func getGithubWebhookURLFromUID(serverURL, webhookUID string) string {
+	return fmt.Sprintf("%s/api/github/incoming_webhook/%s", serverURL, string(webhookUID))
+}

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

@@ -66,6 +66,22 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		return
 	}
 
+	// add a check for Github PR status
+	prClosed, err := isGithubPRClosed(client, owner, name, int(request.PullRequestID))
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("error fetching details of github PR. Error: %w", err), http.StatusConflict,
+		))
+		return
+	}
+
+	if prClosed {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
+			http.StatusConflict))
+		return
+	}
+
 	ghDeployment, err := createDeployment(client, env, request.PRBranchFrom, request.ActionID)
 
 	if err != nil {

+ 28 - 0
api/server/handlers/environment/delete.go

@@ -1,10 +1,12 @@
 package environment
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"net/http"
 
+	"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"
@@ -79,6 +81,9 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		agent.DeleteNamespace(depl.Namespace)
 	}
 
+	ghWebhookID := env.GithubWebhookID
+	webhookUID := env.WebhookID
+
 	// delete the environment
 	env, err = c.Repo().Environment().DeleteEnvironment(env)
 
@@ -87,6 +92,29 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
+	// FIXME: ignore the return status codes for now, should be fixed when we start returning all non-fatal errors
+	if ghWebhookID != 0 {
+		client.Repositories.DeleteHook(context.Background(), owner, name, ghWebhookID)
+	} else {
+		webhookURL := getGithubWebhookURLFromUID(c.Config().ServerConf.ServerURL, string(webhookUID))
+
+		// FIXME: should be cycling through all webhooks if pagination is needed
+		hooks, _, err := client.Repositories.ListHooks(context.Background(), owner, name, &github.ListOptions{})
+
+		if err == nil {
+			for _, hook := range hooks {
+				if hookURL, ok := hook.Config["url"]; ok {
+					if hookURLStr, ok := hookURL.(string); ok {
+						if hookURLStr == webhookURL {
+							client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
+							break
+						}
+					}
+				}
+			}
+		}
+	}
+
 	err = actions.DeleteEnv(&actions.EnvOpts{
 		Client:            client,
 		ServerURL:         c.Config().ServerConf.ServerURL,

+ 8 - 2
api/server/handlers/environment/enable_pull_request.go

@@ -56,14 +56,20 @@ func (c *EnablePullRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	}
 
 	// add an extra check that the installation has permission to read this pull request
-	pr, ghResp, err := client.PullRequests.Get(r.Context(), env.GitRepoOwner, env.GitRepoName, int(request.Number))
+	pr, _, err := client.PullRequests.Get(r.Context(), env.GitRepoOwner, env.GitRepoName, int(request.Number))
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	ghResp, err = client.Actions.CreateWorkflowDispatchEventByFileName(
+	if pr.GetState() == "closed" {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
+			http.StatusConflict))
+		return
+	}
+
+	ghResp, err := client.Actions.CreateWorkflowDispatchEventByFileName(
 		r.Context(), env.GitRepoOwner, env.GitRepoName, fmt.Sprintf("porter_%s_env.yml", env.Name),
 		github.CreateWorkflowDispatchEventRequest{
 			Ref: request.BranchFrom,

+ 144 - 9
api/server/handlers/environment/finalize_deployment.go

@@ -4,6 +4,8 @@ import (
 	"context"
 	"fmt"
 	"net/http"
+	"net/url"
+	"strings"
 
 	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -14,6 +16,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/repository"
 )
 
 type FinalizeDeploymentHandler struct {
@@ -104,6 +107,23 @@ func (c *FinalizeDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 		return
 	}
 
+	// add a check for the PR to be open before creating a comment
+	prClosed, err := isGithubPRClosed(client, owner, name, int(depl.PullRequestID))
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("error fetching details of github PR for deployment ID: %d. Error: %w",
+				depl.ID, err), http.StatusConflict,
+		))
+		return
+	}
+
+	if prClosed {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
+			http.StatusConflict))
+		return
+	}
+
 	workflowRun, err := commonutils.GetLatestWorkflowRun(client, depl.RepoOwner, depl.RepoName,
 		fmt.Sprintf("porter_%s_env.yml", env.Name), depl.PRBranchFrom)
 
@@ -125,9 +145,9 @@ func (c *FinalizeDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 			"| Latest SHA | [`%s`](https://github.com/%s/%s/commit/%s) |\n"+
 			"| Live URL | %s |\n"+
 			"| Build Logs | %s |\n"+
-			"| Porter Deployments URL | %s/preview-environments/details/%s?environment_id=%d |",
+			"| Porter Deployments URL | %s/preview-environments/details/%s?environment_id=%d&project_id=%d&cluster=%s |",
 		depl.CommitSHA, depl.RepoOwner, depl.RepoName, depl.CommitSHA, depl.Subdomain, workflowRun.GetHTMLURL(),
-		c.Config().ServerConf.ServerURL, depl.Namespace, depl.EnvironmentID,
+		c.Config().ServerConf.ServerURL, depl.Namespace, depl.EnvironmentID, project.ID, url.QueryEscape(cluster.Name),
 	)
 
 	if len(request.SuccessfulResources) > 0 {
@@ -146,20 +166,135 @@ 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(),
-		env.GitRepoOwner,
-		env.GitRepoName,
+		depl.RepoOwner,
+		depl.RepoName,
 		int(depl.PullRequestID),
 		&github.IssueComment{
-			Body: github.String(commentBody),
+			Body: body,
 		},
 	)
 
 	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
+}
+
+func isGithubPRClosed(
+	client *github.Client,
+	owner, name string,
+	prNumber int,
+) (bool, error) {
+	ghPR, _, err := client.PullRequests.Get(
+		context.Background(), owner, name, prNumber,
+	)
+
+	if err != nil {
+		return false, err
+	}
+
+	return ghPR.GetState() == "closed", nil
 }

+ 26 - 19
api/server/handlers/environment/finalize_deployment_with_errors.go

@@ -82,12 +82,6 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 		return
 	}
 
-	depl.Status = types.DeploymentStatusFailed
-
-	// we do not care of the error in this case because the list deployments endpoint
-	// talks to the github API to fetch the deployment status correctly
-	c.Repo().Environment().UpdateDeployment(depl)
-
 	client, err := getGithubClientFromEnvironment(c.Config(), env)
 
 	if err != nil {
@@ -97,7 +91,30 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 		return
 	}
 
-	// FIXME: ignore the status of thie API call for now
+	// add a check for the PR to be open before creating a comment
+	prClosed, err := isGithubPRClosed(client, owner, name, int(depl.PullRequestID))
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("error fetching details of github PR for deployment ID: %d. Error: %w",
+				depl.ID, err), http.StatusConflict,
+		))
+		return
+	}
+
+	if prClosed {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
+			http.StatusConflict))
+		return
+	}
+
+	depl.Status = types.DeploymentStatusFailed
+
+	// we do not care of the error in this case because the list deployments endpoint
+	// talks to the github API to fetch the deployment status correctly
+	c.Repo().Environment().UpdateDeployment(depl)
+
+	// FIXME: ignore the status of this API call for now
 	client.Repositories.CreateDeploymentStatus(
 		context.Background(), owner, name, depl.GHDeploymentID, &github.DeploymentStatusRequest{
 			State:       github.String("failure"),
@@ -145,20 +162,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)
 	}
 
-	_, _, 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 {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("error creating github comment: %w", err), http.StatusConflict,
-		))
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		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())
+}

+ 28 - 8
api/server/handlers/environment/reenable_deployment.go

@@ -51,41 +51,61 @@ func (c *ReenableDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 	}
 
 	if depl.Status != types.DeploymentStatusInactive {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("trying to re-enable deployment which is not marked \"inactive\""), http.StatusPreconditionFailed,
+		))
 		return
 	}
 
-	depl.Status = types.DeploymentStatusCreating
-
-	depl, err = c.Repo().Environment().UpdateDeployment(depl)
+	env, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, depl.EnvironmentID)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	// create the backing namespace
-	agent, err := c.GetAgent(r, cluster, "")
+	client, err := getGithubClientFromEnvironment(c.Config(), env)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	_, err = agent.CreateNamespace(depl.Namespace)
+	// add a check for the PR to be open before creating a comment
+	prClosed, err := isGithubPRClosed(client, depl.RepoOwner, depl.RepoName, int(depl.PullRequestID))
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("error fetching details of github PR for deployment ID: %d. Error: %w",
+				depl.ID, err), http.StatusConflict,
+		))
+		return
+	}
+
+	if prClosed {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
+			http.StatusConflict))
+		return
+	}
+
+	depl.Status = types.DeploymentStatusCreating
+
+	depl, err = c.Repo().Environment().UpdateDeployment(depl)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	env, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, depl.EnvironmentID)
+	// create the backing namespace
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	client, err := getGithubClientFromEnvironment(c.Config(), env)
+	_, err = agent.CreateNamespace(depl.Namespace)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 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
+		}
+	}
+}

+ 20 - 0
api/server/handlers/environment/trigger_deployment_workflow.go

@@ -52,6 +52,9 @@ func (c *TriggerDeploymentWorkflowHandler) ServeHTTP(w http.ResponseWriter, r *h
 	}
 
 	if depl.Status == types.DeploymentStatusInactive {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("trying to trigger workflow run for inactive deployment"), http.StatusConflict,
+		))
 		return
 	}
 
@@ -69,6 +72,23 @@ func (c *TriggerDeploymentWorkflowHandler) ServeHTTP(w http.ResponseWriter, r *h
 		return
 	}
 
+	// add a check for the PR to be open before creating a comment
+	prClosed, err := isGithubPRClosed(client, depl.RepoOwner, depl.RepoName, int(depl.PullRequestID))
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("error fetching details of github PR for deployment ID: %d. Error: %w",
+				depl.ID, err), http.StatusConflict,
+		))
+		return
+	}
+
+	if prClosed {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
+			http.StatusConflict))
+		return
+	}
+
 	latestWorkflowRun, err := commonutils.GetLatestWorkflowRun(client, env.GitRepoOwner, env.GitRepoName,
 		fmt.Sprintf("porter_%s_env.yml", env.Name), depl.PRBranchFrom)
 

+ 18 - 0
api/server/handlers/environment/update_deployment.go

@@ -1,6 +1,7 @@
 package environment
 
 import (
+	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz"
@@ -71,6 +72,23 @@ func (c *UpdateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		return
 	}
 
+	// add a check for the PR to be open before creating a comment
+	prClosed, err := isGithubPRClosed(client, owner, name, int(depl.PullRequestID))
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("error fetching details of github PR for deployment ID: %d. Error: %w",
+				depl.ID, err), http.StatusConflict,
+		))
+		return
+	}
+
+	if prClosed {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
+			http.StatusConflict))
+		return
+	}
+
 	ghDeployment, err := createDeployment(client, env, request.PRBranchFrom, request.ActionID)
 
 	if err != nil {

+ 27 - 0
api/server/handlers/environment/update_deployment_status.go

@@ -1,6 +1,7 @@
 package environment
 
 import (
+	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz"
@@ -63,6 +64,32 @@ func (c *UpdateDeploymentStatusHandler) ServeHTTP(w http.ResponseWriter, r *http
 		return
 	}
 
+	client, err := getGithubClientFromEnvironment(c.Config(), env)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("unable to get github client: %w", err), http.StatusConflict,
+		))
+		return
+	}
+
+	// add a check for the PR to be open before creating a comment
+	prClosed, err := isGithubPRClosed(client, depl.RepoOwner, depl.RepoName, int(depl.PullRequestID))
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("error fetching details of github PR for deployment ID: %d. Error: %w",
+				depl.ID, err), http.StatusConflict,
+		))
+		return
+	}
+
+	if prClosed {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
+			http.StatusConflict))
+		return
+	}
+
 	if depl.Status == types.DeploymentStatusInactive && request.Status != string(types.DeploymentStatusCreating) {
 		// a deployment from "inactive" state can only transition to "creating"
 		c.WriteResult(w, r, depl.ToDeploymentType())

+ 61 - 0
api/server/handlers/status/github.go

@@ -0,0 +1,61 @@
+package status
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"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/types"
+)
+
+type GetGithubStatusHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewGetGithubStatusHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *GetGithubStatusHandler {
+	return &GetGithubStatusHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *GetGithubStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	resp, err := http.Get("https://www.githubstatus.com/api/v2/incidents/unresolved.json")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error fetching github incidents: %w", err)))
+		return
+	}
+
+	defer resp.Body.Close()
+
+	data, err := io.ReadAll(resp.Body)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error reading github incidents: %w", err)))
+		return
+	}
+
+	var incidents types.GithubUnresolvedIncidents
+
+	err = json.Unmarshal(data, &incidents)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error unmarshalling json: %w", err)))
+		return
+	}
+
+	if len(incidents.Incidents) > 0 {
+		c.WriteResult(w, r, fmt.Sprintf("https://www.githubstatus.com/incidents/%s", incidents.Incidents[0].ID))
+		return
+	}
+
+	c.WriteResult(w, r, "no active incidents")
+}

+ 3 - 1
api/server/handlers/webhook/github_incoming.go

@@ -2,6 +2,7 @@ package webhook
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -18,6 +19,7 @@ import (
 	"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 GithubIncomingWebhookHandler struct {
@@ -74,7 +76,7 @@ func (c *GithubIncomingWebhookHandler) processPullRequestEvent(event *github.Pul
 
 	env, err := c.Repo().Environment().ReadEnvironmentByWebhookIDOwnerRepoName(webhookID, owner, repo)
 
-	if err != nil {
+	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
 		return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s] error reading environment: %w", webhookID, owner, repo, err)
 	}
 

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

@@ -319,6 +319,63 @@ func getClusterRoutes(
 			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
 		listDeploymentsEndpoint := factory.NewAPIEndpoint(
 			&types.APIRequestMetadata{

+ 2 - 1
api/server/router/router.go

@@ -50,8 +50,9 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 		projectOAuthRegisterer,
 		slackIntegrationRegisterer,
 	)
+	statusRegisterer := NewStatusScopedRegisterer()
 
-	userRegisterer := NewUserScopedRegisterer(projRegisterer)
+	userRegisterer := NewUserScopedRegisterer(projRegisterer, statusRegisterer)
 	panicMW := middleware.NewPanicMiddleware(config)
 
 	if config.ServerConf.PprofEnabled {

+ 83 - 0
api/server/router/status.go

@@ -0,0 +1,83 @@
+package router
+
+import (
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/status"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func NewStatusScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
+		GetRoutes: GetStatusScopedRoutes,
+		Children:  children,
+	}
+}
+
+func GetStatusScopedRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*router.Registerer,
+) []*router.Route {
+	routes, projPath := getStatusRoutes(r, config, basePath, factory)
+
+	if len(children) > 0 {
+		r.Route(projPath.RelativePath, func(r chi.Router) {
+			for _, child := range children {
+				childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+				routes = append(routes, childRoutes...)
+			}
+		})
+	}
+
+	return routes
+}
+
+func getStatusRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*router.Route, *types.Path) {
+	relPath := "/status"
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+
+	routes := make([]*router.Route, 0)
+
+	// GET /api/status/github -> status.NewGetGithubStatusHandler
+	getGithubStatusEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/github",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+			},
+		},
+	)
+
+	getGithubStatusHandler := status.NewGetGithubStatusHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getGithubStatusEndpoint,
+		Handler:  getGithubStatusHandler,
+		Router:   r,
+	})
+
+	return routes, newPath
+}

+ 5 - 0
api/types/environment.go

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

+ 6 - 0
api/types/status.go

@@ -7,3 +7,9 @@ const (
 type StreamStatusRequest struct {
 	Selectors string `schema:"selectors"`
 }
+
+type GithubUnresolvedIncidents struct {
+	Incidents []*struct {
+		ID string `json:"id"`
+	} `json:"incidents"`
+}

+ 14 - 5
cli/cmd/apply.go

@@ -865,10 +865,15 @@ func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
 	}
 
 	for _, res := range t.resourceGroup.Resources {
-		req.SuccessfulResources = append(req.SuccessfulResources, &types.SuccessfullyDeployedResource{
-			ReleaseName: getReleaseName(res),
-			ReleaseType: getReleaseType(res),
-		})
+		releaseType := getReleaseType(res)
+		releaseName := getReleaseName(res)
+
+		if releaseType != "" && releaseName != "" {
+			req.SuccessfulResources = append(req.SuccessfulResources, &types.SuccessfullyDeployedResource{
+				ReleaseName: releaseName,
+				ReleaseType: releaseType,
+			})
+		}
 	}
 
 	// finalize the deployment
@@ -1056,5 +1061,9 @@ func getReleaseType(res *switchboardTypes.Resource) string {
 	// GetSource has alrealy been called and validated previously
 	source, _ := preview.GetSource(res.Source)
 
-	return source.Name
+	if source != nil && source.Name != "" {
+		return source.Name
+	}
+
+	return ""
 }

+ 80 - 10
dashboard/src/main/home/cluster-dashboard/preview-environments/components/PreviewEnvironmentsHeader.tsx

@@ -1,14 +1,84 @@
-import React from "react";
+import React, { useEffect, useState } from "react";
 import styled from "styled-components";
 import DashboardHeader from "../../DashboardHeader";
 import PullRequestIcon from "assets/pull_request_icon.svg";
+import api from "shared/api";
 
-export const PreviewEnvironmentsHeader = () => (
-  <>
-    <DashboardHeader
-      image={PullRequestIcon}
-      title="Preview Environments"
-      description="Create full-stack preview environments for your pull requests."
-    />
-  </>
-);
+export const PreviewEnvironmentsHeader = () => {
+  const [githubStatus, setGithubStatus] = useState<string>(
+    "no active incidents"
+  );
+
+  useEffect(() => {
+    api
+      .getGithubStatus("<token>", {}, {})
+      .then(({ data }) => {
+        setGithubStatus(data);
+      })
+      .catch((err) => {
+        console.error(err);
+      });
+  }, []);
+
+  return (
+    <>
+      <DashboardHeader
+        image={PullRequestIcon}
+        title="Preview Environments"
+        description="Create full-stack preview environments for your pull requests."
+      />
+      {githubStatus != "no active incidents" ? (
+        <AlertCard>
+          <AlertCardIcon className="material-icons">error</AlertCardIcon>
+          <AlertCardContent className="content">
+            <AlertCardTitle className="title">
+              Github has an ongoing incident
+            </AlertCardTitle>
+            Active incident:{" "}
+            <a href={`${githubStatus}`} target="_blank">
+              {githubStatus}
+            </a>
+          </AlertCardContent>
+        </AlertCard>
+      ) : null}
+    </>
+  );
+};
+
+const AlertCard = styled.div`
+  transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+  border-radius: 4px;
+  box-shadow: none;
+  font-weight: 400;
+  font-size: 0.875rem;
+  line-height: 1.43;
+  letter-spacing: 0.01071em;
+  border: 1px solid rgb(229, 115, 115);
+  display: flex;
+  padding: 6px 16px;
+  color: rgb(244, 199, 199);
+  margin-top: 20px;
+  position: relative;
+  margin-bottom: 20px;
+`;
+
+const AlertCardIcon = styled.span`
+  color: rgb(239, 83, 80);
+  margin-right: 12px;
+  padding: 7px 0px;
+  display: flex;
+  font-size: 22px;
+  opacity: 0.9;
+`;
+
+const AlertCardTitle = styled.div`
+  margin: -2px 0px 0.35em;
+  font-size: 1rem;
+  line-height: 1.5;
+  letter-spacing: 0.00938em;
+  font-weight: 500;
+`;
+
+const AlertCardContent = styled.div`
+  padding: 8px 0px;
+`;

+ 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 { PreviewEnvironmentsHeader } from "../components/PreviewEnvironmentsHeader";
 import SearchBar from "components/SearchBar";
+import CheckboxRow from "components/form-components/CheckboxRow";
+import DocsHelper from "components/DocsHelper";
 
 const AvailableStatusFilters = [
   "all",
@@ -34,6 +36,7 @@ const DeploymentList = () => {
   const [deploymentList, setDeploymentList] = useState<PRDeployment[]>([]);
   const [pullRequests, setPullRequests] = useState<PullRequest[]>([]);
   const [searchValue, setSearchValue] = useState("");
+  const [newCommentsDisabled, setNewCommentsDisabled] = useState(false);
 
   const [
     statusSelectorVal,
@@ -66,6 +69,18 @@ const DeploymentList = () => {
     // return mockRequest();
   };
 
+  const getEnvironment = () => {
+    return api.getEnvironment(
+      "<token>",
+      {},
+      {
+        project_id: currentProject.id,
+        cluster_id: currentCluster.id,
+        environment_id: Number(environment_id),
+      }
+    );
+  };
+
   useEffect(() => {
     const status_filter = getQueryParam("status_filter");
 
@@ -93,7 +108,6 @@ const DeploymentList = () => {
 
         setDeploymentList(data.deployments || []);
         setPullRequests(data.pull_requests || []);
-        setIsLoading(false);
       })
       .catch((err) => {
         console.error(err);
@@ -101,6 +115,21 @@ const DeploymentList = () => {
           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 () => {
       isSubscribed = false;
@@ -116,9 +145,15 @@ const DeploymentList = () => {
     } catch (error) {
       setHasError(true);
       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) => {
@@ -232,6 +267,24 @@ const DeploymentList = () => {
     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 (
     <>
       <PreviewEnvironmentsHeader />
@@ -282,6 +335,24 @@ const DeploymentList = () => {
           </StyledStatusSelector>
         </ActionsWrapper>
       </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>
         <EventsGrid>{renderDeploymentList()}</EventsGrid>
       </Container>
@@ -307,6 +378,15 @@ const Flex = styled.div`
   align-items: center;
 `;
 
+const Div = styled.div`
+  margin-bottom: -7px;
+`;
+
+const FlexWrap = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
 const BackButton = styled(DynamicLink)`
   cursor: pointer;
   font-size: 24px;

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

@@ -201,6 +201,32 @@ const listEnvironments = baseApi<
   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<
   {
     gcp_key_data: string;
@@ -2048,6 +2074,11 @@ const updateStackSourceConfig = baseApi<
     `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}/source`
 );
 
+const getGithubStatus = baseApi<{}, {}>(
+  "GET",
+  ({}) => `/api/status/github`
+);
+
 // Bundle export to allow default api import (api.<method> is more readable)
 export default {
   checkAuth,
@@ -2068,6 +2099,8 @@ export default {
   createPreviewEnvironmentDeployment,
   reenablePreviewEnvironmentDeployment,
   listEnvironments,
+  getEnvironment,
+  toggleNewCommentForEnvironment,
   createGCPIntegration,
   createInvite,
   createNamespace,
@@ -2240,4 +2273,7 @@ export default {
   rollbackStack,
   deleteStack,
   updateStackSourceConfig,
+
+  // STATUS
+  getGithubStatus,
 };

+ 95 - 69
go.mod

@@ -6,15 +6,15 @@ require (
 	cloud.google.com/go v0.99.0
 	github.com/AlecAivazis/survey/v2 v2.2.9
 	github.com/Masterminds/semver/v3 v3.1.1
-	github.com/aws/aws-sdk-go v1.35.4
+	github.com/aws/aws-sdk-go v1.43.28
 	github.com/bradleyfalzon/ghinstallation/v2 v2.0.3
-	github.com/buildpacks/pack v0.26.0
+	github.com/buildpacks/pack v0.27.0
 	github.com/cli/cli v1.11.0
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/digitalocean/godo v1.75.0
-	github.com/docker/cli v20.10.14+incompatible
+	github.com/docker/cli v20.10.17+incompatible
 	github.com/docker/distribution v2.8.1+incompatible
-	github.com/docker/docker v20.10.14+incompatible
+	github.com/docker/docker v20.10.17+incompatible
 	github.com/docker/docker-credential-helpers v0.6.4
 	github.com/docker/go-connections v0.4.0
 	github.com/fatih/color v1.13.0
@@ -23,7 +23,7 @@ require (
 	github.com/go-playground/validator/v10 v10.3.0
 	github.com/go-redis/redis/v8 v8.11.0
 	github.com/go-test/deep v1.0.7
-	github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
+	github.com/golang-jwt/jwt/v4 v4.4.1 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/go-github/v39 v39.2.0 // indirect
 	github.com/google/go-github/v41 v41.0.0
@@ -37,32 +37,32 @@ require (
 	github.com/mitchellh/mapstructure v1.4.3
 	github.com/moby/moby v20.10.6+incompatible
 	github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
-	github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799
+	github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198
 	github.com/pkg/errors v0.9.1
 	github.com/porter-dev/switchboard v0.0.0-20220628112428-7665a0121e4f
 	github.com/rs/zerolog v1.26.0
 	github.com/sendgrid/sendgrid-go v3.8.0+incompatible
-	github.com/spf13/cobra v1.4.0
+	github.com/spf13/cobra v1.5.0
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.10.0
-	github.com/stretchr/testify v1.7.1
-	golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
-	golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2
-	golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
+	github.com/stretchr/testify v1.8.0
+	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
+	golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
+	golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26
 	google.golang.org/api v0.62.0
-	google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731
-	google.golang.org/grpc v1.46.0
+	google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03
+	google.golang.org/grpc v1.47.0
 	google.golang.org/protobuf v1.28.0
 	gorm.io/driver/sqlite v1.1.3
 	gorm.io/gorm v1.22.3
-	helm.sh/helm/v3 v3.8.0
-	k8s.io/api v0.23.1
-	k8s.io/apimachinery v0.23.5
-	k8s.io/cli-runtime v0.23.1
-	k8s.io/client-go v0.23.1
+	helm.sh/helm/v3 v3.9.0
+	k8s.io/api v0.24.2
+	k8s.io/apimachinery v0.24.2
+	k8s.io/cli-runtime v0.24.2
+	k8s.io/client-go v0.24.2
 	k8s.io/helm v2.17.0+incompatible
-	k8s.io/kubectl v0.23.1
-	sigs.k8s.io/aws-iam-authenticator v0.5.2
+	k8s.io/kubectl v0.24.1
+	sigs.k8s.io/aws-iam-authenticator v0.5.8
 	sigs.k8s.io/yaml v1.3.0
 )
 
@@ -74,16 +74,42 @@ require (
 )
 
 require (
-	github.com/Azure/azure-sdk-for-go v63.4.0+incompatible // indirect
+	github.com/Azure/azure-sdk-for-go v65.0.0+incompatible // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v0.23.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.5.0 // indirect
+	github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
+	github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
 	github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect
+	github.com/PuerkitoBio/goquery v1.5.1 // indirect
+	github.com/andybalholm/cascadia v1.1.0 // indirect
+	github.com/aws/aws-sdk-go-v2 v1.16.4 // indirect
+	github.com/aws/aws-sdk-go-v2/config v1.15.9 // indirect
+	github.com/aws/aws-sdk-go-v2/credentials v1.12.4 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.5 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.11 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.5 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.3.12 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ecr v1.17.5 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.13.5 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.5 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.11.7 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.16.6 // indirect
+	github.com/aws/smithy-go v1.11.2 // indirect
+	github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04 // indirect
+	github.com/chrismellard/docker-credential-acr-env v0.0.0-20220327082430-c57b701bfc08 // indirect
 	github.com/cosmtrek/air v1.30.0 // indirect
+	github.com/dimchansky/utfbom v1.1.1 // indirect
+	github.com/emicklei/go-restful/v3 v3.8.0 // indirect
+	github.com/go-gorp/gorp/v3 v3.0.2 // indirect
 	github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
+	github.com/google/gnostic v0.6.9 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
+	github.com/mmcdole/gofeed v1.1.3 // indirect
+	github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect
 	github.com/xanzy/go-gitlab v0.68.0 // indirect
 )
@@ -92,8 +118,8 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.14.0
 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
-	github.com/Azure/go-autorest/autorest v0.11.20 // indirect
-	github.com/Azure/go-autorest/autorest/adal v0.9.15 // indirect
+	github.com/Azure/go-autorest/autorest v0.11.27 // indirect
+	github.com/Azure/go-autorest/autorest/adal v0.9.20 // indirect
 	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
 	github.com/Azure/go-autorest/logger v0.2.1 // indirect
 	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
@@ -102,16 +128,16 @@ require (
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver v1.5.0 // indirect
 	github.com/Masterminds/sprig/v3 v3.2.2 // indirect
-	github.com/Masterminds/squirrel v1.5.2 // indirect
+	github.com/Masterminds/squirrel v1.5.3 // indirect
 	github.com/Microsoft/go-winio v0.5.2 // indirect
-	github.com/Microsoft/hcsshim v0.9.2 // indirect
+	github.com/Microsoft/hcsshim v0.9.3 // indirect
 	github.com/PuerkitoBio/purell v1.1.1 // indirect
 	github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
 	github.com/apex/log v1.9.0 // indirect
-	github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
+	github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/buildpacks/imgutil v0.0.0-20220425182719-2edb52457eb0 // indirect
-	github.com/buildpacks/lifecycle v0.14.0 // indirect
+	github.com/buildpacks/imgutil v0.0.0-20220527150729-7a271a852e31 // indirect
+	github.com/buildpacks/lifecycle v0.14.1 // indirect
 	github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 	github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
@@ -119,7 +145,7 @@ require (
 	github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
 	github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
 	github.com/containerd/cgroups v1.0.3 // indirect
-	github.com/containerd/containerd v1.6.3 // indirect
+	github.com/containerd/containerd v1.6.6 // indirect
 	github.com/containerd/stargz-snapshotter/estargz v0.11.4 // indirect
 	github.com/cyphar/filepath-securejoin v0.2.3 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
@@ -130,27 +156,27 @@ require (
 	github.com/emirpasic/gods v1.18.1 // indirect
 	github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect
 	github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
-	github.com/evanphx/json-patch v4.12.0+incompatible // indirect
-	github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
+	github.com/evanphx/json-patch v5.6.0+incompatible // indirect
+	github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/fvbommel/sortorder v1.0.1 // indirect
 	github.com/gdamore/encoding v1.0.0 // indirect
 	github.com/gdamore/tcell/v2 v2.5.1 // indirect
 	github.com/ghodss/yaml v1.0.0 // indirect
-	github.com/go-errors/errors v1.0.1 // indirect
-	github.com/go-logr/logr v1.2.2 // indirect
+	github.com/go-errors/errors v1.4.2 // indirect
+	github.com/go-logr/logr v1.2.3 // indirect
 	github.com/go-openapi/jsonpointer v0.19.5 // indirect
-	github.com/go-openapi/jsonreference v0.19.5 // indirect
-	github.com/go-openapi/swag v0.19.14 // indirect
+	github.com/go-openapi/jsonreference v0.20.0 // indirect
+	github.com/go-openapi/swag v0.21.1 // indirect
 	github.com/go-playground/locales v0.13.0 // indirect
 	github.com/go-playground/universal-translator v0.17.0 // indirect
 	github.com/gobwas/glob v0.2.3 // indirect
 	github.com/gofrs/flock v0.8.1 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
-	github.com/google/btree v1.0.1 // indirect
+	github.com/google/btree v1.1.2 // indirect
 	github.com/google/go-cmp v0.5.8 // indirect
-	github.com/google/go-containerregistry v0.8.0 // indirect
+	github.com/google/go-containerregistry v0.9.0 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
@@ -159,7 +185,7 @@ require (
 	github.com/googleapis/gnostic v0.5.5 // indirect
 	github.com/gorilla/mux v1.8.0 // indirect
 	github.com/gosuri/uitable v0.0.4 // indirect
-	github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
+	github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/heroku/color v0.0.6 // indirect
 	github.com/huandu/xstrings v1.3.2 // indirect
@@ -179,21 +205,21 @@ require (
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.2 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
-	github.com/jmoiron/sqlx v1.3.4 // indirect
+	github.com/jmoiron/sqlx v1.3.5 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
 	github.com/kevinburke/ssh_config v1.2.0 // indirect
-	github.com/klauspost/compress v1.15.2 // indirect
+	github.com/klauspost/compress v1.15.7 // indirect
 	github.com/kris-nova/lolgopher v0.0.0-20180921204813-313b3abb0d9b // indirect
 	github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
 	github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
 	github.com/leodido/go-urn v1.2.0 // indirect
-	github.com/lib/pq v1.10.4 // indirect
+	github.com/lib/pq v1.10.6 // indirect
 	github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/magiconair/properties v1.8.5 // indirect
-	github.com/mailru/easyjson v0.7.6 // indirect
+	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-colorable v0.1.12 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
@@ -202,7 +228,7 @@ require (
 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
-	github.com/mitchellh/go-wordwrap v1.0.0 // indirect
+	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
 	github.com/mitchellh/ioprogress v0.0.0-20180201004757-6a23b12fa88e // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/moby/buildkit v0.10.3
@@ -216,45 +242,45 @@ require (
 	github.com/morikuni/aec v1.0.0 // indirect
 	github.com/onsi/ginkgo v1.16.4 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
-	github.com/opencontainers/runc v1.1.1 // indirect
+	github.com/opencontainers/runc v1.1.2 // indirect
 	github.com/opencontainers/selinux v1.10.1 // indirect
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/prometheus/client_golang v1.12.1 // indirect
+	github.com/prometheus/client_golang v1.12.2 // indirect
 	github.com/prometheus/client_model v0.2.0 // indirect
-	github.com/prometheus/common v0.32.1 // indirect
+	github.com/prometheus/common v0.35.0 // indirect
 	github.com/prometheus/procfs v0.7.3 // indirect
 	github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
-	github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc // indirect
-	github.com/russross/blackfriday v1.5.2 // indirect
+	github.com/rubenv/sql-migrate v1.1.2 // indirect
+	github.com/russross/blackfriday v1.6.0 // indirect
 	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
 	github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 // indirect
 	github.com/sendgrid/rest v2.6.3+incompatible // indirect
 	github.com/sergi/go-diff v1.2.0 // indirect
-	github.com/shopspring/decimal v1.2.0 // indirect
+	github.com/shopspring/decimal v1.3.1 // indirect
 	github.com/sirupsen/logrus v1.8.1 // indirect
 	github.com/spf13/afero v1.6.0 // indirect
-	github.com/spf13/cast v1.4.1 // indirect
+	github.com/spf13/cast v1.5.0 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/src-d/gcfg v1.4.0 // indirect
 	github.com/subosito/gotenv v1.2.0 // indirect
 	github.com/vbatts/tar-split v0.11.2 // indirect
 	github.com/xanzy/ssh-agent v0.3.1 // indirect
-	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
+	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
-	github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
+	github.com/xlab/treeprint v1.1.0 // indirect
 	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
 	go.opencensus.io v0.23.0 // indirect
-	go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
-	golang.org/x/mod v0.5.1 // indirect
-	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
-	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
-	golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
+	go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd // indirect
+	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
+	golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
+	golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect
+	golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
 	golang.org/x/text v0.3.7 // indirect
-	golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
+	golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	gopkg.in/gorp.v1 v1.7.2 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
@@ -262,16 +288,16 @@ require (
 	gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect
 	gopkg.in/src-d/go-git.v4 v4.13.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
-	gopkg.in/yaml.v3 v3.0.0 // indirect
-	k8s.io/apiextensions-apiserver v0.23.1 // indirect
-	k8s.io/apiserver v0.23.1 // indirect
-	k8s.io/component-base v0.23.1 // indirect
-	k8s.io/klog/v2 v2.30.0 // indirect
-	k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
-	k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect
-	oras.land/oras-go v1.1.0 // indirect
-	sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
-	sigs.k8s.io/kustomize/api v0.10.1 // indirect
-	sigs.k8s.io/kustomize/kyaml v0.13.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+	k8s.io/apiextensions-apiserver v0.24.2 // indirect
+	k8s.io/apiserver v0.24.2 // indirect
+	k8s.io/component-base v0.24.2 // indirect
+	k8s.io/klog/v2 v2.70.0 // indirect
+	k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect
+	k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
+	oras.land/oras-go v1.2.0 // indirect
+	sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124 // indirect
+	sigs.k8s.io/kustomize/api v0.11.5 // indirect
+	sigs.k8s.io/kustomize/kyaml v0.13.7 // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
 )

File diff suppressed because it is too large
+ 304 - 0
go.sum


+ 7 - 0
internal/models/environment.go

@@ -19,9 +19,13 @@ type Environment struct {
 	Name string
 	Mode string
 
+	NewCommentsDisabled bool
+
 	// WebhookID uniquely identifies the environment when other fields (project, cluster)
 	// aren't present
 	WebhookID string `gorm:"unique"`
+
+	GithubWebhookID int64
 }
 
 func (e *Environment) ToEnvironmentType() *types.Environment {
@@ -33,6 +37,8 @@ func (e *Environment) ToEnvironmentType() *types.Environment {
 		GitRepoOwner:      e.GitRepoOwner,
 		GitRepoName:       e.GitRepoName,
 
+		NewCommentsDisabled: e.NewCommentsDisabled,
+
 		Name: e.Name,
 		Mode: e.Mode,
 	}
@@ -47,6 +53,7 @@ type Deployment struct {
 	Subdomain      string
 	PullRequestID  uint
 	GHDeploymentID int64
+	GHPRCommentID  int64
 	PRName         string
 	RepoName       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)
 	ReadEnvironmentByWebhookIDOwnerRepoName(webhookID, owner, repo string) (*models.Environment, error)
 	ListEnvironments(projectID, clusterID uint) ([]*models.Environment, error)
+	UpdateEnvironment(environment *models.Environment) (*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)

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

@@ -119,6 +119,14 @@ func (repo *EnvironmentRepository) ListEnvironments(projectID, clusterID uint) (
 	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) {
 	if err := repo.db.Delete(&env).Error; err != nil {
 		return nil, err

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

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

Some files were not shown because too many files changed in this diff