Sfoglia il codice sorgente

Merge branch 'master' of github.com-meehawk:porter-dev/porter into feat/open-application-in-new-tab

Soham Parekh 3 anni fa
parent
commit
8e911c9e69
45 ha cambiato i file con 1375 aggiunte e 382 eliminazioni
  1. 5 2
      api/client/template.go
  2. 1 1
      api/server/handlers/cluster/create_namespace.go
  3. 7 6
      api/server/handlers/environment/create.go
  4. 20 3
      api/server/handlers/environment/enable_pull_request.go
  5. 32 30
      api/server/handlers/environment/finalize_deployment.go
  6. 52 50
      api/server/handlers/environment/finalize_deployment_with_errors.go
  7. 14 0
      api/server/handlers/environment/list_deployments_by_cluster.go
  8. 17 15
      api/server/handlers/environment/reenable_deployment.go
  9. 24 16
      api/server/handlers/environment/trigger_deployment_workflow.go
  10. 19 14
      api/server/handlers/environment/update_deployment.go
  11. 15 13
      api/server/handlers/environment/update_deployment_status.go
  12. 62 6
      api/server/handlers/environment/update_environment_settings.go
  13. 23 0
      api/server/handlers/release/create.go
  14. 116 0
      api/server/handlers/v1/template/get.go
  15. 115 0
      api/server/handlers/v1/template/get_upgrade_notes.go
  16. 76 0
      api/server/handlers/v1/template/list.go
  17. 132 0
      api/server/handlers/webhook/github_incoming.go
  18. 206 1
      api/server/router/v1/project.go
  19. 2 2
      api/types/cluster.go
  20. 21 28
      api/types/environment.go
  21. 17 5
      api/types/template.go
  22. 143 74
      cli/cmd/apply.go
  23. 4 2
      cli/cmd/deploy/create.go
  24. 6 4
      cli/cmd/preview/build_image_driver.go
  25. 6 4
      cli/cmd/preview/update_config_driver.go
  26. 37 6
      cli/cmd/preview/utils.go
  27. 3 2
      dashboard/src/main/home/cluster-dashboard/dashboard/ClusterSettings.tsx
  28. 6 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  29. 32 23
      dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx
  30. 2 2
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/NamespaceLabels.tsx
  31. 5 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentCard.tsx
  32. 44 35
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentSettings.tsx
  33. 2 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/types.ts
  34. 4 2
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_TemplateSelector.tsx
  35. 6 2
      dashboard/src/main/home/cluster-dashboard/stacks/components/NewAppResourceForm.tsx
  36. 4 2
      dashboard/src/main/home/cluster-dashboard/stacks/launch/components/AddResourceButton.tsx
  37. 31 4
      dashboard/src/main/home/launch/Launch.tsx
  38. 1 0
      dashboard/src/main/home/launch/expanded-template/ExpandedTemplate.tsx
  39. 1 0
      dashboard/src/main/home/modals/UpgradeChartModal.tsx
  40. 14 8
      dashboard/src/shared/api.tsx
  41. 19 0
      internal/helm/repo/repo.go
  42. 1 1
      internal/integrations/ci/actions/preview.go
  43. 3 3
      internal/kubernetes/agent.go
  44. 21 8
      internal/models/environment.go
  45. 4 4
      workers/jobs/helm_revisions_count_tracker.go

+ 5 - 2
api/client/template.go

@@ -9,13 +9,14 @@ import (
 
 func (c *Client) ListTemplates(
 	ctx context.Context,
+	projectID uint,
 	req *types.ListTemplatesRequest,
 ) (*types.ListTemplatesResponse, error) {
 	resp := &types.ListTemplatesResponse{}
 
 	err := c.getRequest(
 		fmt.Sprintf(
-			"/templates",
+			"/v1/projects/%d/templates", projectID,
 		),
 		req,
 		resp,
@@ -26,6 +27,7 @@ func (c *Client) ListTemplates(
 
 func (c *Client) GetTemplate(
 	ctx context.Context,
+	projectID uint,
 	name, version string,
 	req *types.GetTemplateRequest,
 ) (*types.GetTemplateResponse, error) {
@@ -33,7 +35,8 @@ func (c *Client) GetTemplate(
 
 	err := c.getRequest(
 		fmt.Sprintf(
-			"/templates/%s/%s",
+			"/v1/projects/%d/templates/%s/versions/%s",
+			projectID,
 			name, version,
 		),
 		req,

+ 1 - 1
api/server/handlers/cluster/create_namespace.go

@@ -55,7 +55,7 @@ func (c *CreateNamespaceHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	namespace, err := agent.CreateNamespace(request.Name, request.Annotations)
+	namespace, err := agent.CreateNamespace(request.Name, request.Labels)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 7 - 6
api/server/handlers/environment/create.go

@@ -74,16 +74,17 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		Mode:                request.Mode,
 		WebhookID:           string(webhookUID),
 		NewCommentsDisabled: request.DisableNewComments,
+		GitDeployBranches:   strings.Join(request.GitDeployBranches, ","),
 	}
 
-	if len(request.NamespaceAnnotations) > 0 {
-		var annotations []string
+	if len(request.NamespaceLabels) > 0 {
+		var labels []string
 
-		for k, v := range request.NamespaceAnnotations {
-			annotations = append(annotations, fmt.Sprintf("%s=%s", k, v))
+		for k, v := range request.NamespaceLabels {
+			labels = append(labels, fmt.Sprintf("%s=%s", k, v))
 		}
 
-		env.NamespaceAnnotations = []byte(strings.Join(annotations, ","))
+		env.NamespaceLabels = []byte(strings.Join(labels, ","))
 	}
 
 	// write Github actions files to the repo
@@ -104,7 +105,7 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 				"content_type": "json",
 				"secret":       c.Config().ServerConf.GithubIncomingWebhookSecret,
 			},
-			Events: []string{"pull_request"},
+			Events: []string{"pull_request", "push"},
 			Active: github.Bool(true),
 		},
 	)

+ 20 - 3
api/server/handlers/environment/enable_pull_request.go

@@ -69,8 +69,25 @@ func (c *EnablePullRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 
 		if !found {
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-				fmt.Errorf("base branch '%s' is not enabled for this preview environment, please enable it in the settings page",
-					request.BranchInto), http.StatusBadRequest,
+				fmt.Errorf("base branch '%s' is not enabled for this preview environment, please enable it "+
+					"in the settings page to continue", request.BranchInto), http.StatusBadRequest,
+			))
+			return
+		}
+	} else if len(envType.GitDeployBranches) > 0 {
+		found := false
+
+		for _, branch := range env.ToEnvironmentType().GitDeployBranches {
+			if branch == request.BranchFrom {
+				found = true
+				break
+			}
+		}
+
+		if found {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("head branch '%s' is enabled for branch deploys for this preview environment, "+
+					"please disable it in the settings page to continue", request.BranchInto), http.StatusBadRequest,
 			))
 			return
 		}
@@ -104,7 +121,7 @@ func (c *EnablePullRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 			Ref: request.BranchFrom,
 			Inputs: map[string]interface{}{
 				"pr_number":      strconv.FormatUint(uint64(request.Number), 10),
-				"pr_title":       *pr.Title,
+				"pr_title":       pr.GetTitle(),
 				"pr_branch_from": request.BranchFrom,
 				"pr_branch_into": request.BranchInto,
 			},

+ 32 - 30
api/server/handlers/environment/finalize_deployment.go

@@ -148,42 +148,44 @@ 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 !depl.IsBranchDeploy() {
+		// 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 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 prClosed {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
+				http.StatusConflict))
+			return
+		}
 
-	commentBody := "## Porter Preview Environments\n"
+		commentBody := "## Porter Preview Environments\n"
 
-	if depl.Subdomain == "" {
-		commentBody += fmt.Sprintf(
-			"✅ The latest SHA ([`%s`](https://github.com/%s/%s/commit/%s)) has been successfully deployed.",
-			depl.CommitSHA, depl.RepoOwner, depl.RepoName, depl.CommitSHA,
-		)
-	} else {
-		commentBody += fmt.Sprintf(
-			"✅ The latest SHA ([`%s`](https://github.com/%s/%s/commit/%s)) has been successfully deployed to %s",
-			depl.CommitSHA, depl.RepoOwner, depl.RepoName, depl.CommitSHA, depl.Subdomain,
-		)
-	}
+		if depl.Subdomain == "" {
+			commentBody += fmt.Sprintf(
+				"✅ The latest SHA ([`%s`](https://github.com/%s/%s/commit/%s)) has been successfully deployed.",
+				depl.CommitSHA, depl.RepoOwner, depl.RepoName, depl.CommitSHA,
+			)
+		} else {
+			commentBody += fmt.Sprintf(
+				"✅ The latest SHA ([`%s`](https://github.com/%s/%s/commit/%s)) has been successfully deployed to %s",
+				depl.CommitSHA, depl.RepoOwner, depl.RepoName, depl.CommitSHA, depl.Subdomain,
+			)
+		}
 
-	err = createOrUpdateComment(client, c.Repo(), env.NewCommentsDisabled, depl, github.String(commentBody))
+		err = createOrUpdateComment(client, c.Repo(), env.NewCommentsDisabled, depl, github.String(commentBody))
 
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
 	}
 
 	c.WriteResult(w, r, depl.ToDeploymentType())

+ 52 - 50
api/server/handlers/environment/finalize_deployment_with_errors.go

@@ -119,20 +119,6 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 		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(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
@@ -147,51 +133,67 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 		},
 	)
 
-	workflowRun, err := commonutils.GetLatestWorkflowRun(client, depl.RepoOwner, depl.RepoName,
-		fmt.Sprintf("porter_%s_env.yml", env.Name), depl.PRBranchFrom)
+	if !depl.IsBranchDeploy() {
+		// 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.NewErrInternal(err))
-		return
-	}
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
+			return
+		}
 
-	commentBody := fmt.Sprintf(
-		"## Porter Preview Environments\n"+
-			"❌ Errors encountered while deploying the changes\n"+
-			"||Deployment Information|\n"+
-			"|-|-|\n"+
-			"| Latest SHA | [`%s`](https://github.com/%s/%s/commit/%s) |\n"+
-			"| Build Logs | %s |\n",
-		depl.CommitSHA, depl.RepoOwner, depl.RepoName, depl.CommitSHA, workflowRun.GetHTMLURL(),
-	)
+		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)
 
