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

Merge branch 'nico/por-409-build-settings-tab' of github.com:porter-dev/porter into nico/por-409-build-settings-tab

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

+ 17 - 0
api/client/deploy.go

@@ -107,3 +107,20 @@ func (c *Client) UpgradeRelease(
 		},
 	)
 }
+
+// DeleteRelease deletes a Porter release
+func (c *Client) DeleteRelease(
+	ctx context.Context,
+	projID, clusterID uint,
+	namespace, name string,
+) error {
+	return c.deleteRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/namespaces/%s/releases/%s/0",
+			projID, clusterID,
+			namespace, name,
+		),
+		nil,
+		nil,
+	)
+}

+ 15 - 29
api/server/handlers/environment/delete_deployment.go

@@ -10,7 +10,6 @@ import (
 	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"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"
@@ -40,33 +39,7 @@ func (c *DeleteDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	envID, reqErr := requestutils.GetURLParamUint(r, "environment_id")
-
-	if reqErr != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
-		return
-	}
-
-	// check that the environment belongs to the project and cluster IDs
-	env, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, envID)
-
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			c.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("environment id not found in cluster and project")))
-			return
-		}
-
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
-		return
-	}
-
-	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
-
-	if !ok {
-		return
-	}
-
-	prNumber, reqErr := requestutils.GetURLParamUint(r, "pr_number")
+	deplID, reqErr := requestutils.GetURLParamUint(r, "deployment_id")
 
 	if reqErr != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
@@ -74,7 +47,7 @@ func (c *DeleteDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	}
 
 	// read the deployment
-	depl, err := c.Repo().Environment().ReadDeploymentByGitDetails(envID, owner, name, prNumber)
+	depl, err := c.Repo().Environment().ReadDeploymentByID(project.ID, cluster.ID, deplID)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
@@ -99,6 +72,19 @@ func (c *DeleteDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		}
 	}
 
