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

Merge branch 'nico/por-515-allow-user-to-change-the-branch-of-a' of github.com:porter-dev/porter into dev

jnfrati 4 лет назад
Родитель
Сommit
6cd90a5fd9

+ 2 - 2
api/server/handlers/environment/create.go

@@ -174,11 +174,11 @@ func getGithubClientFromEnvironment(config *config.Config, env *models.Environme
 	}
 	}
 
 
 	// authenticate as github app installation
 	// authenticate as github app installation
-	itr, err := ghinstallation.NewKeyFromFile(
+	itr, err := ghinstallation.New(
 		http.DefaultTransport,
 		http.DefaultTransport,
 		int64(ghAppId),
 		int64(ghAppId),
 		int64(env.GitInstallationID),
 		int64(env.GitInstallationID),
-		config.ServerConf.GithubAppSecretPath,
+		config.ServerConf.GithubAppSecret,
 	)
 	)
 
 
 	if err != nil {
 	if err != nil {

+ 50 - 34
api/server/handlers/environment/list_deployments_by_cluster.go

@@ -71,6 +71,8 @@ func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *ht
 			deployments = append(deployments, deployment)
 			deployments = append(deployments, deployment)
 		}
 		}
 
 
+		envToGithubClientMap := make(map[uint]*github.Client)
+
 		var wg sync.WaitGroup
 		var wg sync.WaitGroup
 		wg.Add(len(deployments))
 		wg.Add(len(deployments))
 
 
@@ -82,10 +84,21 @@ func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *ht
 				return
 				return
 			}
 			}
 
 
+			if _, ok := envToGithubClientMap[env.ID]; !ok {
+				client, err := getGithubClientFromEnvironment(c.Config(), env)
+
+				if err != nil {
+					c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+					return
+				}
+
+				envToGithubClientMap[env.ID] = client
+			}
+
 			go func(depl *types.Deployment) {
 			go func(depl *types.Deployment) {
 				defer wg.Done()
 				defer wg.Done()
 
 
-				updateDeploymentWithGithubWorkflowRunStatus(c.Config(), env, depl)
+				updateDeploymentWithGithubWorkflowRunStatus(c.Config(), envToGithubClientMap[env.ID], env, depl)
 			}(deployment)
 			}(deployment)
 		}
 		}
 
 