-	if len(request.SuccessfulResources) > 0 {
-		commentBody += "#### Successfully deployed resources\n"
-
-		for _, res := range request.SuccessfulResources {
-			if res.ReleaseType == "job" {
-				commentBody += fmt.Sprintf("- [`%s`](%s/jobs/%s/%s/%s?project_id=%d)\n",
-					res.ReleaseName, c.Config().ServerConf.ServerURL, cluster.Name, depl.Namespace,
-					res.ReleaseName, project.ID)
-			} else {
-				commentBody += fmt.Sprintf("- [`%s`](%s/applications/%s/%s/%s?project_id=%d)\n",
-					res.ReleaseName, c.Config().ServerConf.ServerURL, cluster.Name, depl.Namespace,
-					res.ReleaseName, project.ID)
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		commentBody := fmt.Sprintf(
+			"## Porter Preview Environments\n"+
+				"❌ Errors encountered while deploying the changes\n"+
+				"||Deployment Information|\n"+
+				"|-|-|\n"+
+				"| Latest SHA | [`%s`](https://github.com/%s/%s/commit/%s) |\n"+
+				"| Build Logs | %s |\n",
+			depl.CommitSHA, depl.RepoOwner, depl.RepoName, depl.CommitSHA, workflowRun.GetHTMLURL(),
+		)
+
+		if len(request.SuccessfulResources) > 0 {
+			commentBody += "#### Successfully deployed resources\n"
+
+			for _, res := range request.SuccessfulResources {
+				if res.ReleaseType == "job" {
+					commentBody += fmt.Sprintf("- [`%s`](%s/jobs/%s/%s/%s?project_id=%d)\n",
+						res.ReleaseName, c.Config().ServerConf.ServerURL, cluster.Name, depl.Namespace,
+						res.ReleaseName, project.ID)
+				} else {
+					commentBody += fmt.Sprintf("- [`%s`](%s/applications/%s/%s/%s?project_id=%d)\n",
+						res.ReleaseName, c.Config().ServerConf.ServerURL, cluster.Name, depl.Namespace,
+						res.ReleaseName, project.ID)
+				}
 			}
 		}
-	}
 
-	commentBody += "#### Failed resources\n"
+		commentBody += "#### Failed resources\n"
 
-	for res, err := range request.Errors {
-		commentBody += fmt.Sprintf("<details>\n  <summary><code>%s</code></summary>\n\n  **Error:** %s\n</details>\n", res, err)
-	}
+		for res, err := range request.Errors {
+			commentBody += fmt.Sprintf("<details>\n  <summary><code>%s</code></summary>\n\n  **Error:** %s\n</details>\n", res, err)
+		}
 
-	err = createOrUpdateComment(client, c.Repo(), env.NewCommentsDisabled, depl, github.String(commentBody))
+		err = createOrUpdateComment(client, c.Repo(), env.NewCommentsDisabled, depl, github.String(commentBody))
 
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
 	}
 
 	c.WriteResult(w, r, depl.ToDeploymentType())

+ 14 - 0
api/server/handlers/environment/list_deployments_by_cluster.go

@@ -281,6 +281,10 @@ func fetchOpenPullRequests(
 			}
 		}
 