+	// check that the environment belongs to the project and cluster IDs
+	env, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, depl.EnvironmentID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("environment id not found in cluster and project")))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(reqErr))
+		return
+	}
+
 	client, err := getGithubClientFromEnvironment(c.Config(), env)
 
 	if err != nil {

+ 8 - 13
api/server/handlers/environment/list_deployments_by_cluster.go

@@ -9,6 +9,7 @@ import (
 	"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/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
@@ -140,23 +141,17 @@ func updateDeploymentWithGithubWorkflowRunStatus(
 	env *models.Environment,
 	deployment *types.Deployment,
 ) {
+	if deployment.Status == types.DeploymentStatusInactive {
+		return
+	}
+
 	client, err := getGithubClientFromEnvironment(config, env)
 
 	if err == nil {
-		workflowRuns, _, err := client.Actions.ListWorkflowRunsByFileName(
-			ctx, deployment.RepoOwner, deployment.RepoName,
-			fmt.Sprintf("porter_%s_env.yml", env.Name), &github.ListWorkflowRunsOptions{
-				Branch: deployment.PRBranchFrom,
-				ListOptions: github.ListOptions{
-					Page:    1,
-					PerPage: 1,
-				},
-			},
-		)
-
-		if err == nil && workflowRuns.GetTotalCount() > 0 {
-			latestWorkflowRun := workflowRuns.WorkflowRuns[0]
+		latestWorkflowRun, err := commonutils.GetLatestWorkflowRun(client, env.GitRepoOwner, env.GitRepoName,
+			fmt.Sprintf("porter_%s_env.yml", env.Name), deployment.PRBranchFrom)
 
+		if err == nil {
 			deployment.LastWorkflowRunURL = latestWorkflowRun.GetHTMLURL()
 
 			if (latestWorkflowRun.GetStatus() == "in_progress" ||

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

@@ -1,7 +1,6 @@
 package environment
 
 import (
-	"context"
 	"errors"
 	"fmt"
 	"net/http"
@@ -11,6 +10,7 @@ import (
 	"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/commonutils"
 	"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"
@@ -69,8 +69,8 @@ func (c *TriggerDeploymentWorkflowHandler) ServeHTTP(w http.ResponseWriter, r *h
 		return
 	}
 
-	latestWorkflowRun, err := getLatestWorkflowRun(client, env.GitRepoOwner, env.GitRepoName,
-		fmt.Sprintf("porter_%s_env.yml", env.Name))
+	latestWorkflowRun, err := commonutils.GetLatestWorkflowRun(client, env.GitRepoOwner, env.GitRepoName,
+		fmt.Sprintf("porter_%s_env.yml", env.Name), depl.PRBranchFrom)
 
 	if err != nil && errors.Is(err, ErrNoWorkflowRuns) {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, 400))
@@ -107,25 +107,14 @@ func (c *TriggerDeploymentWorkflowHandler) ServeHTTP(w http.ResponseWriter, r *h
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
-}
 
-func getLatestWorkflowRun(client *github.Client, owner, repo, filename string) (*github.WorkflowRun, error) {
-	workflowRuns, _, err := client.Actions.ListWorkflowRunsByFileName(
-		context.Background(), owner, repo, filename, &github.ListWorkflowRunsOptions{
-			ListOptions: github.ListOptions{
-				Page:    1,
-				PerPage: 1,
-			},
-		},
-	)
+	// set the status to updating manually here for the frontend to case on
+	depl.Status = types.DeploymentStatusUpdating
 
-	if err != nil {
-		return nil, err
-	}
+	_, err = c.Repo().Environment().UpdateDeployment(depl)
 
-	if workflowRuns.GetTotalCount() == 0 {
-		return nil, ErrNoWorkflowRuns
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
 	}
-
-	return workflowRuns.WorkflowRuns[0], nil
 }

+ 29 - 30
api/server/handlers/gitinstallation/rerun_workflow.go

@@ -1,20 +1,18 @@
 package gitinstallation
 
 import (
-	"context"
 	"errors"
 	"fmt"
 	"net/http"
+	"strings"
 
-	"github.com/google/go-github/v41/github"
 	"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/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 )
 
-var ErrNoWorkflowRuns = errors.New("no previous workflow runs found")
-
 type RerunWorkflowHandler struct {
 	handlers.PorterHandlerReadWriter
 }
@@ -37,10 +35,28 @@ func (c *RerunWorkflowHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	}
 
 	filename := r.URL.Query().Get("filename")
+	// if branch is empty then the latest workflow run is rerun, meaning that if
+	// there were multiple workflow runs for the same file but for different branches
+	// only the very latest of the workflow runs will be rerun
+	branch := r.URL.Query().Get("branch")
+	releaseName := r.URL.Query().Get("release_name")
+
+	if filename == "" && releaseName == "" {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("filename and release name are both empty")))
+		return
+	}
 
 	if filename == "" {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("filename query param not set")))
-		return
+		if c.Config().ServerConf.InstanceName != "" {
+			filename = fmt.Sprintf("porter_%s_%s.yml", strings.Replace(
+				strings.ToLower(releaseName), "-", "_", -1),
+				strings.ToLower(c.Config().ServerConf.InstanceName),
+			)
+		} else {
+			filename = fmt.Sprintf("porter_%s.yml", strings.Replace(
+				strings.ToLower(releaseName), "-", "_", -1),
+			)
+		}
 	}
 
 	client, err := GetGithubAppClientFromRequest(c.Config(), r)
@@ -50,11 +66,15 @@ func (c *RerunWorkflowHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	latestWorkflowRun, err := getLatestWorkflowRun(client, owner, name, filename)
+	latestWorkflowRun, err := commonutils.GetLatestWorkflowRun(client, owner, name, filename, branch)
 
-	if err != nil && errors.Is(err, ErrNoWorkflowRuns) {
+	if err != nil && errors.Is(err, commonutils.ErrNoWorkflowRuns) {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, 400))
 		return
+	} else if err != nil && errors.Is(err, commonutils.ErrWorkflowNotFound) {
+		w.WriteHeader(http.StatusNotFound)
+		c.WriteResult(w, r, filename)
+		return
 	} else if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
@@ -73,7 +93,7 @@ func (c *RerunWorkflowHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	latestWorkflowRun, err = getLatestWorkflowRun(client, owner, name, filename)
+	latestWorkflowRun, err = commonutils.GetLatestWorkflowRun(client, owner, name, filename, branch)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
@@ -82,24 +102,3 @@ func (c *RerunWorkflowHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 
 	c.WriteResult(w, r, latestWorkflowRun.GetHTMLURL())
 }
-
-func getLatestWorkflowRun(client *github.Client, owner, repo, filename string) (*github.WorkflowRun, error) {
-	workflowRuns, _, err := client.Actions.ListWorkflowRunsByFileName(
-		context.Background(), owner, repo, filename, &github.ListWorkflowRunsOptions{
-			ListOptions: github.ListOptions{
-				Page:    1,
-				PerPage: 1,
-			},
-		},
-	)
-
-	if err != nil {
-		return nil, err
-	}
-
-	if workflowRuns.GetTotalCount() == 0 {
-		return nil, ErrNoWorkflowRuns
-	}
-
-	return workflowRuns.WorkflowRuns[0], nil
-}

+ 3 - 8
api/server/router/cluster.go

@@ -463,20 +463,15 @@ func getClusterRoutes(
 			Router:   r,
 		})
 
-		// DELETE /api/projects/{project_id}/clusters/{cluster_id}/deployments/{environment_id}/{owner}/{name}/{pr_number} ->
+		// DELETE /api/projects/{project_id}/clusters/{cluster_id}/deployments/{deployment_id} ->
 		// environment.NewDeleteDeploymentHandler
 		deleteDeploymentEndpoint := factory.NewAPIEndpoint(
 			&types.APIRequestMetadata{
 				Verb:   types.APIVerbDelete,
 				Method: types.HTTPVerbDelete,
 				Path: &types.Path{
-					Parent: basePath,
-					RelativePath: fmt.Sprintf(
-						"%s/deployments/{environment_id}/{%s}/{%s}/{pr_number}",
-						relPath,
-						types.URLParamGitRepoOwner,
-						types.URLParamGitRepoName,
-					),
+					Parent:       basePath,
+					RelativePath: relPath + "/deployments/{deployment_id}",
 				},
 				Scopes: []types.PermissionScope{
 					types.UserScope,

+ 38 - 0
api/server/shared/commonutils/git_utils.go

@@ -0,0 +1,38 @@
+package commonutils
+
+import (
+	"context"
+	"errors"
+	"net/http"
+
+	"github.com/google/go-github/v41/github"
+)
+
+var ErrNoWorkflowRuns = errors.New("no previous workflow runs found")
+var ErrWorkflowNotFound = errors.New("no workflow found, file missing")
+
+func GetLatestWorkflowRun(client *github.Client, owner, repo, filename, branch string) (*github.WorkflowRun, error) {
+	workflowRuns, ghResponse, err := client.Actions.ListWorkflowRunsByFileName(
+		context.Background(), owner, repo, filename, &github.ListWorkflowRunsOptions{
+			Branch: branch,
+			ListOptions: github.ListOptions{
+				Page:    1,
+				PerPage: 1,
+			},
+		},
+	)
+
+	if ghResponse.StatusCode == http.StatusNotFound {
+		return nil, ErrWorkflowNotFound
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	if workflowRuns.GetTotalCount() == 0 {
+		return nil, ErrNoWorkflowRuns
+	}
+
+	return workflowRuns.WorkflowRuns[0], nil
+}

+ 20 - 15
cli/cmd/apply.go

@@ -398,26 +398,31 @@ func (d *Driver) applyApplication(resource *models.Resource, client *api.Client,
 	if d.source.Name == "job" && appConfig.WaitForJob && (shouldCreate || !appConfig.OnlyCreate) {
 		color.New(color.FgYellow).Printf("Waiting for job '%s' to finish\n", resource.Name)
 
-		prevProject := cliConf.Project
-		prevCluster := cliConf.Cluster
-		name = resource.Name
-		namespace = d.target.Namespace
-		cliConf.Project = d.target.Project
-		cliConf.Cluster = d.target.Cluster
-
 		err = wait.WaitForJob(client, &wait.WaitOpts{
-			ProjectID: cliConf.Project,
-			ClusterID: cliConf.Cluster,
-			Namespace: namespace,
-			Name:      name,
+			ProjectID: d.target.Project,
+			ClusterID: d.target.Cluster,
+			Namespace: d.target.Namespace,
+			Name:      resource.Name,
 		})
 
 		if err != nil {
-			return nil, err
-		}
+			if appConfig.OnlyCreate {
+				err = client.DeleteRelease(
+					context.Background(),
+					d.target.Project,
+					d.target.Cluster,
+					d.target.Namespace,
+					resource.Name,
+				)
 
-		cliConf.Project = prevProject
-		cliConf.Cluster = prevCluster
+				if err != nil {
+					return nil, fmt.Errorf("error deleting job %s with waitForJob and onlyCreate set to true: %w",
+						resource.Name, err)
+				}
+			}
+
+			return nil, fmt.Errorf("error waiting for job %s: %w", resource.Name, err)
+		}
 	}
 
 	return resource, err

+ 13 - 0
cli/cmd/deploy.go

@@ -12,6 +12,7 @@ import (
 	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/deploy"
 	"github.com/porter-dev/porter/cli/cmd/utils"
+	templaterUtils "github.com/porter-dev/porter/internal/templater/utils"
 	"github.com/spf13/cobra"
 	"k8s.io/client-go/util/homedir"
 )
@@ -637,6 +638,18 @@ func updateUpgradeWithAgent(updateAgent *deploy.DeployAgent) error {
 		return err
 	}
 
+	env, err := updateAgent.GetBuildEnv(&deploy.GetBuildEnvOpts{UseNewConfig: false})
+
+	if err == nil && len(env) > 0 {
+		valuesObj = templaterUtils.CoalesceValues(valuesObj, map[string]interface{}{
+			"container": map[string]interface{}{
+				"env": map[string]interface{}{
+					"normal": env,
+				},
+			},
+		})
+	}
+
 	err = updateAgent.UpdateImageAndValues(valuesObj)
 
 	if err != nil {

+ 11 - 1
cli/cmd/deploy/create.go

@@ -284,6 +284,16 @@ func (c *CreateAgent) CreateFromDocker(
 			env = map[string]string{}
 		}
 
+		buildEnv, err := GetNestedMap(mergedValues, "container", "env", "build")
+
+		if err == nil {
+			for key, val := range buildEnv {
+				if valStr, ok := val.(string); ok {
+					env[key] = valStr
+				}
+			}
+		}
+
 		// add additional env based on options
 		for key, val := range opts.SharedOpts.AdditionalEnv {
 			env[key] = val
@@ -506,7 +516,7 @@ func (c *CreateAgent) CreateSubdomainIfRequired(mergedValues map[string]interfac
 	// check for automatic subdomain creation if web kind
 	if c.CreateOpts.Kind == "web" {
 		// look for ingress.enabled and no custom domains set
-		ingressMap, err := getNestedMap(mergedValues, "ingress")
+		ingressMap, err := GetNestedMap(mergedValues, "ingress")
 
 		if err == nil {
 			enabledVal, enabledExists := ingressMap["enabled"]

+ 15 - 5
cli/cmd/deploy/deploy.go

@@ -166,6 +166,16 @@ func (d *DeployAgent) GetBuildEnv(opts *GetBuildEnvOpts) (map[string]string, err
 		return nil, err
 	}
 
+	buildEnv, err := GetNestedMap(conf, "container", "env", "build")
+
+	if err == nil {
+		for key, val := range buildEnv {
+			if valStr, ok := val.(string); ok {
+				env[key] = valStr
+			}
+		}
+	}
+
 	// add additional env based on options
 	for key, val := range d.opts.SharedOpts.AdditionalEnv {
 		env[key] = val
@@ -412,7 +422,7 @@ func GetEnvForRelease(client *client.Client, config map[string]interface{}, proj
 	res := make(map[string]string)
 
 	// first, get the env vars from "container.env.normal"
-	envConfig, err := getNestedMap(config, "container", "env", "normal")
+	envConfig, err := GetNestedMap(config, "container", "env", "normal")
 
 	// if the field is not found, set envConfig to an empty map; this release has no env set
 	if err != nil {
@@ -435,7 +445,7 @@ func GetEnvForRelease(client *client.Client, config map[string]interface{}, proj
 
 	// next, get the env vars specified by "container.env.synced"
 	// look for container.env.synced
-	envConf, err := getNestedMap(config, "container", "env")
+	envConf, err := GetNestedMap(config, "container", "env")
 
 	// if error, just return the env detected from above
 	if err != nil {
@@ -558,7 +568,7 @@ func (d *DeployAgent) getReleaseImage() (string, error) {
 	}
 
 	// get the image from the conig
-	imageConfig, err := getNestedMap(d.release.Config, "image")
+	imageConfig, err := GetNestedMap(d.release.Config, "image")
 
 	if err != nil {
 		return "", fmt.Errorf("could not get image config from release: %s", err.Error())
@@ -581,7 +591,7 @@ func (d *DeployAgent) getReleaseImage() (string, error) {
 
 func (d *DeployAgent) pullCurrentReleaseImage() (string, error) {
 	// pull the currently deployed image to use cache, if possible
-	imageConfig, err := getNestedMap(d.release.Config, "image")
+	imageConfig, err := GetNestedMap(d.release.Config, "image")
 
 	if err != nil {
 		return "", fmt.Errorf("could not get image config from release: %s", err.Error())
@@ -668,7 +678,7 @@ func (e *NestedMapFieldNotFoundError) Error() string {
 	return fmt.Sprintf("could not find field %s in configuration", e.Field)
 }
 
-func getNestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, error) {
+func GetNestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, error) {
 	var res map[string]interface{}
 	curr := obj
 

+ 1 - 1
cli/cmd/deploy/shared.go

@@ -49,7 +49,7 @@ func coalesceEnvGroups(
 			return err
 		}
 
-		envConfig, err := getNestedMap(config, "container", "env", "normal")
+		envConfig, err := GetNestedMap(config, "container", "env", "normal")
 
 		if err != nil || envConfig == nil {
 			envConfig = make(map[string]interface{})

+ 10 - 0
cli/cmd/preview/build_image_driver.go

@@ -254,6 +254,16 @@ func (d *BuildDriver) Apply(resource *models.Resource) (*models.Resource, error)
 			env = map[string]string{}
 		}
 
+		buildEnv, err := deploy.GetNestedMap(mergedValues, "container", "env", "build")
+
+		if err == nil {
+			for key, val := range buildEnv {
+				if valStr, ok := val.(string); ok {
+					env[key] = valStr
+				}
+			}
+		}
+
 		buildAgent := &deploy.BuildAgent{
 			SharedOpts:  createAgent.CreateOpts.SharedOpts,
 			APIClient:   client,

+ 9 - 4
dashboard/src/components/form-components/KeyValueArray.tsx

@@ -130,7 +130,9 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
                   let obj = this.valuesToObject();
                   this.props.setValues(obj);
                 }}
-                disabled={this.props.disabled || value.includes("PORTERSECRET")}
+                disabled={
+                  this.props.disabled || value?.includes("PORTERSECRET")
+                }
                 spellCheck={false}
               />
               <Spacer />
@@ -145,12 +147,14 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
                   let obj = this.valuesToObject();
                   this.props.setValues(obj);
                 }}
-                disabled={this.props.disabled || value.includes("PORTERSECRET")}
-                type={value.includes("PORTERSECRET") ? "password" : "text"}
+                disabled={
+                  this.props.disabled || value?.includes("PORTERSECRET")
+                }
+                type={value?.includes("PORTERSECRET") ? "password" : "text"}
                 spellCheck={false}
               />
               {this.renderDeleteButton(i)}
-              {this.renderHiddenOption(value.includes("PORTERSECRET"), i)}
+              {this.renderHiddenOption(value?.includes("PORTERSECRET"), i)}
             </InputWrapper>
           );
         })}
@@ -176,6 +180,7 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
               this.props.setValues(newValues);
               this.setState({ values: this.objectToValues(newValues) });
             }}
+            normalEnvVarsOnly
           />
         </Modal>
       );

+ 1 - 1
dashboard/src/components/porter-form/field-components/KeyValueArray.tsx

@@ -31,7 +31,7 @@ const KeyValueArray: React.FC<Props> = (props) => {
       initState: () => {
         let values = props.value[0];
         const normalValues = Object.entries(values?.normal || {});
-        values = omit(values, ["normal", "synced"]);
+        values = omit(values, ["normal", "synced", "build"]);
         return {
           values: hasSetValue(props)
             ? ([...Object.entries(values), ...normalValues]?.map(([k, v]) => {

+ 151 - 121
dashboard/src/main/home/cluster-dashboard/expanded-chart/BuildSettingsTab.tsx

@@ -1,4 +1,3 @@
-import DynamicLink from "components/DynamicLink";
 import Heading from "components/form-components/Heading";
 import Helper from "components/form-components/Helper";
 import KeyValueArray from "components/form-components/KeyValueArray";
@@ -16,6 +15,8 @@ import {
 } from "shared/types";
 import styled, { keyframes } from "styled-components";
 import yaml from "js-yaml";
+import DynamicLink from "components/DynamicLink";
+import { AxiosError } from "axios";
 
 const DEFAULT_PAKETO_STACK = "paketobuildpacks/builder:full";
 const DEFAULT_HEROKU_STACK = "heroku/buildpacks:20";
@@ -62,8 +63,13 @@ const BuildSettingsTab: React.FC<Props> = ({ chart, isPreviousVersion }) => {
 
   const [buildConfig, setBuildConfig] = useState<BuildConfig>(null);
   const [envVariables, setEnvVariables] = useState(
-    chart.config?.container?.env?.normal || null
+    chart.config?.container?.env?.build || null
   );
+  const [runningWorkflowURL, setRunningWorkflowURL] = useState("");
+  const [reRunError, setReRunError] = useState<{
+    title: string;
+    description: string;
+  }>(null);
   const [buttonStatus, setButtonStatus] = useState<
     "loading" | "successful" | string
   >("");
@@ -99,7 +105,7 @@ const BuildSettingsTab: React.FC<Props> = ({ chart, isPreviousVersion }) => {
       return;
     }
 
-    values.container.env.normal = envs;
+    values.container.env.build = { ...envs };
     const valuesYaml = yaml.dump({ ...values });
     try {
       await api.upgradeChartValues(
@@ -128,12 +134,71 @@ const BuildSettingsTab: React.FC<Props> = ({ chart, isPreviousVersion }) => {
           project_id: currentProject.id,
           cluster_id: currentCluster.id,
           git_installation_id: chart.git_action_config?.git_repo_id,
-          owner: chart.git_action_config.repo?.split("/")[0],
-          name: chart.git_action_config.repo?.split("/")[1],
-          filename: `porter_${chart.name.replaceAll("-", "_")}.yaml`,
+          owner: chart.git_action_config?.git_repo?.split("/")[0],
+          name: chart.git_action_config?.git_repo?.split("/")[1],
+          branch: chart.git_action_config?.git_branch,
+          release_name: chart.name,
         }
       );
     } catch (error) {
+      if (!error?.response) {
+        throw error;
+      }
+
+      let tmpError: AxiosError = error;
+
+      /**
+       * @smell
+       * Currently the expanded chart is clearing all the state when a chart update is triggered (saveEnvVariables).
+       * Temporary usage of setCurrentError until a context is applied to keep the state of the ReRunError during re renders.
+       */
+
+      if (tmpError.response.status === 400) {
+        // setReRunError({
+        //   title: "No previous run found",
+        //   description:
+        //     "There are no previous runs for this workflow, please trigger manually a run before changing the build settings.",
+        // });
+        setCurrentError(
+          "There are no previous runs for this workflow, please trigger manually a run before changing the build settings."
+        );
+        return;
+      }
+
+      if (tmpError.response.status === 409) {
+        // setReRunError({
+        //   title: "The workflow is still running",
+        //   description:
+        //     'If you want to make more changes, please choose the option "Save" until the workflow finishes.',
+        // });
+
+        if (typeof tmpError.response.data === "string") {
+          setRunningWorkflowURL(tmpError.response.data);
+        }
+        setCurrentError(
+          'The workflow is still running. If you want to make more changes, please choose the option "Save" until the workflow finishes. You can check the current status of the workflow here ' +
+            tmpError.response.data
+        );
+        return;
+      }
+
+      if (tmpError.response.status === 404) {
+        let description =
+          "Apparently there's no action file that corresponds to this deployment. ";
+        if (typeof tmpError.response.data === "string") {
+          const filename = tmpError.response.data;
+          description = description.concat(
+            `Please check that the file ${filename} exists on your repository.`
+          );
+        }
+        // setReRunError({
+        //   title: "The action doesn't seem to exist",
+        //   description,
+        // });
+
+        setCurrentError(description);
+        return;
+      }
       throw error;
     }
   };
@@ -182,6 +247,34 @@ const BuildSettingsTab: React.FC<Props> = ({ chart, isPreviousVersion }) => {
         </DisabledOverlay>
       ) : null}
       <StyledSettingsSection blurContent={isPreviousVersion}>
+        {/* {reRunError !== null ? (
+        <AlertCard>
+          <AlertCardIcon className="material-icons">error</AlertCardIcon>
+          <AlertCardContent className="content">
+            <AlertCardTitle className="title">
+              {reRunError.title}
+            </AlertCardTitle>
+            {reRunError.description}
+            {runningWorkflowURL.length ? (
+              <>
+                {" "}
+                To go to the workflow{" "}
+                <DynamicLink to={runningWorkflowURL} target="_blank">
+                  click here
+                </DynamicLink>
+              </>
+            ) : null}
+          </AlertCardContent>
+          <AlertCardAction
+            onClick={() => {
+              setReRunError(null);
+              setRunningWorkflowURL("");
+            }}
+          >
+            <span className="material-icons">close</span>
+          </AlertCardAction>
+        </AlertCard>
+      ) : null} */}
         <Heading isAtTop>Build step environment variables:</Heading>
         <KeyValueArray
           values={envVariables}
@@ -191,17 +284,20 @@ const BuildSettingsTab: React.FC<Props> = ({ chart, isPreviousVersion }) => {
             clusterId: currentCluster.id,
           }}
           setValues={(values) => {
-            console.log(values);
             setEnvVariables(values);
           }}
         ></KeyValueArray>
 
-        <Heading>Buildpack settings</Heading>
-        <BuildpackConfigSection
-          currentChart={chart}
-          actionConfig={chart.git_action_config}
-          onChange={(buildConfig) => setBuildConfig(buildConfig)}
-        />
+        {!chart.git_action_config.dockerfile_path ? (
+          <>
+            <Heading>Buildpack settings</Heading>
+            <BuildpackConfigSection
+              currentChart={chart}
+              actionConfig={chart.git_action_config}
+              onChange={(buildConfig) => setBuildConfig(buildConfig)}
+            />
+          </>
+        ) : null}
         <SaveButtonWrapper>
           <MultiSaveButton
             options={[
@@ -252,8 +348,6 @@ const BuildpackConfigSection: React.FC<{
     []
   );
 
-  const [runningWorkflowURL, setRunningWorkflowURL] = useState<string>(null);
-
   useEffect(() => {
     const currentBuildConfig = currentChart?.build_config;
 
@@ -473,28 +567,6 @@ const BuildpackConfigSection: React.FC<{
 
   return (
     <BuildpackConfigurationContainer>
-      {runningWorkflowURL && (
-        <AlertCard>
-          <AlertCardIcon className="material-icons">error</AlertCardIcon>
-          <AlertCardContent className="content">
-            <AlertCardTitle className="title">
-              The workflow is still running
-            </AlertCardTitle>
-            Please wait until it finishes before changing the buildpack
-            configuration. To go to the workflow{" "}
-            <DynamicLink to={runningWorkflowURL} target="_blank">
-              click here
-            </DynamicLink>
-          </AlertCardContent>
-          <AlertCardAction
-            onClick={() => {
-              setRunningWorkflowURL("");
-            }}
-          >
-            <span className="material-icons">close</span>
-          </AlertCardAction>
-        </AlertCard>
-      )}
       <>
         <SelectRow
           value={selectedBuilder}
@@ -527,17 +599,6 @@ const BuildpackConfigSection: React.FC<{
         )}
       </>
     </BuildpackConfigurationContainer>
-    // <SaveButtonWrapper>
-    //   <SaveButton
-    //     onClick={() => {
-    //       handleSubmit();
-    //     }}
-    //     status={buttonStatus}
-    //     text="Save build config"
-    //     makeFlush
-    //     clearPosition
-    //   />
-    // </SaveButtonWrapper>
   );
 };
 
@@ -563,55 +624,6 @@ const fadeIn = keyframes`
   }
 `;
 
-const AlertCard = styled.div`
-  transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
-  border-radius: 4px;
-  box-shadow: none;
-  font-weight: 400;
-  font-size: 0.875rem;
-  line-height: 1.43;
-  letter-spacing: 0.01071em;
-  border: 1px solid rgb(229, 115, 115);
-  display: flex;
-  padding: 6px 16px;
-  color: rgb(244, 199, 199);
-  margin-top: 20px;
-  position: relative;
-`;
-
-const AlertCardIcon = styled.span`
-  color: rgb(239, 83, 80);
-  margin-right: 12px;
-  padding: 7px 0px;
-  display: flex;
-  font-size: 22px;
-  opacity: 0.9;
-`;
-
-const AlertCardTitle = styled.div`
-  margin: -2px 0px 0.35em;
-  font-size: 1rem;
-  line-height: 1.5;
-  letter-spacing: 0.00938em;
-  font-weight: 500;
-`;
-
-const AlertCardContent = styled.div`
-  padding: 8px 0px;
-`;
-
-const AlertCardAction = styled.button`
-  position: absolute;
-  right: 5px;
-  top: 5px;
-  border: none;
-  background-color: unset;
-  color: white;
-  :hover {
-    cursor: pointer;
-  }
-`;
-
 const SaveButtonWrapper = styled.div`
   width: 100%;
   margin-top: 30px;
@@ -683,12 +695,6 @@ const EventName = styled.div`
   color: #ffffff;
 `;
 
-const EventReason = styled.div`
-  font-family: "Work Sans", sans-serif;
-  color: #aaaabb;
-  margin-top: 5px;
-`;
-
 const ActionContainer = styled.div`
   display: flex;
   align-items: center;
@@ -719,27 +725,51 @@ const DeleteButton = styled.button`
   }
 `;
 
-const SubmitButton = styled.button`
-  height: 35px;
-  font-size: 13px;
+const AlertCard = styled.div`
+  transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+  border-radius: 4px;
+  box-shadow: none;
+  font-weight: 400;
+  font-size: 0.875rem;
+  line-height: 1.43;
+  letter-spacing: 0.01071em;
+  border: 1px solid rgb(229, 115, 115);
+  display: flex;
+  padding: 6px 16px;
+  color: rgb(244, 199, 199);
   margin-top: 20px;
-  margin-bottom: 30px;
+  position: relative;
+`;
+
+const AlertCardIcon = styled.span`
+  color: rgb(239, 83, 80);
+  margin-right: 12px;
+  padding: 7px 0px;
+  display: flex;
+  font-size: 22px;
+  opacity: 0.9;
+`;
+
+const AlertCardTitle = styled.div`
+  margin: -2px 0px 0.35em;
+  font-size: 1rem;
+  line-height: 1.5;
+  letter-spacing: 0.00938em;
   font-weight: 500;
-  font-family: "Work Sans", sans-serif;
+`;
+
+const AlertCardContent = styled.div`
+  padding: 8px 0px;
+`;
+
+const AlertCardAction = styled.button`
+  position: absolute;
+  right: 5px;
+  top: 5px;
+  border: none;
+  background-color: unset;
   color: white;
-  padding: 6px 20px 7px 20px;
-  text-align: left;
-  border: 0;
-  border-radius: 5px;
-  background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
-  box-shadow: ${(props) =>
-    !props.disabled ? "0 2px 5px 0 #00000030" : "none"};
-  cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
-  user-select: none;
-  :focus {
-    outline: 0;
-  }
   :hover {
-    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
+    cursor: pointer;
   }
 `;

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

@@ -542,12 +542,12 @@ const ExpandedChart: React.FC<Props> = (props) => {
       );
     }
 
-    // if (currentChart?.git_action_config?.git_repo) {
-    rightTabOptions.push({
-      label: "Build Settings",
-      value: "build-settings",
-    });
-    // }
+    if (currentChart?.git_action_config?.git_repo) {
+      rightTabOptions.push({
+        label: "Build Settings",
+        value: "build-settings",
+      });
+    }
 
     // Settings tab is always last
     if (isAuthorized("application", "", ["get", "delete"])) {
@@ -948,7 +948,6 @@ const LineBreak = styled.div`
 
 const BodyWrapper = styled.div`
   position: relative;
-  overflow: hidden;
   margin-bottom: 120px;
 `;
 

+ 12 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -27,6 +27,7 @@ import ConnectToJobInstructionsModal from "./jobs/ConnectToJobInstructionsModal"
 import CommandLineIcon from "assets/command-line-icon";
 import CronParser from "cron-parser";
 import CronPrettifier from "cronstrue";
+import BuildSettingsTab from "./BuildSettingsTab";
 
 const readableDate = (s: string) => {
   let ts = new Date(s);
@@ -84,6 +85,13 @@ export const ExpandedJobChartFC: React.FC<{
     rightTabOptions.push({ label: "Settings", value: "settings" });
   }
 
+  if (chart?.git_action_config?.git_repo) {
+    rightTabOptions.push({
+      label: "Build Settings",
+      value: "build-settings",
+    });
+  }
+
   const leftTabOptions = [{ label: "Jobs", value: "jobs" }];
 
   const processValuesToUpdateChart = (newConfig?: any) => (
@@ -234,6 +242,10 @@ export const ExpandedJobChartFC: React.FC<{
       );
     }
 
+    if (currentTab === "build-settings") {
+      return <BuildSettingsTab chart={chart} />;
+    }
+
     if (
       currentTab === "settings" &&
       isAuthorized("job", "", ["get", "delete"])

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

@@ -41,10 +41,7 @@ const DeploymentCard: React.FC<{
         {
           cluster_id: currentCluster.id,
           project_id: currentProject.id,
-          environment_id: deployment.environment_id,
-          repo_owner: deployment.gh_repo_owner,
-          repo_name: deployment.gh_repo_name,
-          pr_number: deployment.pull_request_id,
+          deployment_id: deployment.id,
         }
       )
       .then(() => {

+ 6 - 2
dashboard/src/main/home/modals/LoadEnvGroupModal.tsx

@@ -30,6 +30,7 @@ type PropsType = {
   enableSyncedEnvGroups?: boolean;
   syncedEnvGroups?: PopulatedEnvGroup[];
   setSyncedEnvGroups?: (values: PopulatedEnvGroup) => void;
+  normalEnvVarsOnly?: boolean;
 };
 
 type StateType = {
@@ -132,6 +133,9 @@ export default class LoadEnvGroupModal extends Component<PropsType, StateType> {
     } else {
       return this.state.envGroups
         .filter((envGroup) => {
+          if (!Array.isArray(this.props.syncedEnvGroups)) {
+            return true;
+          }
           return !this.props.syncedEnvGroups.find(
             (syncedEnvGroup) => syncedEnvGroup.name === envGroup.name
           );
@@ -265,11 +269,11 @@ export default class LoadEnvGroupModal extends Component<PropsType, StateType> {
                   />
                 </IconWrapper>
               </>
-            ) : (
+            ) : !this.props.normalEnvVarsOnly ? (
               <Helper color="#f5cb42">
                 Upgrade the job template to enable sync env groups
               </Helper>
-            )}
+            ) : null}
           </AbsoluteWrapper>
         </GroupModalSections>
 

+ 29 - 12
dashboard/src/shared/api.tsx

@@ -367,21 +367,15 @@ const deletePRDeployment = baseApi<
   {
     cluster_id: number;
     project_id: number;
-    environment_id: number;
-    repo_owner: string;
-    repo_name: string;
-    pr_number: number;
+    deployment_id: number;
   }
 >("DELETE", (pathParams) => {
   const {
     cluster_id,
     project_id,
-    environment_id,
-    repo_owner,
-    repo_name,
-    pr_number,
+    deployment_id,
   } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/deployments/${environment_id}/${repo_owner}/${repo_name}/${pr_number}`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/deployments/${deployment_id}`;
 });
 
 const getNotificationConfig = baseApi<
@@ -1721,12 +1715,35 @@ const reRunGHWorkflow = baseApi<
     git_installation_id: number;
     owner: string;
     name: string;
-    filename: string;
+    branch: string;
+    filename?: string;
+    release_name?: string;
   }
 >(
   "POST",
-  ({ project_id, git_installation_id, owner, name, cluster_id, filename }) =>
-    `/api/projects/${project_id}/gitrepos/${git_installation_id}/${owner}/${name}/clusters/${cluster_id}/rerun_workflow?filename=${filename}`
+  ({
+    project_id,
+    git_installation_id,
+    owner,
+    name,
+    cluster_id,
+    filename,
+    release_name,
+    branch,
+  }) => {
+    const queryParams = new URLSearchParams();
+
+    queryParams.set("branch", branch);
+
+    if (release_name) {
+      queryParams.set("release_name", release_name);
+    }
+    if (filename) {
+      queryParams.set("filename", filename);
+    }
+
+    return `/api/projects/${project_id}/gitrepos/${git_installation_id}/${owner}/${name}/clusters/${cluster_id}/rerun_workflow?${queryParams.toString()}`;
+  }
 );
 
 const triggerPreviewEnvWorkflow = baseApi<

+ 3 - 0
dashboard/src/shared/types.tsx

@@ -73,6 +73,9 @@ export interface ChartTypeWithExtendedConfig extends ChartType {
         normal: {
           [key: string]: string;
         };
+        build: {
+          [key: string]: string;
+        };
         synced: any;
       };
       lifecycle: { postStart: string; preStop: string };