@@ -99,7 +112,18 @@ func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *ht
 		}
 		}
 
 
 		for _, env := range envList {
 		for _, env := range envList {
-			prs, err := fetchOpenPullRequests(r.Context(), c.Config(), env, deplInfoMap)
+			if _, ok := envToGithubClientMap[env.ID]; !ok {
+				client, err := getGithubClientFromEnvironment(c.Config(), env)
+
+				if err != nil {
+					c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+					return
+				}
+
+				envToGithubClientMap[env.ID] = client
+			}
+
+			prs, err := fetchOpenPullRequests(r.Context(), c.Config(), envToGithubClientMap[env.ID], env, deplInfoMap)
 
 
 			if err != nil {
 			if err != nil {
 				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
@@ -125,6 +149,13 @@ func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *ht
 
 
 		deplInfoMap := make(map[string]bool)
 		deplInfoMap := make(map[string]bool)
 
 
+		client, err := getGithubClientFromEnvironment(c.Config(), env)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
 		for _, depl := range depls {
 		for _, depl := range depls {
 			deployment := depl.ToDeploymentType()
 			deployment := depl.ToDeploymentType()
 			deplInfoMap[fmt.Sprintf(
 			deplInfoMap[fmt.Sprintf(
@@ -140,23 +171,16 @@ func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *ht
 		wg.Add(len(deployments))
 		wg.Add(len(deployments))
 
 
 		for _, deployment := range deployments {
 		for _, deployment := range deployments {
-			env, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, deployment.EnvironmentID)
-
-			if err != nil {
-				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-				return
-			}
-
 			go func(depl *types.Deployment) {
 			go func(depl *types.Deployment) {
 				defer wg.Done()
 				defer wg.Done()
 
 
-				updateDeploymentWithGithubWorkflowRunStatus(c.Config(), env, depl)
+				updateDeploymentWithGithubWorkflowRunStatus(c.Config(), client, env, depl)
 			}(deployment)
 			}(deployment)
 		}
 		}
 
 
 		wg.Wait()
 		wg.Wait()
 
 
-		prs, err := fetchOpenPullRequests(r.Context(), c.Config(), env, deplInfoMap)
+		prs, err := fetchOpenPullRequests(r.Context(), c.Config(), client, env, deplInfoMap)
 
 
 		if err != nil {
 		if err != nil {
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
@@ -174,6 +198,7 @@ func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *ht
 
 
 func updateDeploymentWithGithubWorkflowRunStatus(
 func updateDeploymentWithGithubWorkflowRunStatus(
 	config *config.Config,
 	config *config.Config,
+	client *github.Client,
 	env *models.Environment,
 	env *models.Environment,
 	deployment *types.Deployment,
 	deployment *types.Deployment,
 ) {
 ) {
@@ -181,25 +206,21 @@ func updateDeploymentWithGithubWorkflowRunStatus(
 		return
 		return
 	}
 	}
 
 
-	client, err := getGithubClientFromEnvironment(config, env)
+	latestWorkflowRun, err := commonutils.GetLatestWorkflowRun(client, env.GitRepoOwner, env.GitRepoName,
+		fmt.Sprintf("porter_%s_env.yml", env.Name), deployment.PRBranchFrom)
 
 
 	if err == nil {
 	if err == nil {
-		latestWorkflowRun, err := commonutils.GetLatestWorkflowRun(client, env.GitRepoOwner, env.GitRepoName,
-			fmt.Sprintf("porter_%s_env.yml", env.Name), deployment.PRBranchFrom)
-
-		if err == nil {
-			deployment.LastWorkflowRunURL = latestWorkflowRun.GetHTMLURL()
-
-			if (latestWorkflowRun.GetStatus() == "in_progress" ||
-				latestWorkflowRun.GetStatus() == "queued") &&
-				deployment.Status != types.DeploymentStatusCreating {
-				deployment.Status = types.DeploymentStatusUpdating
-			} else if latestWorkflowRun.GetStatus() == "completed" {
-				if latestWorkflowRun.GetConclusion() == "failure" {
-					deployment.Status = types.DeploymentStatusFailed
-				} else if latestWorkflowRun.GetConclusion() == "timed_out" {
-					deployment.Status = types.DeploymentStatusTimedOut
-				}
+		deployment.LastWorkflowRunURL = latestWorkflowRun.GetHTMLURL()
+
+		if (latestWorkflowRun.GetStatus() == "in_progress" ||
+			latestWorkflowRun.GetStatus() == "queued") &&
+			deployment.Status != types.DeploymentStatusCreating {
+			deployment.Status = types.DeploymentStatusUpdating
+		} else if latestWorkflowRun.GetStatus() == "completed" {
+			if latestWorkflowRun.GetConclusion() == "failure" {
+				deployment.Status = types.DeploymentStatusFailed
+			} else if latestWorkflowRun.GetConclusion() == "timed_out" {
+				deployment.Status = types.DeploymentStatusTimedOut
 			}
 			}
 		}
 		}
 	}
 	}
@@ -208,15 +229,10 @@ func updateDeploymentWithGithubWorkflowRunStatus(
 func fetchOpenPullRequests(
 func fetchOpenPullRequests(
 	ctx context.Context,
 	ctx context.Context,
 	config *config.Config,
 	config *config.Config,
+	client *github.Client,
 	env *models.Environment,
 	env *models.Environment,
 	deplInfoMap map[string]bool,
 	deplInfoMap map[string]bool,
 ) ([]*types.PullRequest, error) {
 ) ([]*types.PullRequest, error) {
-	client, err := getGithubClientFromEnvironment(config, env)
-
-	if err != nil {
-		return nil, err
-	}
-
 	openPRs, resp, err := client.PullRequests.List(ctx, env.GitRepoOwner, env.GitRepoName,
 	openPRs, resp, err := client.PullRequests.List(ctx, env.GitRepoOwner, env.GitRepoName,
 		&github.PullRequestListOptions{
 		&github.PullRequestListOptions{
 			ListOptions: github.ListOptions{
 			ListOptions: github.ListOptions{

+ 71 - 0
api/server/handlers/release/update_action_config.go

@@ -0,0 +1,71 @@
+package release
+
+import (
+	"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 UpdateActionConfigHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewUpdateActionConfigHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *UpdateActionConfigHandler {
+	return &UpdateActionConfigHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *UpdateActionConfigHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+	name, _ := requestutils.GetURLParamString(r, types.URLParamReleaseName)
+	namespace := r.Context().Value(types.NamespaceScope).(string)
+
+	request := &types.UpdateActionConfigRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	release, err := c.Repo().Release().ReadRelease(cluster.ID, name, namespace)
+
+	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			w.WriteHeader(http.StatusNotFound)
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+	}
+
+	actionConfig, err := c.Repo().GitActionConfig().ReadGitActionConfig(release.ID)
+
+	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			w.WriteHeader(http.StatusNotFound)
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+	}
+
+	actionConfig.GitBranch = request.GitActionConfig.GitBranch
+
+	if err := c.Repo().GitActionConfig().UpdateGitActionConfig(actionConfig); err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+}

+ 19 - 17
api/server/handlers/stack/delete.go

@@ -32,29 +32,31 @@ func (p *StackDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 	namespace, _ := r.Context().Value(types.NamespaceScope).(string)
 	namespace, _ := r.Context().Value(types.NamespaceScope).(string)
 
 
-	revision, err := p.Repo().Stack().ReadStackRevisionByNumber(stack.ID, stack.Revisions[0].ID)
+	if len(stack.Revisions) > 0 {
+		revision, err := p.Repo().Stack().ReadStackRevisionByNumber(stack.ID, stack.Revisions[0].ID)
 
 
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
+		if err != nil {
+			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
 
 
-	helmAgent, err := p.GetHelmAgent(r, cluster, namespace)
+		helmAgent, err := p.GetHelmAgent(r, cluster, namespace)
 
 
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
+		if err != nil {
+			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
 
 
-	// delete all resources in stack
-	for _, appResource := range revision.Resources {
-		deleteAppResource(&deleteAppResourceOpts{
-			helmAgent: helmAgent,
-			name:      appResource.Name,
-		})
+		// delete all resources in stack
+		for _, appResource := range revision.Resources {
+			deleteAppResource(&deleteAppResourceOpts{
+				helmAgent: helmAgent,
+				name:      appResource.Name,
+			})
+		}
 	}
 	}
 
 
-	stack, err = p.Repo().Stack().DeleteStack(stack)
+	stack, err := p.Repo().Stack().DeleteStack(stack)
 
 
 	if err != nil {
 	if err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 20 - 12
api/server/handlers/webhook/github_incoming.go

@@ -37,14 +37,15 @@ func NewGithubIncomingWebhookHandler(
 
 
 func (c *GithubIncomingWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 func (c *GithubIncomingWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	payload, err := github.ValidatePayload(r, []byte(c.Config().ServerConf.GithubIncomingWebhookSecret))
 	payload, err := github.ValidatePayload(r, []byte(c.Config().ServerConf.GithubIncomingWebhookSecret))
+
 	if err != nil {
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error validating webhook payload: %w", err)))
 		return
 		return
 	}
 	}
 
 
 	event, err := github.ParseWebHook(github.WebHookType(r), payload)
 	event, err := github.ParseWebHook(github.WebHookType(r), payload)
 	if err != nil {
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error parsing webhook: %w", err)))
 		return
 		return
 	}
 	}
 
 
@@ -53,7 +54,7 @@ func (c *GithubIncomingWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.
 		err = c.processPullRequestEvent(event, r)
 		err = c.processPullRequestEvent(event, r)
 
 
 		if err != nil {
 		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error processing pull request webhook event: %w", err)))
 			return
 			return
 		}
 		}
 	}
 	}
@@ -73,14 +74,15 @@ func (c *GithubIncomingWebhookHandler) processPullRequestEvent(event *github.Pul
 	env, err := c.Repo().Environment().ReadEnvironmentByWebhookIDOwnerRepoName(webhookID, owner, repo)
 	env, err := c.Repo().Environment().ReadEnvironmentByWebhookIDOwnerRepoName(webhookID, owner, repo)
 
 
 	if err != nil {
 	if err != nil {
-		return err
+		return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s] error reading environment: %w", webhookID, owner, repo, err)
 	}
 	}
 
 
 	// create deployment on GitHub API
 	// create deployment on GitHub API
 	client, err := getGithubClientFromEnvironment(c.Config(), env)
 	client, err := getGithubClientFromEnvironment(c.Config(), env)
 
 
 	if err != nil {
 	if err != nil {
-		return err
+		return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d] error getting github client: %w",
+			webhookID, owner, repo, env.ID, err)
 	}
 	}
 
 
 	if env.Mode == "auto" && event.GetAction() == "opened" {
 	if env.Mode == "auto" && event.GetAction() == "opened" {
@@ -98,7 +100,8 @@ func (c *GithubIncomingWebhookHandler) processPullRequestEvent(event *github.Pul
 		)
 		)
 
 
 		if err != nil {
 		if err != nil {
-			return err
+			return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d] error creating workflow dispatch event: %w",
+				webhookID, owner, repo, env.ID, err)
 		}
 		}
 	} else if event.GetAction() == "synchronize" || event.GetAction() == "closed" {
 	} else if event.GetAction() == "synchronize" || event.GetAction() == "closed" {
 		depl, err := c.Repo().Environment().ReadDeploymentByGitDetails(
 		depl, err := c.Repo().Environment().ReadDeploymentByGitDetails(
@@ -106,7 +109,8 @@ func (c *GithubIncomingWebhookHandler) processPullRequestEvent(event *github.Pul
 		)
 		)
 
 
 		if err != nil {
 		if err != nil {
-			return err
+			return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d] error reading deployment: %w",
+				webhookID, owner, repo, env.ID, err)
 		}
 		}
 
 
 		if depl.Status != types.DeploymentStatusInactive {
 		if depl.Status != types.DeploymentStatusInactive {
@@ -125,13 +129,15 @@ func (c *GithubIncomingWebhookHandler) processPullRequestEvent(event *github.Pul
 				)
 				)
 
 
 				if err != nil {
 				if err != nil {
-					return err
+					return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, deploymentID: %d] error creating workflow dispatch event: %w",
+						webhookID, owner, repo, env.ID, depl.ID, err)
 				}
 				}
 			} else {
 			} else {
 				err = c.deleteDeployment(r, depl, env, client)
 				err = c.deleteDeployment(r, depl, env, client)
 
 
 				if err != nil {
 				if err != nil {
-					return err
+					return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, deploymentID: %d] error deleting deployment: %w",
+						webhookID, owner, repo, env.ID, depl.ID, err)
 				}
 				}
 			}
 			}
 		}
 		}
@@ -149,7 +155,7 @@ func (c *GithubIncomingWebhookHandler) deleteDeployment(
 	cluster, err := c.Repo().Cluster().ReadCluster(env.ProjectID, env.ClusterID)
 	cluster, err := c.Repo().Cluster().ReadCluster(env.ProjectID, env.ClusterID)
 
 
 	if err != nil {
 	if err != nil {
-		return err
+		return fmt.Errorf("[projectID: %d, clusterID: %d] error reading cluster: %w", env.ProjectID, env.ClusterID, err)
 	}
 	}
 
 
 	agent, err := c.GetAgent(r, cluster, "")
 	agent, err := c.GetAgent(r, cluster, "")
@@ -163,7 +169,8 @@ func (c *GithubIncomingWebhookHandler) deleteDeployment(
 		err = agent.DeleteNamespace(depl.Namespace)
 		err = agent.DeleteNamespace(depl.Namespace)
 
 
 		if err != nil {
 		if err != nil {
-			return err
+			return fmt.Errorf("[owner: %s, repo: %s, environmentID: %d, deploymentID: %d] error deleting namespace '%s': %w",
+				env.GitRepoOwner, env.GitRepoName, env.ID, depl.ID, depl.Namespace, err)
 		}
 		}
 	}
 	}
 
 
@@ -188,7 +195,8 @@ func (c *GithubIncomingWebhookHandler) deleteDeployment(
 	_, err = c.Repo().Environment().UpdateDeployment(depl)
 	_, err = c.Repo().Environment().UpdateDeployment(depl)
 
 
 	if err != nil {
 	if err != nil {
-		return err
+		return fmt.Errorf("[owner: %s, repo: %s, environmentID: %d, deploymentID: %d] error updating deployment: %w",
+			env.GitRepoOwner, env.GitRepoName, env.ID, depl.ID, err)
 	}
 	}
 
 
 	return nil
 	return nil

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

@@ -815,5 +815,35 @@ func getReleaseRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
+	// PATCH /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/releases/{name}/git_action_config -> release.NewUpdateGitActionConfigHandler
+	updateGitActionConfigEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPatch,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/releases/{name}/git_action_config",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+			},
+		},
+	)
+
+	updateGitActionConfigHandler := release.NewUpdateBuildConfigHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: updateGitActionConfigEndpoint,
+		Handler:  updateGitActionConfigHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 	return routes, newPath
 }
 }

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

@@ -46,6 +46,7 @@ type ServerConf struct {
 	GithubAppWebhookSecret string `env:"GITHUB_APP_WEBHOOK_SECRET"`
 	GithubAppWebhookSecret string `env:"GITHUB_APP_WEBHOOK_SECRET"`
 	GithubAppID            string `env:"GITHUB_APP_ID"`
 	GithubAppID            string `env:"GITHUB_APP_ID"`
 	GithubAppSecretPath    string `env:"GITHUB_APP_SECRET_PATH"`
 	GithubAppSecretPath    string `env:"GITHUB_APP_SECRET_PATH"`
+	GithubAppSecret        []byte
 
 
 	GoogleClientID         string `env:"GOOGLE_CLIENT_ID"`
 	GoogleClientID         string `env:"GOOGLE_CLIENT_ID"`
 	GoogleClientSecret     string `env:"GOOGLE_CLIENT_SECRET"`
 	GoogleClientSecret     string `env:"GOOGLE_CLIENT_SECRET"`

+ 9 - 0
api/server/shared/config/loader/loader.go

@@ -2,6 +2,7 @@ package loader
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"io/ioutil"
 	"net/http"
 	"net/http"
 	"strconv"
 	"strconv"
 
 
@@ -167,6 +168,14 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 				BaseURL:      sc.ServerURL,
 				BaseURL:      sc.ServerURL,
 			}, sc.GithubAppName, sc.GithubAppWebhookSecret, sc.GithubAppSecretPath, AppID)
 			}, sc.GithubAppName, sc.GithubAppWebhookSecret, sc.GithubAppSecretPath, AppID)
 		}
 		}
+
+		secret, err := ioutil.ReadFile(sc.GithubAppSecretPath)
+
+		if err != nil {
+			return nil, fmt.Errorf("could not read github app secret: %s", err)
+		}
+
+		sc.GithubAppSecret = append(sc.GithubAppSecret, secret...)
 	}
 	}
 
 
 	if sc.SlackClientID != "" && sc.SlackClientSecret != "" {
 	if sc.SlackClientID != "" && sc.SlackClientSecret != "" {

+ 4 - 0
api/types/release.go

@@ -193,3 +193,7 @@ type GetReleaseAllPodsResponse []v1.Pod
 type PatchUpdateReleaseTags struct {
 type PatchUpdateReleaseTags struct {
 	Tags []string `json:"tags"`
 	Tags []string `json:"tags"`
 }
 }
+
+type UpdateActionConfigRequest struct {
+	GitActionConfig *GitActionConfig `json:"git_action_config"`
+}

+ 16 - 5
dashboard/src/components/repo-selector/BranchList.tsx

@@ -12,9 +12,14 @@ import SearchBar from "../SearchBar";
 type Props = {
 type Props = {
   actionConfig: ActionConfigType;
   actionConfig: ActionConfigType;
   setBranch: (x: string) => void;
   setBranch: (x: string) => void;
+  currentBranch?: string;
 };
 };
 
 
-const BranchList: React.FC<Props> = ({ setBranch, actionConfig }) => {
+const BranchList: React.FC<Props> = ({
+  setBranch,
+  actionConfig,
+  currentBranch,
+}) => {
   const [loading, setLoading] = useState(true);
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState(false);
   const [error, setError] = useState(false);
   const [branches, setBranches] = useState<string[]>([]);
   const [branches, setBranches] = useState<string[]>([]);
@@ -108,6 +113,9 @@ const BranchList: React.FC<Props> = ({ setBranch, actionConfig }) => {
         >
         >
           <img src={branch_icon} alt={"branch icon"} />
           <img src={branch_icon} alt={"branch icon"} />
           {branch}
           {branch}
+          {currentBranch === branch && (
+            <i className="material-icons-outlined">check</i>
+          )}
         </BranchName>
         </BranchName>
       );
       );
     });
     });