+		if isDeployBranch(pr.GetHead().GetRef(), env) {
+			continue
+		}
+
 		if _, ok := deplInfoMap[fmt.Sprintf("%s-%s-%d", env.GitRepoOwner, env.GitRepoName, pr.GetNumber())]; !ok {
 			prs = append(prs, &types.PullRequest{
 				Title:      pr.GetTitle(),
@@ -295,3 +299,13 @@ func fetchOpenPullRequests(
 
 	return prs, nil
 }
+
+func isDeployBranch(branch string, env *models.Environment) bool {
+	for _, b := range env.ToEnvironmentType().GitDeployBranches {
+		if b == branch {
+			return true
+		}
+	}
+
+	return false
+}

+ 17 - 15
api/server/handlers/environment/reenable_deployment.go

@@ -71,21 +71,23 @@ func (c *ReenableDeploymentHandler) 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, 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.IsBranchDeploy() {
+		// 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

+ 24 - 16
api/server/handlers/environment/trigger_deployment_workflow.go

@@ -72,21 +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
+	if !depl.IsBranchDeploy() {
+		// 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,
@@ -105,12 +107,18 @@ func (c *TriggerDeploymentWorkflowHandler) ServeHTTP(w http.ResponseWriter, r *h
 		return
 	}
 
+	prNumber := depl.PullRequestID
+
+	if depl.IsBranchDeploy() {
+		prNumber = depl.ID
+	}
+
 	ghResp, err := client.Actions.CreateWorkflowDispatchEventByFileName(
 		r.Context(), env.GitRepoOwner, env.GitRepoName, fmt.Sprintf("porter_%s_env.yml", env.Name),
 		github.CreateWorkflowDispatchEventRequest{
 			Ref: depl.PRBranchFrom,
 			Inputs: map[string]interface{}{
-				"pr_number":      strconv.FormatUint(uint64(depl.PullRequestID), 10),
+				"pr_number":      strconv.FormatUint(uint64(prNumber), 10),
 				"pr_title":       depl.PRName,
 				"pr_branch_from": depl.PRBranchFrom,
 				"pr_branch_into": depl.PRBranchInto,

+ 19 - 14
api/server/handlers/environment/update_deployment.go

@@ -109,21 +109,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 !depl.IsBranchDeploy() {
+		// 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 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 prClosed {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
+				http.StatusConflict))
+			return
+		}
 	}
 
 	ghDeployment, err := createGithubDeployment(client, env, request.PRBranchFrom, request.ActionID)
@@ -133,7 +135,10 @@ func (c *UpdateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		return
 	}
 
-	depl.Namespace = request.Namespace
+	if !depl.IsBranchDeploy() {
+		depl.Namespace = request.Namespace
+	}
+
 	depl.GHDeploymentID = ghDeployment.GetID()
 	depl.CommitSHA = request.CommitSHA
 

+ 15 - 13
api/server/handlers/environment/update_deployment_status.go

@@ -115,21 +115,23 @@ func (c *UpdateDeploymentStatusHandler) ServeHTTP(w http.ResponseWriter, r *http
 		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 !depl.IsBranchDeploy() {
+		// 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 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 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) {

+ 62 - 6
api/server/handlers/environment/update_environment_settings.go

@@ -1,6 +1,7 @@
 package environment
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"net/http"
@@ -79,6 +80,61 @@ func (c *UpdateEnvironmentSettingsHandler) ServeHTTP(w http.ResponseWriter, r *h
 		env.GitRepoBranches = strings.Join(request.GitRepoBranches, ",")
 	}
 
+	newBranches = []string{}
+
+	for _, br := range request.GitDeployBranches {
+		name := strings.TrimSpace(br)
+
+		if len(name) > 0 {
+			newBranches = append(newBranches, name)
+		}
+	}
+
+	changed = !reflect.DeepEqual(env.ToEnvironmentType().GitDeployBranches, newBranches)
+
+	if changed {
+		// let us check if the webhook has access to the "push" event
+		client, err := getGithubClientFromEnvironment(c.Config(), env)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		hook, _, err := client.Repositories.GetHook(
+			context.Background(), env.GitRepoOwner, env.GitRepoName, env.GithubWebhookID,
+		)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		found := false
+
+		for _, ev := range hook.Events {
+			if ev == "push" {
+				found = true
+				break
+			}
+		}
+
+		if !found {
+			hook.Events = append(hook.Events, "push")
+
+			_, _, err := client.Repositories.EditHook(
+				context.Background(), env.GitRepoOwner, env.GitRepoName, env.GithubWebhookID, hook,
+			)
+
+			if err != nil {
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				return
+			}
+		}
+
+		env.GitDeployBranches = strings.Join(request.GitDeployBranches, ",")
+	}
+
 	if request.DisableNewComments != env.NewCommentsDisabled {
 		env.NewCommentsDisabled = request.DisableNewComments
 		changed = true
@@ -89,18 +145,18 @@ func (c *UpdateEnvironmentSettingsHandler) ServeHTTP(w http.ResponseWriter, r *h
 		changed = true
 	}
 
-	if len(request.NamespaceAnnotations) > 0 {
-		var annotations []string
+	if len(request.NamespaceLabels) > 0 {
+		var labels []string
 
-		for k, v := range request.NamespaceAnnotations {
-			annotations = append(annotations, fmt.Sprintf("%s=%s", k, v))
+		for k, v := range request.NamespaceLabels {
+			labels = append(labels, fmt.Sprintf("%s=%s", k, v))
 		}
 
-		env.NamespaceAnnotations = []byte(strings.Join(annotations, ","))
+		env.NamespaceLabels = []byte(strings.Join(labels, ","))
 
 		changed = true
 	} else {
-		env.NamespaceAnnotations = []byte{}
+		env.NamespaceLabels = []byte{}
 
 		changed = true
 	}

+ 23 - 0
api/server/handlers/release/create.go

@@ -19,6 +19,7 @@ import (
 	"github.com/porter-dev/porter/internal/encryption"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/helm/loader"
+	"github.com/porter-dev/porter/internal/helm/repo"
 	"github.com/porter-dev/porter/internal/integrations/ci/actions"
 	"github.com/porter-dev/porter/internal/integrations/ci/gitlab"
 	"github.com/porter-dev/porter/internal/models"
@@ -76,6 +77,28 @@ func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		request.RepoURL = c.Config().ServerConf.DefaultApplicationHelmRepoURL
 	}
 
+	// if the repo url is not an addon or application url, validate against the helm repos
+	if request.RepoURL != c.Config().ServerConf.DefaultAddonHelmRepoURL && request.RepoURL != c.Config().ServerConf.DefaultApplicationHelmRepoURL {
+		// load the helm repos in the project
+		hrs, err := c.Repo().HelmRepo().ListHelmReposByProjectID(cluster.ProjectID)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		isValid := repo.ValidateRepoURL(c.Config().ServerConf.DefaultAddonHelmRepoURL, c.Config().ServerConf.DefaultApplicationHelmRepoURL, hrs, request.RepoURL)
+
+		if !isValid {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("invalid repo_url parameter"),
+				http.StatusBadRequest,
+			))
+
+			return
+		}
+	}
+
 	if request.TemplateVersion == "latest" {
 		request.TemplateVersion = ""
 	}

+ 116 - 0
api/server/handlers/v1/template/get.go

@@ -0,0 +1,116 @@
+package template
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"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/helm/loader"
+	"github.com/porter-dev/porter/internal/helm/repo"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/templater/parser"
+)
+
+type TemplateGetHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewTemplateGetHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *TemplateGetHandler {
+	return &TemplateGetHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (t *TemplateGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	request := &types.GetTemplateRequest{}
+
+	ok := t.DecodeAndValidate(w, r, request)
+
+	if !ok {
+		return
+	}
+
+	if request.RepoURL == "" {
+		request.RepoURL = t.Config().ServerConf.DefaultApplicationHelmRepoURL
+	}
+
+	hrs, err := t.Repo().HelmRepo().ListHelmReposByProjectID(project.ID)
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	isValid := repo.ValidateRepoURL(t.Config().ServerConf.DefaultAddonHelmRepoURL, t.Config().ServerConf.DefaultApplicationHelmRepoURL, hrs, request.RepoURL)
+
+	if !isValid {
+		t.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("invalid repo_url parameter"),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	name, _ := requestutils.GetURLParamString(r, types.URLParamTemplateName)
+
+	if name == "" {
+		t.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("template name is required"),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	version, _ := requestutils.GetURLParamString(r, types.URLParamTemplateVersion)
+
+	// if version passed as latest, pass empty string to loader to get latest
+	if version == "latest" {
+		version = ""
+	}
+
+	chart, err := loader.LoadChartPublic(request.RepoURL, name, version)
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	parserDef := &parser.ClientConfigDefault{
+		HelmChart: chart,
+	}
+
+	res := &types.GetTemplateResponse{}
+	res.Metadata = chart.Metadata
+	res.Values = chart.Values
+	res.RepoURL = request.RepoURL
+
+	for _, file := range chart.Files {
+		if strings.Contains(file.Name, "form.yaml") {
+			formYAML, err := parser.FormYAMLFromBytes(parserDef, file.Data, "declared", "")
+
+			if err != nil {
+				break
+			}
+
+			res.Form = formYAML
+		} else if strings.Contains(file.Name, "README.md") {
+			res.Markdown = string(file.Data)
+		}
+	}
+
+	t.WriteResult(w, r, res)
+}

+ 115 - 0
api/server/handlers/v1/template/get_upgrade_notes.go

@@ -0,0 +1,115 @@
+package template
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"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/helm/loader"
+	"github.com/porter-dev/porter/internal/helm/repo"
+	"github.com/porter-dev/porter/internal/helm/upgrade"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type TemplateGetUpgradeNotesHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewTemplateGetUpgradeNotesHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *TemplateGetUpgradeNotesHandler {
+	return &TemplateGetUpgradeNotesHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (t *TemplateGetUpgradeNotesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	request := &types.GetTemplateUpgradeNotesRequest{}
+
+	ok := t.DecodeAndValidate(w, r, request)
+
+	if !ok {
+		return
+	}
+
+	hrs, err := t.Repo().HelmRepo().ListHelmReposByProjectID(project.ID)
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	isValid := repo.ValidateRepoURL(t.Config().ServerConf.DefaultAddonHelmRepoURL, t.Config().ServerConf.DefaultApplicationHelmRepoURL, hrs, request.RepoURL)
+
+	if !isValid {
+		t.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("invalid repo_url parameter"),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	name, _ := requestutils.GetURLParamString(r, types.URLParamTemplateName)
+
+	if name == "" {
+		t.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("template name is required"),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	version, _ := requestutils.GetURLParamString(r, types.URLParamTemplateVersion)
+
+	// if version passed as latest, pass empty string to loader to get latest
+	if version == "latest" {
+		version = ""
+	}
+
+	prevVersion := request.PrevVersion
+
+	if prevVersion == "" {
+		prevVersion = "v0.0.0"
+	}
+
+	chart, err := loader.LoadChartPublic(request.RepoURL, name, version)
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := &upgrade.UpgradeFile{}
+
+	for _, file := range chart.Files {
+		if strings.Contains(file.Name, "upgrade.yaml") {
+			upgradeFile, err := upgrade.ParseUpgradeFileFromBytes(file.Data)
+
+			if err != nil {
+				break
+			}
+
+			upgradeFile, err = upgradeFile.GetUpgradeFileBetweenVersions(prevVersion, version)
+
+			if err != nil {
+				break
+			}
+
+			res = upgradeFile
+		}
+	}
+
+	t.WriteResult(w, r, res)
+}

+ 76 - 0
api/server/handlers/v1/template/list.go

@@ -0,0 +1,76 @@
+package template
+
+import (
+	"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/types"
+	"github.com/porter-dev/porter/internal/helm/loader"
+	"github.com/porter-dev/porter/internal/helm/repo"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type TemplateListHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewTemplateListHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *TemplateListHandler {
+	return &TemplateListHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (t *TemplateListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	request := &types.ListTemplatesRequest{}
+
+	ok := t.DecodeAndValidate(w, r, request)
+
+	if !ok {
+		return
+	}
+
+	repoURL := request.RepoURL
+
+	if repoURL == "" {
+		repoURL = t.Config().ServerConf.DefaultApplicationHelmRepoURL
+	}
+
+	hrs, err := t.Repo().HelmRepo().ListHelmReposByProjectID(project.ID)
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	isValid := repo.ValidateRepoURL(t.Config().ServerConf.DefaultAddonHelmRepoURL, t.Config().ServerConf.DefaultApplicationHelmRepoURL, hrs, repoURL)
+
+	if !isValid {
+		t.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("invalid repo_url parameter"),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	repoIndex, err := loader.LoadRepoIndexPublic(repoURL)
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	porterCharts := loader.RepoIndexToPorterChartList(repoIndex, repoURL)
+
+	t.WriteResult(w, r, porterCharts)
+}

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

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"net/http"
 	"strconv"
+	"strings"
 	"sync"
 
 	"github.com/bradleyfalzon/ghinstallation/v2"
@@ -59,6 +60,13 @@ func (c *GithubIncomingWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error processing pull request webhook event: %w", err)))
 			return
 		}
+	case *github.PushEvent:
+		err = c.processPushEvent(event, r)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error processing push webhook event: %w", err)))
+			return
+		}
 	}
 }
 
@@ -103,6 +111,21 @@ func (c *GithubIncomingWebhookHandler) processPullRequestEvent(event *github.Pul
 		if !found {
 			return nil
 		}
+	} else if len(envType.GitDeployBranches) > 0 {
+		// if the pull request's head branch is in the list of deploy branches
+		// then we ignore it to avoid a double deploy
+		found := false
+
+		for _, br := range envType.GitDeployBranches {
+			if br == event.GetPullRequest().GetHead().GetRef() {
+				found = true
+				break
+			}
+		}
+
+		if found {
+			return nil
+		}
 	}
 
 	// create deployment on GitHub API
@@ -320,6 +343,115 @@ func (c *GithubIncomingWebhookHandler) deleteDeployment(
 	return nil
 }
 
+func (c *GithubIncomingWebhookHandler) processPushEvent(event *github.PushEvent, r *http.Request) error {
+	if !strings.HasPrefix(event.GetRef(), "refs/heads/") {
+		return nil
+	}
+
+	// get the webhook id from the request
+	webhookID, reqErr := requestutils.GetURLParamString(r, types.URLParamIncomingWebhookID)
+
+	if reqErr != nil {
+		return fmt.Errorf(reqErr.Error())
+	}
+
+	owner := event.GetRepo().GetOwner().GetLogin()
+	repo := event.GetRepo().GetName()
+
+	env, err := c.Repo().Environment().ReadEnvironmentByWebhookIDOwnerRepoName(webhookID, owner, repo)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil
+		}
+
+		return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s] error reading environment: %w", webhookID, owner, repo, err)
+	}
+
+	envType := env.ToEnvironmentType()
+
+	branch := strings.TrimPrefix(event.GetRef(), "refs/heads/")
+
+	if len(envType.GitDeployBranches) > 0 {
+		found := false
+
+		for _, br := range envType.GitDeployBranches {
+			if br == branch {
+				found = true
+				break
+			}
+		}
+
+		if !found {
+			return nil
+		}
+	}
+
+	client, err := getGithubClientFromEnvironment(c.Config(), env)
+
+	if err != nil {
+		return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s] error creating github client: %w", webhookID, owner, repo, err)
+	}
+
+	namespace := fmt.Sprintf("previewbranch-%s-%s-%s", branch, strings.ReplaceAll(strings.ToLower(owner), "_", "-"),
+		strings.ReplaceAll(strings.ToLower(repo), "_", "-"))
+
+	if len(namespace) > 63 {
+		namespace = namespace[:63] // Kubernetes' DNS 1123 label requirement
+	}
+
+	var deplID uint
+
+	depl, err := c.Repo().Environment().ReadDeployment(env.ID, namespace)
+
+	if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
+		depl, err := c.Repo().Environment().CreateDeployment(&models.Deployment{
+			EnvironmentID: env.ID,
+			Namespace:     namespace,
+			Status:        types.DeploymentStatusCreating,
+			PRName:        fmt.Sprintf("Deployment for branch %s", branch),
+			RepoName:      repo,
+			RepoOwner:     owner,
+			CommitSHA:     event.GetAfter()[:7],
+			PRBranchFrom:  branch,
+			PRBranchInto:  branch,
+		})
+
+		if err != nil {
+			return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, branch: %s] "+
+				"error creating new deployment: %w", webhookID, owner, repo, env.ID, branch, err)
+		}
+
+		deplID = depl.ID
+	} else if err != nil {
+		return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, branch: %s] "+
+			"error reading deployment: %w", webhookID, owner, repo, env.ID, branch, err)
+	} else {
+		deplID = depl.ID
+	}
+
+	// FIXME: we should case on if env mode is auto or manual
+	_, err = client.Actions.CreateWorkflowDispatchEventByFileName(
+		r.Context(), owner, repo, fmt.Sprintf("porter_%s_env.yml", env.Name),
+		github.CreateWorkflowDispatchEventRequest{
+			Ref: branch,
+			Inputs: map[string]interface{}{
+				"pr_number":      fmt.Sprintf("%d", deplID),
+				"pr_title":       fmt.Sprintf("Deployment for branch %s", branch),
+				"pr_branch_from": branch,
+				"pr_branch_into": branch,
+			},
+		},
+	)
+
+	if err != nil {
+		return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, branch: %s] "+
+			"error creating workflow dispatch event: %w", webhookID, owner, repo, env.ID, branch, err)
+	}
+
+	return nil
+}
+
 func isSystemNamespace(namespace string) bool {
 	return namespace == "cert-manager" || namespace == "ingress-nginx" ||
 		namespace == "kube-node-lease" || namespace == "kube-public" ||

+ 206 - 1
api/server/router/v1/project.go

@@ -1,14 +1,18 @@
 package v1
 
 import (
+	"fmt"
+
 	"github.com/go-chi/chi"
 	"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"
+
+	v1Template "github.com/porter-dev/porter/api/server/handlers/v1/template"
 )
 
-// swagger:parameters createRegistry listRegistries
+// swagger:parameters createRegistry listRegistries listTemplates
 type projectPathParams struct {
 	// The project id
 	// in: path
@@ -17,6 +21,27 @@ type projectPathParams struct {
 	ProjectID uint `json:"project_id"`
 }
 
+// swagger:parameters getTemplate getTemplateUpgradeNotes
+type getTemplatePathParams struct {
+	// The project id
+	// in: path
+	// required: true
+	// minimum: 1
+	ProjectID uint `json:"project_id"`
+
+	// The name of the template
+	// in: path
+	// required: true
+	// type: string
+	Name string `json:"name"`
+
+	// The version of the template
+	// in: path
+	// required: true
+	// type: string
+	Version string `json:"version"`
+}
+
 func NewV1ProjectScopedRegisterer(children ...*router.Registerer) *router.Registerer {
 	return &router.Registerer{
 		GetRoutes: GetV1ProjectScopedRoutes,
@@ -61,5 +86,185 @@ func getV1ProjectRoutes(
 
 	var routes []*router.Route
 
+	// GET /api/v1/projects/{project_id}/templates -> v1Template.NewTemplateListHandler
+	// swagger:operation GET /api/v1/projects/{project_id}/templates listTemplates
+	//
+	// Lists templates for a given `repo_url`.
+	//
+	// ---
+	// produces:
+	// - application/json
+	// summary: List templates
+	// tags:
+	// - Templates
+	// parameters:
+	//   - name: project_id
+	//   - name: repo_url
+	//     in: query
+	//     description: |
+	//       The full path (including scheme) of the Helm registry to list templates from.
+	//     type: string
+	// responses:
+	//   '200':
+	//     description: Successfully listed templates
+	//     schema:
+	//       $ref: '#/definitions/ListTemplatesResponse'
+	//   '400':
+	//     description: A malformed or bad request
+	//   '403':
+	//     description: Forbidden
+	listTemplatesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/templates", relPath),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listTemplatesRequest := v1Template.NewTemplateListHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listTemplatesEndpoint,
+		Handler:  listTemplatesRequest,
+		Router:   r,
+	})
+
+	// GET /api/v1/projects/{project_id}/templates/{name}/versions/{version} -> v1Template.NewTemplateGetHandler
+	// swagger:operation GET /api/v1/projects/{project_id}/templates/{name}/versions/{version} getTemplate
+	//
+	// Retrieves a given template by a `name` and a `version`
+	//
+	// ---
+	// produces:
+	// - application/json
+	// summary: Get template
+	// tags:
+	// - Templates
+	// parameters:
+	//   - name: project_id
+	//   - name: name
+	//   - name: version
+	//   - name: repo_url
+	//     in: query
+	//     description: |
+	//       The full path (including scheme) of the Helm registry to list templates from.
+	//     type: string
+	// responses:
+	//   '200':
+	//     description: Successfully got the template
+	//     schema:
+	//       $ref: '#/definitions/GetTemplateResponse'
+	//   '400':
+	//     description: A malformed or bad request
+	//   '403':
+	//     description: Forbidden
+	getTemplateEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/templates/{%s}/versions/{%s}",
+					relPath,
+					types.URLParamTemplateName,
+					types.URLParamTemplateVersion,
+				),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	getTemplateRequest := v1Template.NewTemplateGetHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getTemplateEndpoint,
+		Handler:  getTemplateRequest,
+		Router:   r,
+	})
+
+	// GET /api/v1/projects/{project_id}/templates/{name}/versions/{version}/upgrade_notes -> v1Template.NewTemplateGetUpgradeNotesHandler
+	// swagger:operation GET /api/v1/projects/{project_id}/templates/{name}/versions/{version}/upgrade_notes getTemplateUpgradeNotes
+	//
+	// Retrieves a given template by a `name` and a `version`
+	//
+	// ---
+	// produces:
+	// - application/json
+	// summary: Get template upgrade notes
+	// tags:
+	// - Templates
+	// parameters:
+	//   - name: project_id
+	//   - name: name
+	//   - name: version
+	//   - name: prev_version
+	//     in: query
+	//     description: |
+	//       The previous version of the templates to generate upgrade notes from.
+	//     type: string
+	//   - name: repo_url
+	//     in: query
+	//     description: |
+	//       The full path (including scheme) of the Helm registry to list templates from.
+	//     type: string
+	// responses:
+	//   '200':
+	//     description: Successfully got the upgrade notes
+	//     schema:
+	//       $ref: '#/definitions/GetTemplateUpgradeNotesResponse'
+	//   '400':
+	//     description: A malformed or bad request
+	//   '403':
+	//     description: Forbidden
+	getTemplateUpgradeNotesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"/templates/{%s}/versions/{%s}/upgrade_notes",
+					types.URLParamTemplateName,
+					types.URLParamTemplateVersion,
+				),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	getTemplateUpgradeNotesRequest := v1Template.NewTemplateGetUpgradeNotesHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getTemplateUpgradeNotesEndpoint,
+		Handler:  getTemplateUpgradeNotesRequest,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 2 - 2
api/types/cluster.go

@@ -228,8 +228,8 @@ type CreateNamespaceRequest struct {
 	// example: sampleNS
 	Name string `json:"name" form:"required"`
 
-	// annotations for the kubernetes namespace, if any
-	Annotations map[string]string `json:"annotations,omitempty"`
+	// labels for the kubernetes namespace, if any
+	Labels map[string]string `json:"labels,omitempty"`
 }
 
 type GetTemporaryKubeconfigResponse struct {

+ 21 - 28
api/types/environment.go

@@ -16,15 +16,17 @@ type Environment struct {
 	DeploymentCount      uint              `json:"deployment_count"`
 	LastDeploymentStatus string            `json:"last_deployment_status"`
 	NewCommentsDisabled  bool              `json:"new_comments_disabled"`
-	NamespaceAnnotations map[string]string `json:"namespace_annotations,omitempty"`
+	NamespaceLabels      map[string]string `json:"namespace_labels,omitempty"`
+	GitDeployBranches    []string          `json:"git_deploy_branches"`
 }
 
 type CreateEnvironmentRequest struct {
-	Name                 string            `json:"name" form:"required"`
-	Mode                 string            `json:"mode" form:"oneof=auto manual" default:"manual"`
-	DisableNewComments   bool              `json:"disable_new_comments"`
-	GitRepoBranches      []string          `json:"git_repo_branches"`
-	NamespaceAnnotations map[string]string `json:"namespace_annotations"`
+	Name               string            `json:"name" form:"required"`
+	Mode               string            `json:"mode" form:"oneof=auto manual" default:"manual"`
+	DisableNewComments bool              `json:"disable_new_comments"`
+	GitRepoBranches    []string          `json:"git_repo_branches"`
+	NamespaceLabels    map[string]string `json:"namespace_labels"`
+	GitDeployBranches  []string          `json:"git_deploy_branches"`
 }
 
 type GitHubMetadata struct {
@@ -72,7 +74,7 @@ type CreateDeploymentRequest struct {
 	*GitHubMetadata
 
 	Namespace     string `json:"namespace" form:"required"`
-	PullRequestID uint   `json:"pull_request_id" form:"required"`
+	PullRequestID uint   `json:"pull_request_id"`
 }
 
 type SuccessfullyDeployedResource struct {
@@ -84,18 +86,14 @@ type FinalizeDeploymentRequest struct {
 	SuccessfulResources []*SuccessfullyDeployedResource `json:"successful_resources"`
 	Subdomain           string                          `json:"subdomain"`
 	PRNumber            uint                            `json:"pr_number"`
-
-	// legacy usage for backwards compatibility
-	Namespace string `json:"namespace"`
+	Namespace           string                          `json:"namespace"`
 }
 
 type FinalizeDeploymentWithErrorsRequest struct {
 	SuccessfulResources []*SuccessfullyDeployedResource `json:"successful_resources"`
 	Errors              map[string]string               `json:"errors" form:"required"`
 	PRNumber            uint                            `json:"pr_number"`
-
-	// legacy usage for backwards compatibility
-	Namespace string `json:"namespace"`
+	Namespace           string                          `json:"namespace"`
 }
 
 type UpdateDeploymentRequest struct {
@@ -104,9 +102,7 @@ type UpdateDeploymentRequest struct {
 	PRBranchFrom string `json:"gh_pr_branch_from" form:"required"`
 	CommitSHA    string `json:"commit_sha" form:"required"`
 	PRNumber     uint   `json:"pr_number"`
-
-	// legacy usage for backwards compatibility
-	Namespace string `json:"namespace"`
+	Namespace    string `json:"namespace"`
 }
 
 type ListDeploymentRequest struct {
@@ -119,9 +115,7 @@ type UpdateDeploymentStatusRequest struct {
 	PRBranchFrom string `json:"gh_pr_branch_from" form:"required"`
 	Status       string `json:"status" form:"required,oneof=created creating inactive failed"`
 	PRNumber     uint   `json:"pr_number"`
-
-	// legacy usage for backwards compatibility
-	Namespace string `json:"namespace"`
+	Namespace    string `json:"namespace"`
 }
 
 type DeleteDeploymentRequest struct {
@@ -129,11 +123,9 @@ type DeleteDeploymentRequest struct {
 }
 
 type GetDeploymentRequest struct {
-	DeploymentID uint `schema:"id"`
-	PRNumber     uint `schema:"pr_number"`
-
-	// legacy usage for backwards compatibility
-	Namespace string `schema:"namespace"`
+	DeploymentID uint   `schema:"id"`
+	PRNumber     uint   `schema:"pr_number"`
+	Namespace    string `schema:"namespace"`
 }
 
 type PullRequest struct {
@@ -160,8 +152,9 @@ type ValidatePorterYAMLResponse struct {
 }
 
 type UpdateEnvironmentSettingsRequest struct {
-	Mode                 string            `json:"mode" form:"oneof=auto manual"`
-	DisableNewComments   bool              `json:"disable_new_comments"`
-	GitRepoBranches      []string          `json:"git_repo_branches"`
-	NamespaceAnnotations map[string]string `json:"namespace_annotations"`
+	Mode               string            `json:"mode" form:"oneof=auto manual"`
+	DisableNewComments bool              `json:"disable_new_comments"`
+	GitRepoBranches    []string          `json:"git_repo_branches"`
+	NamespaceLabels    map[string]string `json:"namespace_labels"`
+	GitDeployBranches  []string          `json:"git_deploy_branches"`
 }

+ 17 - 5
api/types/template.go

@@ -19,14 +19,24 @@ type ListTemplatesRequest struct {
 }
 
 type PorterTemplateSimple struct {
-	Name        string   `json:"name"`
-	Versions    []string `json:"versions"`
-	Description string   `json:"description"`
-	Icon        string   `json:"icon"`
-	RepoURL     string   `json:"repo_url,omitempty"`
+	// The name of the template
+	Name string `json:"name"`
+
+	// The list of valid versions for the template
+	Versions []string `json:"versions"`
+
+	// A description for the template
+	Description string `json:"description"`
+
+	// An image URI for the icon
+	Icon string `json:"icon"`
+
+	// The repo URL for the template
+	RepoURL string `json:"repo_url,omitempty"`
 }
 
 // ListTemplatesResponse is how a chart gets displayed when listed
+// swagger:model ListTemplatesResponse
 type ListTemplatesResponse []PorterTemplateSimple
 
 type GetTemplateRequest struct {
@@ -34,6 +44,7 @@ type GetTemplateRequest struct {
 }
 
 // GetTemplateResponse is a chart with detailed information and a form for reading
+// swagger:model GetTemplateResponse
 type GetTemplateResponse struct {
 	Markdown string                 `json:"markdown"`
 	Metadata *chart.Metadata        `json:"metadata"`
@@ -47,4 +58,5 @@ type GetTemplateUpgradeNotesRequest struct {
 	PrevVersion string `schema:"prev_version"`
 }
 
+// swagger:model GetTemplateUpgradeNotesResponse
 type GetTemplateUpgradeNotesResponse upgrade.UpgradeFile

+ 143 - 74
cli/cmd/apply.go

@@ -239,21 +239,21 @@ func NewDeployDriver(resource *switchboardModels.Resource, opts *drivers.SharedD
 		output:      make(map[string]interface{}),
 	}
 
-	source, err := preview.GetSource(resource.Name, resource.Source)
+	target, err := preview.GetTarget(resource.Name, resource.Target)
 
 	if err != nil {
 		return nil, err
 	}
 
-	driver.source = source
+	driver.target = target
 
-	target, err := preview.GetTarget(resource.Name, resource.Target)
+	source, err := preview.GetSource(target.Project, resource.Name, resource.Source)
 
 	if err != nil {
 		return nil, err
 	}
 
-	driver.target = target
+	driver.source = source
 
 	return driver, nil
 }
@@ -754,6 +754,10 @@ func NewDeploymentHook(client *api.Client, resourceGroup *switchboardTypes.Resou
 	return res, nil
 }
 
+func (t *DeploymentHook) isBranchDeploy() bool {
+	return t.branchFrom != "" && t.branchInto != "" && t.branchFrom == t.branchInto
+}
+
 func (t *DeploymentHook) PreApply() error {
 	if isSystemNamespace(t.namespace) {
 		color.New(color.FgYellow).Printf("attempting to deploy to system namespace '%s'\n", t.namespace)
@@ -784,6 +788,10 @@ func (t *DeploymentHook) PreApply() error {
 		return fmt.Errorf("could not find environment for deployment")
 	}
 
+	if t.isBranchDeploy() {
+		t.namespace = preview.GetNamespaceForBranchDeploy(t.branchFrom, t.repoOwner, t.repoName)
+	}
+
 	nsList, err := t.client.GetK8sNamespaces(
 		context.Background(), t.projectID, t.clusterID,
 	)
@@ -811,8 +819,8 @@ func (t *DeploymentHook) PreApply() error {
 			Name: t.namespace,
 		}
 
-		if len(deplEnv.NamespaceAnnotations) > 0 {
-			createNS.Annotations = deplEnv.NamespaceAnnotations
+		if len(deplEnv.NamespaceLabels) > 0 {
+			createNS.Labels = deplEnv.NamespaceLabels
 		}
 
 		// create the new namespace
@@ -826,51 +834,72 @@ func (t *DeploymentHook) PreApply() error {
 		}
 	}
 
-	// attempt to read the deployment -- if it doesn't exist, create it
-	_, err = t.client.GetDeployment(
-		context.Background(),
-		t.projectID, t.clusterID, t.envID,
-		&types.GetDeploymentRequest{
-			PRNumber: t.prID,
-		},
-	)
+	var deplErr error
+
+	if t.isBranchDeploy() {
+		_, deplErr = t.client.GetDeployment(
+			context.Background(),
+			t.projectID, t.clusterID, t.envID,
+			&types.GetDeploymentRequest{
+				Namespace: t.namespace,
+			},
+		)
+	} else {
+		_, deplErr = t.client.GetDeployment(
+			context.Background(),
+			t.projectID, t.clusterID, t.envID,
+			&types.GetDeploymentRequest{
+				PRNumber: t.prID,
+			},
+		)
+	}
 
-	if err != nil && strings.Contains(err.Error(), "not found") {
+	if deplErr != nil && strings.Contains(deplErr.Error(), "not found") {
 		// in this case, create the deployment
+		createReq := &types.CreateDeploymentRequest{
+			Namespace:     t.namespace,
+			PullRequestID: t.prID,
+			CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
+				ActionID: t.actionID,
+			},
+			GitHubMetadata: &types.GitHubMetadata{
+				PRName:       t.prName,
+				RepoName:     t.repoName,
+				RepoOwner:    t.repoOwner,
+				CommitSHA:    t.commitSHA,
+				PRBranchFrom: t.branchFrom,
+				PRBranchInto: t.branchInto,
+			},
+		}
+
+		if t.isBranchDeploy() {
+			createReq.PullRequestID = 0
+		}
+
 		_, err = t.client.CreateDeployment(
 			context.Background(),
 			t.projectID, t.gitInstallationID, t.clusterID,
-			t.repoOwner, t.repoName,
-			&types.CreateDeploymentRequest{
-				Namespace:     t.namespace,
-				PullRequestID: t.prID,
-				CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
-					ActionID: t.actionID,
-				},
-				GitHubMetadata: &types.GitHubMetadata{
-					PRName:       t.prName,
-					RepoName:     t.repoName,
-					RepoOwner:    t.repoOwner,
-					CommitSHA:    t.commitSHA,
-					PRBranchFrom: t.branchFrom,
-					PRBranchInto: t.branchInto,
-				},
-			},
+			t.repoOwner, t.repoName, createReq,
 		)
 	} else if err == nil {
+		updateReq := &types.UpdateDeploymentRequest{
+			Namespace: t.namespace,
+			PRNumber:  t.prID,
+			CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
+				ActionID: t.actionID,
+			},
+			PRBranchFrom: t.branchFrom,
+			CommitSHA:    t.commitSHA,
+		}
+
+		if t.isBranchDeploy() {
+			updateReq.PRNumber = 0
+		}
+
 		_, err = t.client.UpdateDeployment(
 			context.Background(),
 			t.projectID, t.gitInstallationID, t.clusterID,
-			t.repoOwner, t.repoName,
-			&types.UpdateDeploymentRequest{
-				Namespace: t.namespace,
-				PRNumber:  t.prID,
-				CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
-					ActionID: t.actionID,
-				},
-				PRBranchFrom: t.branchFrom,
-				CommitSHA:    t.commitSHA,
-			},
+			t.repoOwner, t.repoName, updateReq,
 		)
 	}
 
@@ -953,12 +982,17 @@ func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
 	}
 
 	req := &types.FinalizeDeploymentRequest{
-		PRNumber:  t.prID,
 		Subdomain: strings.Join(subdomains, ", "),
 	}
 
+	if t.isBranchDeploy() {
+		req.Namespace = t.namespace
+	} else {
+		req.PRNumber = t.prID
+	}
+
 	for _, res := range t.resourceGroup.Resources {
-		releaseType := getReleaseType(res)
+		releaseType := getReleaseType(t.projectID, res)
 		releaseName := getReleaseName(res)
 
 		if releaseType != "" && releaseName != "" {
@@ -980,54 +1014,89 @@ func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
 }
 
 func (t *DeploymentHook) OnError(error) {
+	var deplErr error
+
+	if t.isBranchDeploy() {
+		_, deplErr = t.client.GetDeployment(
+			context.Background(),
+			t.projectID, t.clusterID, t.envID,
+			&types.GetDeploymentRequest{
+				Namespace: t.namespace,
+			},
+		)
+	} else {
+		_, deplErr = t.client.GetDeployment(
+			context.Background(),
+			t.projectID, t.clusterID, t.envID,
+			&types.GetDeploymentRequest{
+				PRNumber: t.prID,
+			},
+		)
+	}
+
 	// if the deployment exists, throw an error for that deployment
-	_, err := t.client.GetDeployment(
-		context.Background(),
-		t.projectID, t.clusterID, t.envID,
-		&types.GetDeploymentRequest{
-			PRNumber: t.prID,
-		},
-	)
+	if deplErr == nil {
+		req := &types.UpdateDeploymentStatusRequest{
+			CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
+				ActionID: t.actionID,
+			},
+			PRBranchFrom: t.branchFrom,
+			Status:       string(types.DeploymentStatusFailed),
+		}
+
+		if t.isBranchDeploy() {
+			req.Namespace = t.namespace
+		} else {
+			req.PRNumber = t.prID
+		}
 
-	if err == nil {
 		// FIXME: try to use the error with a custom logger
 		t.client.UpdateDeploymentStatus(
 			context.Background(),
 			t.projectID, t.gitInstallationID, t.clusterID,
-			t.repoOwner, t.repoName,
-			&types.UpdateDeploymentStatusRequest{
-				PRNumber: t.prID,
-				CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
-					ActionID: t.actionID,
-				},
-				PRBranchFrom: t.branchFrom,
-				Status:       string(types.DeploymentStatusFailed),
-			},
+			t.repoOwner, t.repoName, req,
 		)
 	}
 }
 
 func (t *DeploymentHook) OnConsolidatedErrors(allErrors map[string]error) {
-	// if the deployment exists, throw an error for that deployment
-	_, getDeplErr := t.client.GetDeployment(
-		context.Background(),
-		t.projectID, t.clusterID, t.envID,
-		&types.GetDeploymentRequest{
-			PRNumber: t.prID,
-		},
-	)
+	var deplErr error
 
-	if getDeplErr == nil {
+	if t.isBranchDeploy() {
+		_, deplErr = t.client.GetDeployment(
+			context.Background(),
+			t.projectID, t.clusterID, t.envID,
+			&types.GetDeploymentRequest{
+				Namespace: t.namespace,
+			},
+		)
+	} else {
+		_, deplErr = t.client.GetDeployment(
+			context.Background(),
+			t.projectID, t.clusterID, t.envID,
+			&types.GetDeploymentRequest{
+				PRNumber: t.prID,
+			},
+		)
+	}
+
+	// if the deployment exists, throw an error for that deployment
+	if deplErr == nil {
 		req := &types.FinalizeDeploymentWithErrorsRequest{
-			PRNumber: t.prID,
-			Errors:   make(map[string]string),
+			Errors: make(map[string]string),
+		}
+
+		if t.isBranchDeploy() {
+			req.Namespace = t.namespace
+		} else {
+			req.PRNumber = t.prID
 		}
 
 		for _, res := range t.resourceGroup.Resources {
 			if _, ok := allErrors[res.Name]; !ok {
 				req.SuccessfulResources = append(req.SuccessfulResources, &types.SuccessfullyDeployedResource{
 					ReleaseName: getReleaseName(res),
-					ReleaseType: getReleaseType(res),
+					ReleaseType: getReleaseType(t.projectID, res),
 				})
 			}
 		}
@@ -1150,10 +1219,10 @@ func getReleaseName(res *switchboardTypes.Resource) string {
 	return res.Name
 }
 
-func getReleaseType(res *switchboardTypes.Resource) string {
+func getReleaseType(projectID uint, res *switchboardTypes.Resource) string {
 	// can ignore the error because this method is called once
 	// GetSource has alrealy been called and validated previously
-	source, _ := preview.GetSource(res.Name, res.Source)
+	source, _ := preview.GetSource(projectID, res.Name, res.Source)
 
 	if source != nil && source.Name != "" {
 		return source.Name

+ 4 - 2
cli/cmd/deploy/create.go

@@ -451,6 +451,7 @@ func (c *CreateAgent) GetImageRepoURL(name, namespace string) (uint, string, err
 func (c *CreateAgent) GetLatestTemplateVersion(templateName string) (string, error) {
 	resp, err := c.Client.ListTemplates(
 		context.Background(),
+		c.CreateOpts.ProjectID,
 		&types.ListTemplatesRequest{},
 	)
 
@@ -478,9 +479,10 @@ func (c *CreateAgent) GetLatestTemplateVersion(templateName string) (string, err
 
 // GetLatestTemplateDefaultValues gets the default config (`values.yaml`) set for a specific
 // template.
-func (c *CreateAgent) GetLatestTemplateDefaultValues(templateName, templateVersion string) (map[string]interface{}, error) {
+func (c *CreateAgent) GetLatestTemplateDefaultValues(projectID uint, templateName, templateVersion string) (map[string]interface{}, error) {
 	chart, err := c.Client.GetTemplate(
 		context.Background(),
+		projectID,
 		templateName,
 		templateVersion,
 		&types.GetTemplateRequest{},
@@ -502,7 +504,7 @@ func (c *CreateAgent) GetMergedValues(overrideValues map[string]interface{}) (st
 	}
 
 	// get the values of the template
-	values, err := c.GetLatestTemplateDefaultValues(c.CreateOpts.Kind, latestVersion)
+	values, err := c.GetLatestTemplateDefaultValues(c.CreateOpts.ProjectID, c.CreateOpts.Kind, latestVersion)
 
 	if err != nil {
 		return "", nil, err

+ 6 - 4
cli/cmd/preview/build_image_driver.go

@@ -33,19 +33,21 @@ func NewBuildDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (
 		output:      make(map[string]interface{}),
 	}
 
-	source, err := GetSource(resource.Name, resource.Source)
+	target, err := GetTarget(resource.Name, resource.Target)
+
 	if err != nil {
 		return nil, err
 	}
 
-	driver.source = source
+	driver.target = target
+
+	source, err := GetSource(target.Project, resource.Name, resource.Source)
 
-	target, err := GetTarget(resource.Name, resource.Target)
 	if err != nil {
 		return nil, err
 	}
 
-	driver.target = target
+	driver.source = source
 
 	return driver, nil
 }

+ 6 - 4
cli/cmd/preview/update_config_driver.go

@@ -34,19 +34,21 @@ func NewUpdateConfigDriver(resource *models.Resource, opts *drivers.SharedDriver
 		output:      make(map[string]interface{}),
 	}
 
-	source, err := GetSource(resource.Name, resource.Source)
+	target, err := GetTarget(resource.Name, resource.Target)
+
 	if err != nil {
 		return nil, err
 	}
 
-	driver.source = source
+	driver.target = target
+
+	source, err := GetSource(driver.target.Project, resource.Name, resource.Source)
 
-	target, err := GetTarget(resource.Name, resource.Target)
 	if err != nil {
 		return nil, err
 	}
 
-	driver.target = target
+	driver.source = source
 
 	return driver, nil
 }

+ 37 - 6
cli/cmd/preview/utils.go

@@ -5,13 +5,14 @@ import (
 	"fmt"
 	"os"
 	"strconv"
+	"strings"
 
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/internal/integrations/preview"
 )
 
-func GetSource(resourceName string, input map[string]interface{}) (*preview.Source, error) {
+func GetSource(projectID uint, resourceName string, input map[string]interface{}) (*preview.Source, error) {
 	output := &preview.Source{}
 
 	// first read from env vars
@@ -64,7 +65,7 @@ func GetSource(resourceName string, input map[string]interface{}) (*preview.Sour
 	if output.Repo == "" {
 		output.Repo = "https://charts.getporter.dev"
 
-		values, err := existsInRepo(output.Name, output.Version, output.Repo)
+		values, err := existsInRepo(projectID, output.Name, output.Version, output.Repo)
 
 		if err == nil {
 			// found in "https://charts.getporter.dev"
@@ -75,7 +76,7 @@ func GetSource(resourceName string, input map[string]interface{}) (*preview.Sour
 
 		output.Repo = "https://chart-addons.getporter.dev"
 
-		values, err = existsInRepo(output.Name, output.Version, output.Repo)
+		values, err = existsInRepo(projectID, output.Name, output.Version, output.Repo)
 
 		if err == nil {
 			// found in https://chart-addons.getporter.dev
@@ -87,7 +88,7 @@ func GetSource(resourceName string, input map[string]interface{}) (*preview.Sour
 			"'https://charts.getporter.dev' or 'https://chart-addons.getporter.dev'", resourceName)
 	} else {
 		// we look in the passed-in repo
-		values, err := existsInRepo(output.Name, output.Version, output.Repo)
+		values, err := existsInRepo(projectID, output.Name, output.Version, output.Repo)
 
 		if err == nil {
 			output.SourceValues = values
@@ -119,7 +120,7 @@ func GetTarget(resourceName string, input map[string]interface{}) (*preview.Targ
 		output.Cluster = uint(cluster)
 	}
 
-	output.Namespace = os.Getenv("PORTER_NAMESPACE")
+	output.Namespace = getNamespace()
 
 	// next, check for values in the YAML file
 	if output.Project == 0 {
@@ -175,9 +176,38 @@ func GetTarget(resourceName string, input map[string]interface{}) (*preview.Targ
 	return output, nil
 }
 
-func existsInRepo(name, version, url string) (map[string]interface{}, error) {
+func GetNamespaceForBranchDeploy(branch, owner, name string) string {
+	namespace := fmt.Sprintf("previewbranch-%s-%s-%s", branch,
+		strings.ReplaceAll(strings.ToLower(owner), "_", "-"),
+		strings.ReplaceAll(strings.ToLower(name), "_", "-"))
+
+	if len(namespace) > 63 {
+		namespace = namespace[:63] // Kubernetes' DNS 1123 label requirement
+	}
+
+	return namespace
+}
+
+func getNamespace() string {
+	if owner, ok := os.LookupEnv("PORTER_REPO_OWNER"); ok {
+		if repo, ok := os.LookupEnv("PORTER_REPO_NAME"); ok {
+			if branchFrom, ok := os.LookupEnv("PORTER_BRANCH_FROM"); ok {
+				if branchInto, ok := os.LookupEnv("PORTER_BRANCH_INTO"); ok {
+					if branchInto == branchFrom { // branch deploy
+						return GetNamespaceForBranchDeploy(branchInto, owner, repo)
+					}
+				}
+			}
+		}
+	}
+
+	return os.Getenv("PORTER_NAMESPACE")
+}
+
+func existsInRepo(projectID uint, name, version, url string) (map[string]interface{}, error) {
 	chart, err := config.GetAPIClient().GetTemplate(
 		context.Background(),
+		projectID,
 		name, version,
 		&types.GetTemplateRequest{
 			TemplateGetBaseRequest: types.TemplateGetBaseRequest{
@@ -185,6 +215,7 @@ func existsInRepo(name, version, url string) (map[string]interface{}, error) {
 			},
 		},
 	)
+
 	if err != nil {
 		return nil, err
 	}

+ 3 - 2
dashboard/src/main/home/cluster-dashboard/dashboard/ClusterSettings.tsx

@@ -304,8 +304,9 @@ const ClusterSettings: React.FC = () => {
         <DarkMatter />
         {keyRotationSection}
         <DarkMatter />
-        {renameClusterSection}
-        <DarkMatter />
+        {/* Disabled this field due to https://discord.com/channels/542888846271184896/856554532972134420/1042497537912864788 */}
+        {/* {renameClusterSection}
+        <DarkMatter /> */}
         <Heading>Delete Cluster</Heading>
         {helperText}
         <Button

+ 6 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -728,7 +728,11 @@ const ExpandedChart: React.FC<Props> = (props) => {
 
   // Check if porter agent is installed. If not installed hide the `Logs` component
   useEffect(() => {
-    if (!currentCluster.agent_integration_enabled) {
+    if (
+      !currentCluster.agent_integration_enabled ||
+      // If chart is an add on, we don't need to check if agent is installed
+      !["web", "worker", "job"].includes(currentChart?.chart?.metadata?.name)
+    ) {
       return;
     }
 
@@ -757,7 +761,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
           );
         }
       });
-  }, []);
+  }, [currentChart]);
 
   useEffect(() => {
     if (logData.revision) {

+ 32 - 23
dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx

@@ -15,9 +15,7 @@ import PullRequestIcon from "assets/pull_request_icon.svg";
 import CheckboxRow from "components/form-components/CheckboxRow";
 import BranchFilterSelector from "./components/BranchFilterSelector";
 import Helper from "components/form-components/Helper";
-import NamespaceAnnotations, {
-  KeyValueType,
-} from "./components/NamespaceAnnotations";
+import NamespaceLabels, { KeyValueType } from "./components/NamespaceLabels";
 
 const ConnectNewRepo: React.FC = () => {
   const { currentProject, currentCluster, setCurrentError } = useContext(
@@ -42,17 +40,16 @@ const ConnectNewRepo: React.FC = () => {
   });
 
   // Branch selector data
-  const [selectedBranches, setSelectedBranches] = useState<string[]>([]);
+  const [baseBranches, setBaseBranches] = useState<string[]>([]);
+  const [deployBranches, setDeployBranches] = useState<string[]>([]);
   const [availableBranches, setAvailableBranches] = useState<string[]>([]);
   const [isLoadingBranches, setIsLoadingBranches] = useState(false);
 
   // Disable new comments data
   const [isNewCommentsDisabled, setIsNewCommentsDisabled] = useState(false);
 
-  // Namespace annotations
-  const [namespaceAnnotations, setNamespaceAnnotations] = useState<
-    KeyValueType[]
-  >([]);
+  // Namespace labels
+  const [namespaceLabels, setNamespaceLabels] = useState<KeyValueType[]>([]);
 
   useEffect(() => {
     api
@@ -117,11 +114,11 @@ const ConnectNewRepo: React.FC = () => {
 
   const addRepo = () => {
     let [owner, repoName] = repo.split("/");
-    let annotations: Record<string, string> = {};
+    const labels: Record<string, string> = {};
 
     setStatus("loading");
 
-    namespaceAnnotations
+    namespaceLabels
       .filter((elem: KeyValueType, index: number, self: KeyValueType[]) => {
         // remove any collisions that are duplicates
         let numCollisions = self.reduce((n, _elem: KeyValueType) => {
@@ -139,7 +136,7 @@ const ConnectNewRepo: React.FC = () => {
       })
       .forEach((elem: KeyValueType) => {
         if (elem.key !== "" && elem.value !== "") {
-          annotations[elem.key] = elem.value;
+          labels[elem.key] = elem.value;
         }
       });
 
@@ -150,8 +147,9 @@ const ConnectNewRepo: React.FC = () => {
           name: `preview`,
           mode: enableAutomaticDeployments ? "auto" : "manual",
           disable_new_comments: isNewCommentsDisabled,
-          git_repo_branches: selectedBranches,
-          namespace_annotations: annotations,
+          git_repo_branches: baseBranches,
+          namespace_labels: labels,
+          git_deploy_branches: deployBranches,
         },
         {
           project_id: currentProject.id,
@@ -258,6 +256,17 @@ const ConnectNewRepo: React.FC = () => {
         />
       </CheckboxWrapper>
 
+      <Heading>Deploy from branches</Heading>
+      <Helper>
+        Choose the list of branches that you want to deploy changes from.
+      </Helper>
+      <BranchFilterSelector
+        onChange={setDeployBranches}
+        options={availableBranches}
+        value={deployBranches}
+        showLoading={isLoadingBranches}
+      />
+
       <Heading>Select allowed branches</Heading>
       <Helper>
         If the pull request has a base branch included in this list, it will be
@@ -266,25 +275,25 @@ const ConnectNewRepo: React.FC = () => {
         (Leave empty to allow all branches)
       </Helper>
       <BranchFilterSelector
-        onChange={setSelectedBranches}
+        onChange={setBaseBranches}
         options={availableBranches}
-        value={selectedBranches}
+        value={baseBranches}
         showLoading={isLoadingBranches}
       />
 
-      <Heading>Namespace annotations</Heading>
+      <Heading>Namespace labels</Heading>
       <Helper>
-        Custom annotations to be injected into the Kubernetes namespace created
-        for each deployment.
+        Custom labels to be injected into the Kubernetes namespace created for
+        each deployment.
       </Helper>
-      <NamespaceAnnotations
-        values={namespaceAnnotations}
+      <NamespaceLabels
+        values={namespaceLabels}
         setValues={(x: KeyValueType[]) => {
-          let annotations: KeyValueType[] = [];
+          let labels: KeyValueType[] = [];
           x.forEach((entry) => {
-            annotations.push({ key: entry.key, value: entry.value });
+            labels.push({ key: entry.key, value: entry.value });
           });
-          setNamespaceAnnotations(annotations);
+          setNamespaceLabels(labels);
         }}
       />
 

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/preview-environments/components/NamespaceAnnotations.tsx → dashboard/src/main/home/cluster-dashboard/preview-environments/components/NamespaceLabels.tsx

@@ -11,7 +11,7 @@ type PropsType = {
   setValues: (x: KeyValueType[]) => void;
 };
 
-const NamespaceAnnotations = ({ values, setValues }: PropsType) => {
+const NamespaceLabels = ({ values, setValues }: PropsType) => {
   useEffect(() => {
     if (!values) {
       setValues([]);
@@ -82,7 +82,7 @@ const NamespaceAnnotations = ({ values, setValues }: PropsType) => {
   );
 };
 
-export default NamespaceAnnotations;
+export default NamespaceLabels;
 
 const Spacer = styled.div`
   width: 10px;

+ 5 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentCard.tsx

@@ -193,6 +193,8 @@ const DeploymentCard: React.FC<{
     },
   ];
 
+  console.error(deployment, deployment.gh_pr_branch_from, deployment.gh_pr_branch_into, deployment.gh_pr_branch_from === deployment.gh_pr_branch_into);
+
   return (
     <DeploymentCardWrapper
       to={`/preview-environments/details/${deployment.id}?environment_id=${deployment.environment_id}`}
@@ -216,7 +218,9 @@ const DeploymentCard: React.FC<{
               {deployment.gh_pr_name}
             </StyledLink>
           </EllipsisTextWrapper>
-          {deployment.gh_pr_branch_from && deployment.gh_pr_branch_into ? (
+          {deployment.gh_pr_branch_from &&
+          deployment.gh_pr_branch_into &&
+          deployment.gh_pr_branch_from !== deployment.gh_pr_branch_into ? (
             <MergeInfoWrapper>
               <MergeInfo
                 onMouseOver={() => setShowMergeInfoTooltip(true)}

+ 44 - 35
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentSettings.tsx

@@ -18,9 +18,7 @@ import Banner from "components/Banner";
 import InputRow from "components/form-components/InputRow";
 import Modal from "main/home/modals/Modal";
 import { useRouting } from "shared/routing";
-import NamespaceAnnotations, {
-  KeyValueType,
-} from "../components/NamespaceAnnotations";
+import NamespaceLabels, { KeyValueType } from "../components/NamespaceLabels";
 import BranchFilterSelector from "../components/BranchFilterSelector";
 
 const EnvironmentSettings = () => {
@@ -32,7 +30,8 @@ const EnvironmentSettings = () => {
   const { currentProject, currentCluster, setCurrentError } = useContext(
     Context
   );
-  const [selectedBranches, setSelectedBranches] = useState([]);
+  const [baseBranches, setBaseBranches] = useState([]);
+  const [deployBranches, setDeployBranches] = useState([]);
   const [environment, setEnvironment] = useState<Environment>();
   const [saveStatus, setSaveStatus] = useState("");
   const [newCommentsDisabled, setNewCommentsDisabled] = useState(false);
@@ -40,9 +39,7 @@ const EnvironmentSettings = () => {
     deploymentMode,
     setDeploymentMode,
   ] = useState<EnvironmentDeploymentMode>("manual");
-  const [namespaceAnnotations, setNamespaceAnnotations] = useState<
-    KeyValueType[]
-  >([]);
+  const [namespaceLabels, setNamespaceLabels] = useState<KeyValueType[]>([]);
   const {
     environment_id: environmentId,
     repo_name: repoName,
@@ -68,21 +65,20 @@ const EnvironmentSettings = () => {
       );
 
       setEnvironment(environment);
-      setSelectedBranches(environment.git_repo_branches);
+      setBaseBranches(environment.git_repo_branches);
       setNewCommentsDisabled(environment.new_comments_disabled);
       setDeploymentMode(environment.mode);
+      setDeployBranches(environment.git_deploy_branches);
 
-      if (environment.namespace_annotations) {
-        const annotations: KeyValueType[] = [];
+      if (environment.namespace_labels) {
+        const labels: KeyValueType[] = Object.entries(
+          environment.namespace_labels
+        ).map(([key, value]) => ({
+          key,
+          value,
+        }));
 
-        Object.keys(environment.namespace_annotations).forEach((k) => {
-          annotations.push({
-            key: k,
-            value: environment.namespace_annotations[k],
-          });
-        });
-
-        setNamespaceAnnotations(annotations);
+        setNamespaceLabels(labels);
       }
     };
 
@@ -126,11 +122,11 @@ const EnvironmentSettings = () => {
   }, [environment]);
 
   const handleSave = async () => {
-    let annotations: Record<string, string> = {};
+    let labels: Record<string, string> = {};
 
     setSaveStatus("loading");
 
-    namespaceAnnotations
+    namespaceLabels
       .filter((elem: KeyValueType, index: number, self: KeyValueType[]) => {
         // remove any collisions that are duplicates
         let numCollisions = self.reduce((n, _elem: KeyValueType) => {
@@ -148,7 +144,7 @@ const EnvironmentSettings = () => {
       })
       .forEach((elem: KeyValueType) => {
         if (elem.key !== "" && elem.value !== "") {
-          annotations[elem.key] = elem.value;
+          labels[elem.key] = elem.value;
         }
       });
 
@@ -158,8 +154,9 @@ const EnvironmentSettings = () => {
         {
           mode: deploymentMode,
           disable_new_comments: newCommentsDisabled,
-          git_repo_branches: selectedBranches,
-          namespace_annotations: annotations,
+          git_repo_branches: baseBranches,
+          namespace_labels: labels,
+          git_deploy_branches: deployBranches,
         },
         {
           project_id: currentProject.id,
@@ -277,6 +274,17 @@ const EnvironmentSettings = () => {
           }
         />
         <Br />
+        <Heading>Deploy from branches</Heading>
+        <Helper>
+          Choose the list of branches that you want to deploy changes from.
+        </Helper>
+        <BranchFilterSelector
+          onChange={setDeployBranches}
+          options={availableBranches}
+          value={deployBranches}
+          showLoading={isLoadingBranches}
+        />
+        <Br />
         <Heading>Select allowed branches</Heading>
         <Helper>
           If the pull request has a base branch included in this list, it will
@@ -285,25 +293,26 @@ const EnvironmentSettings = () => {
           (Leave empty to allow all branches)
         </Helper>
         <BranchFilterSelector
-          onChange={setSelectedBranches}
+          onChange={setBaseBranches}
           options={availableBranches}
-          value={selectedBranches}
+          value={baseBranches}
           showLoading={isLoadingBranches}
         />
         <Br />
-        <Heading>Namespace annotations</Heading>
+        <Heading>Namespace labels</Heading>
         <Helper>
-          Custom annotations to be injected into the Kubernetes namespace
-          created for each deployment.
+          Custom labels to be injected into the Kubernetes namespace created for
+          each deployment.
         </Helper>
-        <NamespaceAnnotations
-          values={namespaceAnnotations}
+        <NamespaceLabels
+          values={namespaceLabels}
           setValues={(x: KeyValueType[]) => {
-            let annotations: KeyValueType[] = [];
-            x.forEach((entry) => {
-              annotations.push({ key: entry.key, value: entry.value });
-            });
-            setNamespaceAnnotations(annotations);
+            const labels: KeyValueType[] = x.map((entry) => ({
+              key: entry.key,
+              value: entry.value,
+            }));
+
+            setNamespaceLabels(labels);
           }}
         />
         <SavePreviewEnvironmentSettings

+ 2 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/types.ts

@@ -44,7 +44,8 @@ export type Environment = {
   last_deployment_status: DeploymentStatusUnion;
   deployment_count: number;
   mode: EnvironmentDeploymentMode;
-  namespace_annotations: Record<string, string>;
+  namespace_labels: Record<string, string>;
+  git_deploy_branches: string[];
 };
 
 export type PullRequest = {

+ 4 - 2
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_TemplateSelector.tsx

@@ -11,7 +11,7 @@ import TitleSection from "components/TitleSection";
 import { Context } from "shared/Context";
 
 const TemplateSelector = () => {
-  const { capabilities } = useContext(Context);
+  const { capabilities, currentProject } = useContext(Context);
 
   const [templates, setTemplates] = useState<PorterTemplate[]>([]);
   const [selectedVersion, setSelectedVersion] = useState<{
@@ -28,7 +28,9 @@ const TemplateSelector = () => {
         {
           repo_url: capabilities?.default_app_helm_repo_url,
         },
-        {}
+        {
+          project_id: currentProject.id,
+        }
       );
       let sortedVersionData = res.data
         .map((template: PorterTemplate) => {

+ 6 - 2
dashboard/src/main/home/cluster-dashboard/stacks/components/NewAppResourceForm.tsx

@@ -64,7 +64,7 @@ const NewAppResourceForm = (props: {
     onSubmit,
   } = props;
 
-  const { currentCluster } = useContext(Context);
+  const { currentProject, currentCluster } = useContext(Context);
 
   const [hasError, setHasError] = useState(false);
   const [isLoading, setIsLoading] = useState(true);
@@ -209,7 +209,11 @@ const NewAppResourceForm = (props: {
       .getTemplateInfo<ExpandedPorterTemplate>(
         "<token>",
         {},
-        { name: templateInfo.name, version: templateInfo.version }
+        {
+          project_id: currentProject.id,
+          name: templateInfo.name,
+          version: templateInfo.version,
+        }
       )
       .then((res) => {
         if (isSubscribed) {

+ 4 - 2
dashboard/src/main/home/cluster-dashboard/stacks/launch/components/AddResourceButton.tsx

@@ -11,7 +11,7 @@ import styled from "styled-components";
 import { Context } from "shared/Context";
 
 export const AddResourceButton = () => {
-  const { capabilities } = useContext(Context);
+  const { currentProject, capabilities } = useContext(Context);
   const [templates, setTemplates] = useState<PorterTemplate[]>([]);
   const [currentTemplate, setCurrentTemplate] = useState<PorterTemplate>();
   const [currentVersion, setCurrentVersion] = useState("");
@@ -23,7 +23,9 @@ export const AddResourceButton = () => {
         {
           repo_url: capabilities?.default_app_helm_repo_url,
         },
-        {}
+        {
+          project_id: currentProject.id,
+        }
       );
       let sortedVersionData = res.data
         .map((template: PorterTemplate) => {

+ 31 - 4
dashboard/src/main/home/launch/Launch.tsx

@@ -45,6 +45,8 @@ type StateType = {
   tabOptions: TabOption[];
 };
 class Templates extends Component<PropsType, StateType> {
+  private previousContext: any;
+
   state = {
     currentTemplate: null as PorterTemplate | null,
     form: null as any,
@@ -58,7 +60,28 @@ class Templates extends Component<PropsType, StateType> {
     tabOptions: initialTabOptions,
   };
 
-  async componentDidMount() {
+  componentDidMount() {
+    this.previousContext = this.context;
+    this.setTemplatesAndRepos();
+  }
+
+  componentDidUpdate() {
+    // if project ID has changed, load in a new set of templates
+    if (
+      this.context.currentProject?.id != this.previousContext.currentProject?.id
+    ) {
+      this.setTemplatesAndRepos();
+    }
+
+    this.previousContext = this.context;
+  }
+
+  setTemplatesAndRepos = async () => {
+    // if the project ID is not defined, return
+    if (!this.context.currentProject) {
+      return;
+    }
+
     let default_addon_helm_repo_url = this.context?.capabilities
       ?.default_addon_helm_repo_url;
     let default_app_helm_repo_url = this.context?.capabilities
@@ -69,7 +92,9 @@ class Templates extends Component<PropsType, StateType> {
         {
           repo_url: default_addon_helm_repo_url,
         },
-        {}
+        {
+          project_id: this.context.currentProject.id,
+        }
       );
       let sortedVersionData = res.data.map((template: any) => {
         let versions = template.versions.reverse();
@@ -96,7 +121,9 @@ class Templates extends Component<PropsType, StateType> {
         {
           repo_url: default_app_helm_repo_url,
         },
-        {}
+        {
+          project_id: this.context.currentProject.id,
+        }
       );
       let sortedVersionData = res.data.map((template: any) => {
         let versions = template.versions.reverse();
@@ -191,7 +218,7 @@ class Templates extends Component<PropsType, StateType> {
     } catch (error) {
       this.setState({ loading: false, error: true });
     }
-  }
+  };
 
   isTryingToClone = () => {
     const queryParams = getQueryParams(this.props);

+ 1 - 0
dashboard/src/main/home/launch/expanded-template/ExpandedTemplate.tsx

@@ -82,6 +82,7 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
 
       api
         .getTemplateInfo("<token>", params, {
+          project_id: this.context.currentProject.id,
           name: this.props.currentTemplate.name.toLowerCase().trim(),
           version: this.props.currentTemplate.currentVersion,
         })

+ 1 - 0
dashboard/src/main/home/modals/UpgradeChartModal.tsx

@@ -45,6 +45,7 @@ export default class UpgradeChartModal extends Component<PropsType, StateType> {
           prev_version: this.props.currentChart.chart.metadata.version,
         },
         {
+          project_id: this.context.currentProject.id,
           name: chartName,
           version: this.props.currentChart.latest_version,
         }

+ 14 - 8
dashboard/src/shared/api.tsx

@@ -145,7 +145,8 @@ const createEnvironment = baseApi<
     mode: "auto" | "manual";
     disable_new_comments: boolean;
     git_repo_branches: string[];
-    namespace_annotations: Record<string, string>;
+    namespace_labels: Record<string, string>;
+    git_deploy_branches: string[];
   },
   {
     project_id: number;
@@ -170,7 +171,8 @@ const updateEnvironment = baseApi<
     mode: "auto" | "manual";
     disable_new_comments: boolean;
     git_repo_branches: string[]; // Array with branch names
-    namespace_annotations: Record<string, string>;
+    namespace_labels: Record<string, string>;
+    git_deploy_branches: string[];
   },
   {
     project_id: number;
@@ -1228,9 +1230,9 @@ const getTemplateInfo = baseApi<
   {
     repo_url?: string;
   },
-  { name: string; version: string }
+  { project_id: number; name: string; version: string }
 >("GET", (pathParams) => {
-  return `/api/templates/${pathParams.name}/${pathParams.version}`;
+  return `/api/v1/projects/${pathParams.project_id}/templates/${pathParams.name}/versions/${pathParams.version}`;
 });
 
 const getTemplateUpgradeNotes = baseApi<
@@ -1238,17 +1240,21 @@ const getTemplateUpgradeNotes = baseApi<
     repo_url?: string;
     prev_version: string;
   },
-  { name: string; version: string }
+  { project_id: number; name: string; version: string }
 >("GET", (pathParams) => {
-  return `/api/templates/${pathParams.name}/${pathParams.version}/upgrade_notes`;
+  return `/api/v1/projects/${pathParams.project_id}/templates/${pathParams.name}/versions/${pathParams.version}/upgrade_notes`;
 });
 
 const getTemplates = baseApi<
   {
     repo_url?: string;
   },
-  {}
->("GET", "/api/templates");
+  {
+    project_id: number;
+  }
+>("GET", (pathParams) => {
+  return `/api/v1/projects/${pathParams.project_id}/templates`;
+});
 
 const getHelmRepos = baseApi<
   {},

+ 19 - 0
internal/helm/repo/repo.go

@@ -83,3 +83,22 @@ func (hr *HelmRepo) getChartBasic(
 
 	return loader.LoadChart(client, hr.RepoURL, chartName, chartVersion)
 }
+
+func ValidateRepoURL(
+	defaultAddonRepoURL, defaultAppRepoURL string,
+	hrs []*models.HelmRepo,
+	repo_url string,
+) bool {
+	if repo_url == defaultAddonRepoURL || repo_url == defaultAppRepoURL {
+		return true
+	}
+
+	// otherwise, iterate through helm repos
+	for _, hr := range hrs {
+		if hr.RepoURL == repo_url {
+			return true
+		}
+	}
+
+	return false
+}

+ 1 - 1
internal/integrations/ci/actions/preview.go

@@ -241,7 +241,7 @@ func getPreviewApplyActionYAML(opts *EnvOpts) ([]byte, error) {
 				"inputs": map[string]interface{}{
 					"pr_number": map[string]interface{}{
 						"description": "Pull request number",
-						"type":        "number",
+						"type":        "string",
 						"required":    true,
 					},
 					"pr_title": map[string]interface{}{

+ 3 - 3
internal/kubernetes/agent.go

@@ -616,7 +616,7 @@ func (a *Agent) ListNamespaces() (*v1.NamespaceList, error) {
 }
 
 // CreateNamespace creates a namespace with the given name.
-func (a *Agent) CreateNamespace(name string, annotations map[string]string) (*v1.Namespace, error) {
+func (a *Agent) CreateNamespace(name string, labels map[string]string) (*v1.Namespace, error) {
 	// check if namespace exists
 	checkNS, err := a.Clientset.CoreV1().Namespaces().Get(
 		context.TODO(),
@@ -666,8 +666,8 @@ func (a *Agent) CreateNamespace(name string, annotations map[string]string) (*v1
 		},
 	}
 
-	if len(annotations) > 0 {
-		namespace.SetAnnotations(annotations)
+	if len(labels) > 0 {
+		namespace.SetLabels(labels)
 	}
 
 	return a.Clientset.CoreV1().Namespaces().Create(

+ 21 - 8
internal/models/environment.go

@@ -23,7 +23,9 @@ type Environment struct {
 	Mode string
 
 	NewCommentsDisabled  bool
+	NamespaceLabels      []byte
 	NamespaceAnnotations []byte
+	GitDeployBranches    string
 
 	// WebhookID uniquely identifies the environment when other fields (project, cluster)
 	// aren't present
@@ -59,8 +61,8 @@ func (e *Environment) ToEnvironmentType() *types.Environment {
 		GitRepoOwner:      e.GitRepoOwner,
 		GitRepoName:       e.GitRepoName,
 
-		NewCommentsDisabled:  e.NewCommentsDisabled,
-		NamespaceAnnotations: make(map[string]string),
+		NewCommentsDisabled: e.NewCommentsDisabled,
+		NamespaceLabels:     make(map[string]string),
 
 		Name: e.Name,
 		Mode: e.Mode,
@@ -74,15 +76,23 @@ func (e *Environment) ToEnvironmentType() *types.Environment {
 		env.GitRepoBranches = []string{}
 	}
 
-	if len(e.NamespaceAnnotations) > 0 {
-		env.NamespaceAnnotations = make(map[string]string)
-		annotations := string(e.NamespaceAnnotations)
+	branches = getGitRepoBranches(e.GitDeployBranches)
 
-		for _, a := range strings.Split(annotations, ",") {
+	if len(branches) > 0 {
+		env.GitDeployBranches = branches
+	} else {
+		env.GitDeployBranches = []string{}
+	}
+
+	if len(e.NamespaceLabels) > 0 {
+		env.NamespaceLabels = make(map[string]string)
+		labels := string(e.NamespaceLabels)
+
+		for _, a := range strings.Split(labels, ",") {
 			k, v, found := strings.Cut(a, "=")
 
 			if found {
-				env.NamespaceAnnotations[k] = v
+				env.NamespaceLabels[k] = v
 			}
 		}
 	}
@@ -109,7 +119,6 @@ type Deployment struct {
 }
 
 func (d *Deployment) ToDeploymentType() *types.Deployment {
-
 	ghMetadata := &types.GitHubMetadata{
 		DeploymentID: d.GHDeploymentID,
 		PRName:       d.PRName,
@@ -132,3 +141,7 @@ func (d *Deployment) ToDeploymentType() *types.Deployment {
 		GitHubMetadata: ghMetadata,
 	}
 }
+
+func (d *Deployment) IsBranchDeploy() bool {
+	return d.PullRequestID == 0 && d.PRBranchFrom != "" && d.PRBranchInto != "" && d.PRBranchFrom == d.PRBranchInto
+}

+ 4 - 4
workers/jobs/helm_revisions_count_tracker.go

@@ -237,19 +237,19 @@ func (t *helmRevisionsCountTracker) Run() error {
 							continue
 						}
 
-						if len(revisions) <= 100 {
-							log.Printf("release %s of namespace %s in cluster ID %d has <= 100 revisions. "+
+						if len(revisions) <= 20 {
+							log.Printf("release %s of namespace %s in cluster ID %d has <= 20 revisions. "+
 								"skipping release...", rel.Name, ns.Name, cluster.ID)
 							continue
 						}
 
-						log.Printf("release %s of namespace %s in cluster ID %d has more than 100 revisions. attempting to "+
+						log.Printf("release %s of namespace %s in cluster ID %d has more than 20 revisions. attempting to "+
 							"delete the older ones.", rel.Name, ns.Name, cluster.ID)
 
 						// sort revisions from newest to oldest
 						releaseutil.Reverse(revisions, releaseutil.SortByRevision)
 
-						for i := 100; i < len(revisions); i += 1 {
+						for i := 20; i < len(revisions); i += 1 {
 							rev := revisions[i]
 
 							// store the revision in the s3 bucket before deleting it