@@ -144,10 +152,6 @@ const BranchName = styled.div`
   background: #ffffff11;
   background: #ffffff11;
   :hover {
   :hover {
     background: #ffffff22;
     background: #ffffff22;
-
-    > i {
-      background: #ffffff22;
-    }
   }
   }
 
 
   > img {
   > img {
@@ -156,6 +160,13 @@ const BranchName = styled.div`
     margin-left: 12px;
     margin-left: 12px;
     margin-right: 12px;
     margin-right: 12px;
   }
   }
+
+  > i {
+    margin-left: auto;
+    margin-right: 15px;
+    font-size: 18px;
+    color: #03b503;
+  }
 `;
 `;
 
 
 const LoadingWrapper = styled.div`
 const LoadingWrapper = styled.div`

+ 21 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/BuildSettingsTab.tsx

@@ -18,6 +18,8 @@ import yaml from "js-yaml";
 import { AxiosError } from "axios";
 import { AxiosError } from "axios";
 import { AddCustomBuildpackForm } from "components/repo-selector/BuildpackSelection";
 import { AddCustomBuildpackForm } from "components/repo-selector/BuildpackSelection";
 import { DeviconsNameList } from "assets/devicons-name-list";
 import { DeviconsNameList } from "assets/devicons-name-list";
+import Selector from "components/Selector";
+import BranchList from "components/repo-selector/BranchList";
 
 
 type Buildpack = {
 type Buildpack = {
   name: string;
   name: string;
@@ -72,6 +74,12 @@ const BuildSettingsTab: React.FC<Props> = ({ chart, isPreviousVersion }) => {
     "loading" | "successful" | string
     "loading" | "successful" | string
   >("");
   >("");
 
 
+  const [currentBranch, setCurrentBranch] = useState(
+    () => chart?.git_action_config?.git_branch
+  );
+
+  const saveNewBranch = async (newBranch: string) => {};
+
   const saveBuildConfig = async (config: BuildConfig) => {
   const saveBuildConfig = async (config: BuildConfig) => {
     if (config === null) {
     if (config === null) {
       return;
       return;
@@ -231,7 +239,7 @@ const BuildSettingsTab: React.FC<Props> = ({ chart, isPreviousVersion }) => {
     }
     }
   };
   };
 
 
-  const getActionConfig = () => {
+  const currentActionConfig = useMemo(() => {
     const actionConf = chart.git_action_config;
     const actionConf = chart.git_action_config;
     if (actionConf && actionConf.gitlab_integration_id) {
     if (actionConf && actionConf.gitlab_integration_id) {
       return {
       return {
@@ -244,7 +252,7 @@ const BuildSettingsTab: React.FC<Props> = ({ chart, isPreviousVersion }) => {
       kind: "github",
       kind: "github",
       ...actionConf,
       ...actionConf,
     } as FullActionConfigType;
     } as FullActionConfigType;
-  };
+  }, [chart]);
 
 
   return (
   return (
     <Wrapper>
     <Wrapper>
@@ -296,12 +304,22 @@ const BuildSettingsTab: React.FC<Props> = ({ chart, isPreviousVersion }) => {
           }}
           }}
         ></KeyValueArray>
         ></KeyValueArray>
 
 
+        <Heading>Select default branch</Heading>
+        <Helper>
+          Change the default branch the deployments will be made from.
+        </Helper>
+        <BranchList
+          actionConfig={currentActionConfig}
+          setBranch={setCurrentBranch}
+          currentBranch={currentBranch}
+        />
+
         {!chart.git_action_config.dockerfile_path ? (
         {!chart.git_action_config.dockerfile_path ? (
           <>
           <>
             <Heading>Buildpack Settings</Heading>
             <Heading>Buildpack Settings</Heading>
             <BuildpackConfigSection
             <BuildpackConfigSection
               currentChart={chart}
               currentChart={chart}
-              actionConfig={getActionConfig()}
+              actionConfig={currentActionConfig}
               onChange={(buildConfig) => setBuildConfig(buildConfig)}
               onChange={(buildConfig) => setBuildConfig(buildConfig)}
             />
             />
           </>
           </>

+ 7 - 9
dashboard/src/main/home/cluster-dashboard/stacks/launch/Overview.tsx

@@ -46,14 +46,12 @@ const Overview = () => {
       )
       )
       .then((res) => {
       .then((res) => {
         if (res.data) {
         if (res.data) {
-          const availableNamespaces = res.data.items.filter(
-            (namespace: any) => {
-              return namespace.status.phase !== "Terminating";
-            }
-          );
+          const availableNamespaces = res.data.filter((namespace: any) => {
+            return namespace.status !== "Terminating";
+          });
           const namespaceOptions = availableNamespaces.map(
           const namespaceOptions = availableNamespaces.map(
-            (x: { metadata: { name: string } }) => {
-              return { label: x.metadata.name, value: x.metadata.name };
+            (x: { name: string }) => {
+              return { label: x.name, value: x.name };
             }
             }
           );
           );
           if (availableNamespaces.length > 0) {
           if (availableNamespaces.length > 0) {
@@ -203,7 +201,7 @@ const ClusterSection = styled.div`
 
 
 const Br = styled.div<{ height?: string }>`
 const Br = styled.div<{ height?: string }>`
   width: 100%;
   width: 100%;
-  height: ${props => props.height || "1px"};
+  height: ${(props) => props.height || "1px"};
 `;
 `;
 
 
 const Required = styled.div`
 const Required = styled.div`
@@ -230,4 +228,4 @@ const StyledLaunchFlow = styled.div`
   margin-top: ${(props: { disableMarginTop?: boolean }) =>
   margin-top: ${(props: { disableMarginTop?: boolean }) =>
     props.disableMarginTop ? "inherit" : "calc(50vh - 380px)"};
     props.disableMarginTop ? "inherit" : "calc(50vh - 380px)"};
   padding-bottom: 150px;
   padding-bottom: 150px;
-`;
+`;

+ 1 - 0
internal/repository/git_action_config.go

@@ -7,4 +7,5 @@ import "github.com/porter-dev/porter/internal/models"
 type GitActionConfigRepository interface {
 type GitActionConfigRepository interface {
 	CreateGitActionConfig(gr *models.GitActionConfig) (*models.GitActionConfig, error)
 	CreateGitActionConfig(gr *models.GitActionConfig) (*models.GitActionConfig, error)
 	ReadGitActionConfig(id uint) (*models.GitActionConfig, error)
 	ReadGitActionConfig(id uint) (*models.GitActionConfig, error)
+	UpdateGitActionConfig(gr *models.GitActionConfig) error
 }
 }

+ 4 - 0
internal/repository/gorm/git_action_config.go

@@ -49,3 +49,7 @@ func (repo *GitActionConfigRepository) ReadGitActionConfig(id uint) (*models.Git
 
 
 	return ga, nil
 	return ga, nil
 }
 }
+
+func (repo *GitActionConfigRepository) UpdateGitActionConfig(ga *models.GitActionConfig) error {
+	return repo.db.Save(ga).Error
+}

+ 6 - 0
internal/repository/test/repository.go

@@ -45,6 +45,7 @@ type TestRepository struct {
 	apiToken                  repository.APITokenRepository
 	apiToken                  repository.APITokenRepository
 	policy                    repository.PolicyRepository
 	policy                    repository.PolicyRepository
 	tag                       repository.TagRepository
 	tag                       repository.TagRepository
+	stack                     repository.StackRepository
 }
 }
 
 
 func (t *TestRepository) User() repository.UserRepository {
 func (t *TestRepository) User() repository.UserRepository {
@@ -207,6 +208,10 @@ func (t *TestRepository) Tag() repository.TagRepository {
 	return t.tag
 	return t.tag
 }
 }
 
 
+func (t *TestRepository) Stack() repository.StackRepository {
+	return t.stack
+}
+
 // NewRepository returns a Repository which persists users in memory
 // NewRepository returns a Repository which persists users in memory
 // and accepts a parameter that can trigger read/write errors
 // and accepts a parameter that can trigger read/write errors
 func NewRepository(canQuery bool, failingMethods ...string) repository.Repository {
 func NewRepository(canQuery bool, failingMethods ...string) repository.Repository {
@@ -251,5 +256,6 @@ func NewRepository(canQuery bool, failingMethods ...string) repository.Repositor
 		apiToken:                  NewAPITokenRepository(canQuery),
 		apiToken:                  NewAPITokenRepository(canQuery),
 		policy:                    NewPolicyRepository(canQuery),
 		policy:                    NewPolicyRepository(canQuery),
 		tag:                       NewTagRepository(),
 		tag:                       NewTagRepository(),
+		stack:                     NewStackRepository(),
 	}
 	}
 }
 }

+ 56 - 0
internal/repository/test/stack.go

@@ -0,0 +1,56 @@
+package test
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+)
+
+type StackRepository struct{}
+
+func NewStackRepository() repository.StackRepository {
+	return &StackRepository{}
+}
+
+// CreateStack creates a new stack
+func (repo *StackRepository) CreateStack(stack *models.Stack) (*models.Stack, error) {
+	panic("unimplemented")
+}
+
+// ReadStack gets a stack specified by its string id
+func (repo *StackRepository) ListStacks(projectID, clusterID uint, namespace string) ([]*models.Stack, error) {
+	panic("unimplemented")
+}
+
+// ReadStack gets a stack specified by its string id
+func (repo *StackRepository) ReadStackByStringID(projectID uint, stackID string) (*models.Stack, error) {
+	panic("unimplemented")
+}
+
+// DeleteStack creates a new stack
+func (repo *StackRepository) DeleteStack(stack *models.Stack) (*models.Stack, error) {
+	panic("unimplemented")
+}
+
+func (repo *StackRepository) UpdateStackRevision(revision *models.StackRevision) (*models.StackRevision, error) {
+	panic("unimplemented")
+}
+
+func (repo *StackRepository) ReadStackRevision(stackRevisionID uint) (*models.StackRevision, error) {
+	panic("unimplemented")
+}
+
+func (repo *StackRepository) ReadStackRevisionByNumber(stackID uint, revisionNumber uint) (*models.StackRevision, error) {
+	panic("unimplemented")
+}
+
+func (repo *StackRepository) AppendNewRevision(revision *models.StackRevision) (*models.StackRevision, error) {
+	panic("unimplemented")
+}
+
+func (repo *StackRepository) ReadStackResource(resourceID uint) (*models.StackResource, error) {
+	panic("unimplemented")
+}
+
+func (repo *StackRepository) UpdateStackResource(resource *models.StackResource) (*models.StackResource, error) {
+	panic("unimplemented")
+}