Browse Source

Merge remote-tracking branch 'origin/master' into multiapp-parse

Ian Edwards 2 years ago
parent
commit
aecb9ebecb

+ 3 - 8
README.md

@@ -1,6 +1,6 @@
 # Porter
 # Porter
 
 
-[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Go Report Card](https://goreportcard.com/badge/gojp/goreportcard)](https://goreportcard.com/report/github.com/porter-dev/porter) [![Discord](https://img.shields.io/discord/542888846271184896?color=7389D8&label=community&logo=discord&logoColor=ffffff)](https://discord.gg/mmGAw5nNjr)
+[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Go Report Card](https://goreportcard.com/badge/gojp/goreportcard)](https://goreportcard.com/report/github.com/porter-dev/porter)
 [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow)](https://twitter.com/porterdotrun)
 [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow)](https://twitter.com/porterdotrun)
 
 
 **Porter is a Kubernetes-powered PaaS that runs in your own cloud provider.** Porter brings the Heroku experience to your own AWS/GCP account, while upgrading your infrastructure to Kubernetes. Get started on Porter without the overhead of DevOps and customize your infrastructure later when you need to.
 **Porter is a Kubernetes-powered PaaS that runs in your own cloud provider.** Porter brings the Heroku experience to your own AWS/GCP account, while upgrading your infrastructure to Kubernetes. Get started on Porter without the overhead of DevOps and customize your infrastructure later when you need to.
@@ -9,9 +9,7 @@
 
 
 ## Community and Updates
 ## Community and Updates
 
 
-For help, questions, or if you just want a place to hang out, [join our Discord community.](https://discord.gg/mmGAw5nNjr)
-
-To keep updated on our progress, please watch the repo for new releases (**Watch > Custom > Releases**) and [follow us on Twitter](https://twitter.com/getporterdev)!
+To keep updated on our progress, please watch the repo for new releases (**Watch > Custom > Releases**) and [follow us on Twitter](https://twitter.com/porterdotrun)!
 
 
 ## Why Porter?
 ## Why Porter?
 
 
@@ -30,6 +28,7 @@ Porter brings the simplicity of a traditional PaaS to your own cloud provider wh
 - One-click provisioning of a Kubernetes cluster in your own cloud console
 - One-click provisioning of a Kubernetes cluster in your own cloud console
   - ✅ AWS
   - ✅ AWS
   - ✅ GCP
   - ✅ GCP
+  - ✅ Azure
 - Simple deploy of any public or private Docker image
 - Simple deploy of any public or private Docker image
 - Auto CI/CD with [buildpacks](https://buildpacks.io) for non-Dockerized apps
 - Auto CI/CD with [buildpacks](https://buildpacks.io) for non-Dockerized apps
 - Heroku-like GUI to monitor application status, logs, and history
 - Heroku-like GUI to monitor application status, logs, and history
@@ -61,7 +60,3 @@ Below are instructions for a quickstart. For full documentation, please visit ou
 2. Create a Project and [put in your cloud provider credentials](https://docs.getporter.dev/docs/getting-started-with-porter-on-aws). Porter will automatically provision a Kubernetes cluster in your own cloud. It is also possible to [link up an existing Kubernetes cluster.](https://docs.getporter.dev/docs/cli-documentation#connecting-to-an-existing-cluster)
 2. Create a Project and [put in your cloud provider credentials](https://docs.getporter.dev/docs/getting-started-with-porter-on-aws). Porter will automatically provision a Kubernetes cluster in your own cloud. It is also possible to [link up an existing Kubernetes cluster.](https://docs.getporter.dev/docs/cli-documentation#connecting-to-an-existing-cluster)
 
 
 3. 🚀 Deploy your applications from a [git repository](https://docs.getporter.dev/docs/applications) or [Docker image registry](https://docs.getporter.dev/docs/cli-documentation#porter-docker-configure).
 3. 🚀 Deploy your applications from a [git repository](https://docs.getporter.dev/docs/applications) or [Docker image registry](https://docs.getporter.dev/docs/cli-documentation#porter-docker-configure).
-
-## Want to Help?
-
-We welcome all contributions. If you're interested in contributing, please read our [contributing guide](https://github.com/porter-dev/porter/blob/master/CONTRIBUTING.md) and [join our Discord community](https://discord.gg/GJynMR3KXK).

+ 7 - 5
api/server/handlers/porter_app/apply.go

@@ -229,13 +229,11 @@ func addPorterSubdomainsIfNecessary(ctx context.Context, app v2.PorterApp, deplo
 	ctx, span := telemetry.NewSpan(ctx, "add-porter-subdomains-if-necessary")
 	ctx, span := telemetry.NewSpan(ctx, "add-porter-subdomains-if-necessary")
 	defer span.End()
 	defer span.End()
 
 
+	services := make([]v2.Service, 0)
+
 	for _, service := range app.Services {
 	for _, service := range app.Services {
 		if service.Type == v2.ServiceType_Web {
 		if service.Type == v2.ServiceType_Web {
-			if service.Private == nil || !*service.Private {
-				continue
-			}
-
-			if service.Domains != nil && len(service.Domains) == 0 {
+			if service.Private != nil && !*service.Private && service.Domains != nil && len(service.Domains) == 0 {
 				if deploymentTarget.Namespace != DeploymentTargetSelector_Default {
 				if deploymentTarget.Namespace != DeploymentTargetSelector_Default {
 					createSubdomainInput.AppName = fmt.Sprintf("%s-%s", createSubdomainInput.AppName, deploymentTarget.ID[:6])
 					createSubdomainInput.AppName = fmt.Sprintf("%s-%s", createSubdomainInput.AppName, deploymentTarget.ID[:6])
 				}
 				}
@@ -254,7 +252,11 @@ func addPorterSubdomainsIfNecessary(ctx context.Context, app v2.PorterApp, deplo
 				}
 				}
 			}
 			}
 		}
 		}
+
+		services = append(services, service)
 	}
 	}
 
 
+	app.Services = services
+
 	return app, nil
 	return app, nil
 }
 }

+ 63 - 17
api/server/handlers/porter_app/update_app_environment_group.go

@@ -69,7 +69,8 @@ type UpdateAppEnvironmentRequest struct {
 
 
 // UpdateAppEnvironmentResponse represents the fields on the response object from the /apps/{porter_app_name}/environment-group endpoint
 // UpdateAppEnvironmentResponse represents the fields on the response object from the /apps/{porter_app_name}/environment-group endpoint
 type UpdateAppEnvironmentResponse struct {
 type UpdateAppEnvironmentResponse struct {
-	EnvGroups []environment_groups.EnvironmentGroup `json:"env_groups"`
+	Base64AppProto string                                `json:"b64_app_proto"`
+	EnvGroups      []environment_groups.EnvironmentGroup `json:"env_groups"`
 }
 }
 
 
 // ServeHTTP updates or creates the environment group for an app
 // ServeHTTP updates or creates the environment group for an app
@@ -114,24 +115,35 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 	}
 	}
 	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetID})
 	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetID})
 
 
+	appProto := &porterv1.PorterApp{}
+
 	if request.Base64AppProto == "" {
 	if request.Base64AppProto == "" {
-		err := telemetry.Error(ctx, span, nil, "b64 yaml is empty")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-		return
-	}
+		if appName == "" {
+			err := telemetry.Error(ctx, span, nil, "app name is empty and no base64 proto provided")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
 
 
-	decoded, err := base64.StdEncoding.DecodeString(request.Base64AppProto)
-	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error decoding base yaml")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
+		appProto.Name = appName
+	} else {
+		decoded, err := base64.StdEncoding.DecodeString(request.Base64AppProto)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error decoding base yaml")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+
+		err = helpers.UnmarshalContractObject(decoded, appProto)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error unmarshalling app proto")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
 	}
 	}
 
 
-	appProto := &porterv1.PorterApp{}
-	err = helpers.UnmarshalContractObject(decoded, appProto)
-	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error unmarshalling app proto")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+	if appProto.Name == "" {
+		err := telemetry.Error(ctx, span, nil, "app proto name is empty")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 		return
 	}
 	}
 
 
@@ -252,8 +264,25 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 				Version: latestEnvironmentGroup.Version,
 				Version: latestEnvironmentGroup.Version,
 			})
 			})
 
 
+			var protoEnvGroups []*porterv1.EnvGroup
+			for _, envGroup := range latestEnvGroups {
+				protoEnvGroups = append(protoEnvGroups, &porterv1.EnvGroup{
+					Name:    envGroup.Name,
+					Version: int64(envGroup.Version),
+				})
+			}
+			appProto.EnvGroups = protoEnvGroups
+
+			encodedApp, err := encodeAppProto(ctx, appProto)
+			if err != nil {
+				err := telemetry.Error(ctx, span, err, "error encoding app proto")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+				return
+			}
+
 			res := &UpdateAppEnvironmentResponse{
 			res := &UpdateAppEnvironmentResponse{
-				EnvGroups: latestEnvGroups,
+				EnvGroups:      latestEnvGroups,
+				Base64AppProto: encodedApp,
 			}
 			}
 
 
 			c.WriteResult(w, r, res)
 			c.WriteResult(w, r, res)
@@ -363,8 +392,25 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 		Version: version,
 		Version: version,
 	})
 	})
 
 
+	var protoEnvGroups []*porterv1.EnvGroup
+	for _, envGroup := range latestEnvGroups {
+		protoEnvGroups = append(protoEnvGroups, &porterv1.EnvGroup{
+			Name:    envGroup.Name,
+			Version: int64(envGroup.Version),
+		})
+	}
+	appProto.EnvGroups = protoEnvGroups
+
+	encodedApp, err := encodeAppProto(ctx, appProto)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error encoding app proto")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
 	res := &UpdateAppEnvironmentResponse{
 	res := &UpdateAppEnvironmentResponse{
-		EnvGroups: latestEnvGroups,
+		EnvGroups:      latestEnvGroups,
+		Base64AppProto: encodedApp,
 	}
 	}
 
 
 	c.WriteResult(w, r, res)
 	c.WriteResult(w, r, res)

+ 25 - 12
cli/cmd/v2/apply.go

@@ -76,7 +76,7 @@ func Apply(ctx context.Context, inp ApplyInput) error {
 	}
 	}
 
 
 	// overrides incorporated into the app contract baed on the deployment target
 	// overrides incorporated into the app contract baed on the deployment target
-	var b64AppOverrides string
+	var overrides *porter_app.EncodedAppWithEnv
 
 
 	appName := inp.AppName
 	appName := inp.AppName
 	if porterYamlExists {
 	if porterYamlExists {
@@ -107,6 +107,8 @@ func Apply(ctx context.Context, inp ApplyInput) error {
 		}
 		}
 		b64AppProto = parsedApp.B64AppProto
 		b64AppProto = parsedApp.B64AppProto
 
 
+		overrides = parsedApp.PreviewApp
+
 		// override app name if provided
 		// override app name if provided
 		appName, err = appNameFromB64AppProto(parsedApp.B64AppProto)
 		appName, err = appNameFromB64AppProto(parsedApp.B64AppProto)
 		if err != nil {
 		if err != nil {
@@ -137,21 +139,32 @@ func Apply(ctx context.Context, inp ApplyInput) error {
 			return fmt.Errorf("error updating app env group in proto: %w", err)
 			return fmt.Errorf("error updating app env group in proto: %w", err)
 		}
 		}
 
 
-		if inp.PreviewApply && parsedApp.PreviewApp != nil {
-			b64AppOverrides = parsedApp.PreviewApp.B64AppProto
+		color.New(color.FgGreen).Printf("Successfully parsed Porter YAML: applying app \"%s\"\n", appName) // nolint:errcheck,gosec
+	}
 
 
-			envGroupResp, err := client.CreateOrUpdateAppEnvironment(ctx, cliConf.Project, cliConf.Cluster, appName, deploymentTargetID, parsedApp.PreviewApp.EnvVariables, parsedApp.PreviewApp.EnvSecrets, parsedApp.PreviewApp.B64AppProto)
-			if err != nil {
-				return fmt.Errorf("error calling create or update app environment group endpoint: %w", err)
-			}
+	// b64AppOverrides is the base64-encoded app proto with preview environment specific overrides and env groups
+	var b64AppOverrides string
 
 
-			b64AppOverrides, err = updateEnvGroupsInProto(ctx, b64AppOverrides, envGroupResp.EnvGroups)
-			if err != nil {
-				return fmt.Errorf("error updating app env group in proto: %w", err)
-			}
+	if inp.PreviewApply {
+		var previewEnvVariables map[string]string
+		var previewEnvSecrets map[string]string
+
+		if overrides != nil {
+			b64AppOverrides = overrides.B64AppProto
+			previewEnvVariables = overrides.EnvVariables
+			previewEnvSecrets = overrides.EnvSecrets
 		}
 		}
 
 
-		color.New(color.FgGreen).Printf("Successfully parsed Porter YAML: applying app \"%s\"\n", appName) // nolint:errcheck,gosec
+		envGroupResp, err := client.CreateOrUpdateAppEnvironment(ctx, cliConf.Project, cliConf.Cluster, appName, deploymentTargetID, previewEnvVariables, previewEnvSecrets, b64AppOverrides)
+		if err != nil {
+			return fmt.Errorf("error calling create or update app environment group endpoint: %w", err)
+		}
+		b64AppOverrides = envGroupResp.Base64AppProto
+
+		b64AppOverrides, err = updateEnvGroupsInProto(ctx, b64AppOverrides, envGroupResp.EnvGroups)
+		if err != nil {
+			return fmt.Errorf("error updating app env group in proto: %w", err)
+		}
 	}
 	}
 
 
 	if appName == "" {
 	if appName == "" {

+ 3 - 0
dashboard/src/assets/chat.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M19.4666 9.86644V5.06644C19.4666 3.88823 18.5114 2.93311 17.3332 2.93311H4.53324C3.35503 2.93311 2.3999 3.88823 2.3999 5.06644V13.4143C2.3999 14.5925 3.35503 15.5476 4.53324 15.5476H6.2028V19.9998L10.655 15.5476H10.9332M16.4405 18.2838L19.2231 21.0664V18.2838H19.4666C20.6448 18.2838 21.5999 17.3287 21.5999 16.1505V12.5331C21.5999 11.3549 20.6448 10.3998 19.4666 10.3998H13.0666C11.8884 10.3998 10.9332 11.3549 10.9332 12.5331V16.1505C10.9332 17.3287 11.8884 18.2838 13.0666 18.2838H16.4405Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 47 - 54
dashboard/src/components/GCPProvisionerSettings.tsx

@@ -281,70 +281,63 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
       }),
       }),
     });
     });
 
 
-    if (preflightData) {
-      if (props.clusterId) {
-        data["cluster"]["clusterId"] = props.clusterId;
-      }
-
-      try {
-        setIsReadOnly(true);
-        setErrorMessage("");
-        setErrorDetails("")
 
 
-        if (!props.clusterId) {
-          markStepStarted("provisioning-started", region);
-        }
+    if (props.clusterId) {
+      data["cluster"]["clusterId"] = props.clusterId;
+    }
 
 
-        const res = await api.createContract("<token>", data, {
-          project_id: currentProject.id,
-        });
+    try {
+      setIsReadOnly(true);
+      setErrorMessage("");
+      setErrorDetails("")
 
 
-        setErrorMessage("");
-        setErrorDetails("");
-
-        // Only refresh and set clusters on initial create
-        setShouldRefreshClusters(true);
-        api
-          .getClusters("<token>", {}, { id: currentProject.id })
-          .then(({ data }) => {
-            data.forEach((cluster: ClusterType) => {
-              if (cluster.id === res.data.contract_revision?.cluster_id) {
-                // setHasFinishedOnboarding(true);
-                setCurrentCluster(cluster);
-                OFState.actions.goTo("clean_up");
-                pushFiltered(props, "/cluster-dashboard", ["project_id"], {
-                  cluster: cluster.name,
-                });
-              }
-            });
-          })
-          .catch((err) => {
-            setErrorMessage("Error fetching clusters");
-            setErrorDetails(err)
-          });
+      if (!props.clusterId) {
+        markStepStarted("provisioning-started", region);
+      }
 
 
-      } catch (err) {
-        const errMessage = err.response.data.error.replace("unknown: ", "");
-        setIsClicked(false);
-        setIsLoading(true);
+      const res = await api.createContract("<token>", data, {
+        project_id: currentProject.id,
+      });
 
 
-        // TODO: handle different error conditions here from preflights
-        setErrorMessage(DEFAULT_ERROR_MESSAGE);
-        setErrorDetails(errMessage)
-      } finally {
-        setIsReadOnly(false);
-        setIsClicked(false);
-        setIsLoading(true);
+      setErrorMessage("");
+      setErrorDetails("");
+
+      // Only refresh and set clusters on initial create
+      setShouldRefreshClusters(true);
+      api
+        .getClusters("<token>", {}, { id: currentProject.id })
+        .then(({ data }) => {
+          data.forEach((cluster: ClusterType) => {
+            if (cluster.id === res.data.contract_revision?.cluster_id) {
+              // setHasFinishedOnboarding(true);
+              setCurrentCluster(cluster);
+              OFState.actions.goTo("clean_up");
+              pushFiltered(props, "/cluster-dashboard", ["project_id"], {
+                cluster: cluster.name,
+              });
+            }
+          });
+        })
+        .catch((err) => {
+          setErrorMessage("Error fetching clusters");
+          setErrorDetails(err)
+        });
 
 
-      }
-    } else {
+    } catch (err) {
+      const errMessage = err.response.data.error.replace("unknown: ", "");
       setIsClicked(false);
       setIsClicked(false);
       setIsLoading(true);
       setIsLoading(true);
-
+      showIntercomWithMessage({ message: "I am running into an issue provisioning a cluster." });
       // TODO: handle different error conditions here from preflights
       // TODO: handle different error conditions here from preflights
       setErrorMessage(DEFAULT_ERROR_MESSAGE);
       setErrorMessage(DEFAULT_ERROR_MESSAGE);
-      setErrorDetails("Could not perform Preflight Checks ")
+      setErrorDetails(errMessage)
+    } finally {
+      setIsReadOnly(false);
+      setIsClicked(false);
+      setIsLoading(true);
+
     }
     }
+
   };
   };
 
 
   useEffect(() => {
   useEffect(() => {
@@ -539,7 +532,7 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
         </StyledForm>
         </StyledForm>
 
 
         <Button
         <Button
-          disabled={isDisabled() || isLoading || preflightFailed || statusPreflight() != ""}
+          disabled={isDisabled() || isLoading}
           onClick={createCluster}
           onClick={createCluster}
           status={getStatus()}
           status={getStatus()}
         >
         >

+ 57 - 36
dashboard/src/lib/porter-apps/index.ts

@@ -131,6 +131,15 @@ export const porterAppFormValidator = z
         "if using Docker registry or building via a Dockerfile, service must not include `docker run` in its start command; instead, leave the start command empty",
         "if using Docker registry or building via a Dockerfile, service must not include `docker run` in its start command; instead, leave the start command empty",
       path: ["app", "services"],
       path: ["app", "services"],
     }
     }
+  )
+  .refine(
+    ({ app }) => {
+      return app.services.length !== 0;
+    },
+    {
+      message: "app must have at least one service",
+      path: ["app", "services"],
+    }
   );
   );
 export type PorterAppFormData = z.infer<typeof porterAppFormValidator>;
 export type PorterAppFormData = z.infer<typeof porterAppFormValidator>;
 
 
@@ -476,14 +485,10 @@ export function applyPreviewOverrides({
   overrides,
   overrides,
 }: {
 }: {
   app: ClientPorterApp;
   app: ClientPorterApp;
-  overrides: DetectedServices["previews"];
+  overrides?: DetectedServices["previews"];
 }): ClientPorterApp {
 }): ClientPorterApp {
-  if (!overrides) {
-    return app;
-  }
-
   const services = app.services.map((svc) => {
   const services = app.services.map((svc) => {
-    const override = overrides.services.find(
+    const override = overrides?.services.find(
       (s) => s.name.value === svc.name.value
       (s) => s.name.value === svc.name.value
     );
     );
     if (override) {
     if (override) {
@@ -493,24 +498,41 @@ export function applyPreviewOverrides({
       });
       });
 
 
       if (ds.config.type == "web") {
       if (ds.config.type == "web") {
-        ds.config.domains = [];
+        return {
+          ...ds,
+          config: {
+            ...ds.config,
+            domains: [],
+          },
+        };
       }
       }
       return ds;
       return ds;
     }
     }
 
 
     if (svc.config.type == "web") {
     if (svc.config.type == "web") {
-      svc.config.domains = [];
+      return {
+        ...svc,
+        config: {
+          ...svc.config,
+          domains: [],
+        },
+      };
     }
     }
+
     return svc;
     return svc;
   });
   });
-  const additionalServices = overrides.services
-    .filter((s) => !app.services.find((svc) => svc.name.value === s.name.value))
-    .map((svc) => deserializeService({ service: serializeService(svc) }));
+  const additionalServices =
+    overrides?.services
+      .filter(
+        (s) => !app.services.find((svc) => svc.name.value === s.name.value)
+      )
+      .map((svc) => deserializeService({ service: serializeService(svc) })) ??
+    [];
 
 
   app.services = [...services, ...additionalServices];
   app.services = [...services, ...additionalServices];
 
 
   if (app.predeploy) {
   if (app.predeploy) {
-    const predeployOverride = overrides.predeploy;
+    const predeployOverride = overrides?.predeploy;
     if (predeployOverride) {
     if (predeployOverride) {
       app.predeploy = [
       app.predeploy = [
         deserializeService({
         deserializeService({
@@ -521,33 +543,32 @@ export function applyPreviewOverrides({
     }
     }
   }
   }
 
 
-  const envOverrides = overrides.variables;
-  if (envOverrides) {
-    const env = app.env.map((e) => {
-      const override = envOverrides[e.key];
-      if (override) {
-        return {
-          ...e,
-          locked: true,
-          value: override,
-        };
-      }
-
-      return e;
-    });
+  const envOverrides = overrides?.variables;
 
 
-    const additionalEnv = Object.entries(envOverrides)
-      .filter(([key]) => !app.env.find((e) => e.key === key))
-      .map(([key, value]) => ({
-        key,
-        value,
-        hidden: false,
+  const env = app.env.map((e) => {
+    const override = envOverrides?.[e.key];
+    if (override) {
+      return {
+        ...e,
         locked: true,
         locked: true,
-        deleted: false,
-      }));
+        value: override,
+      };
+    }
 
 
-    app.env = [...env, ...additionalEnv];
-  }
+    return e;
+  });
+
+  const additionalEnv = Object.entries(envOverrides ?? {})
+    .filter(([key]) => !app.env.find((e) => e.key === key))
+    .map(([key, value]) => ({
+      key,
+      value,
+      hidden: false,
+      locked: true,
+      deleted: false,
+    }));
+
+  app.env = [...env, ...additionalEnv];
 
 
   return app;
   return app;
 }
 }

+ 3 - 8
dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx

@@ -392,9 +392,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
         const appErrors = Object.keys(errors.app ?? {});
         const appErrors = Object.keys(errors.app ?? {});
         if (appErrors.includes("build")) {
         if (appErrors.includes("build")) {
           errorMessage = "Build settings are not properly configured.";
           errorMessage = "Build settings are not properly configured.";
-        }
-
-        if (appErrors.includes("services")) {
+        } else if (appErrors.includes("services")) {
           errorMessage = "Service settings are not properly configured";
           errorMessage = "Service settings are not properly configured";
           if (
           if (
             errors.app?.services?.root?.message ||
             errors.app?.services?.root?.message ||
@@ -405,11 +403,8 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
               errors.app?.services?.message;
               errors.app?.services?.message;
             errorMessage = `${errorMessage} - ${serviceErrorMessage}`;
             errorMessage = `${errorMessage} - ${serviceErrorMessage}`;
           }
           }
-          errorMessage = `${errorMessage}.`;
-        }
-
-        // this is the high level error message coming from the apply
-        if (appErrors.includes("message")) {
+          errorMessage = `${errorMessage}. To undo all changes, refresh the page.`;
+        } else if (appErrors.includes("message")) {  // this is the high level error message coming from the apply
           errorMessage = errors.app?.message ?? errorMessage;
           errorMessage = errors.app?.message ?? errorMessage;
         }
         }
       }
       }

+ 12 - 3
dashboard/src/main/home/app-dashboard/new-app-flow/GithubActionModal.tsx

@@ -59,6 +59,7 @@ const GithubActionModal: React.FC<Props> = ({
         projectId,
         projectId,
         clusterId,
         clusterId,
         stackName,
         stackName,
+        branch,
         porterYamlPath
         porterYamlPath
       );
       );
     }
     }
@@ -72,6 +73,14 @@ const GithubActionModal: React.FC<Props> = ({
     );
     );
   }, [type]);
   }, [type]);
 
 
+  const headingText = useMemo(() => {
+    if (type === "preview") {
+      return `./github/workflows/porter_preview_${stackName}.yml`;
+    }
+
+    return `./github/workflows/porter_stack_${stackName}.yml`;
+  }, [type, stackName]);
+
   const submit = async () => {
   const submit = async () => {
     if (
     if (
       githubAppInstallationID &&
       githubAppInstallationID &&
@@ -141,7 +150,7 @@ const GithubActionModal: React.FC<Props> = ({
         noWrapper
         noWrapper
         expandText="[+] Show code"
         expandText="[+] Show code"
         collapseText="[-] Hide code"
         collapseText="[-] Hide code"
-        Header={<ModalHeader>.github/workflows/porter.yml</ModalHeader>}
+        Header={<ModalHeader>{headingText}</ModalHeader>}
         isInitiallyExpanded
         isInitiallyExpanded
         spaced
         spaced
         copy={actionYamlContents}
         copy={actionYamlContents}
@@ -226,8 +235,8 @@ const GithubActionModal: React.FC<Props> = ({
 export default withRouter(GithubActionModal);
 export default withRouter(GithubActionModal);
 
 
 const ModalHeader = styled.div`
 const ModalHeader = styled.div`
-  font-weight: 600;
-  font-size: 16px;
+  font-weight: 500;
+  font-size: 14px;
   font-family: monospace;
   font-family: monospace;
   height: 40px;
   height: 40px;
   display: flex;
   display: flex;

+ 4 - 4
dashboard/src/main/home/app-dashboard/new-app-flow/utils.ts

@@ -60,16 +60,16 @@ export const getPreviewGithubAction = (
   branchName: string,
   branchName: string,
   porterYamlPath: string = "porter.yaml"
   porterYamlPath: string = "porter.yaml"
 ) => {
 ) => {
-  return `on:
+  return `"on":
   pull_request:
   pull_request:
-    paths:
-    - *
-    - '!./github/workflows/porter-**'
     branches:
     branches:
     - ${branchName}
     - ${branchName}
     types:
     types:
     - opened
     - opened
     - synchronize
     - synchronize
+    paths:
+    - '**'
+    - '!./github/workflows/porter-**'
     
     
 name: Deploy to Preview Environment
 name: Deploy to Preview Environment
 jobs:
 jobs:

+ 3 - 3
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceStatusFooter.tsx

@@ -60,8 +60,8 @@ const ServiceStatusFooter: React.FC<ServiceStatusFooterProps> = ({
         <>
         <>
             {status.map((versionStatus, i) => {
             {status.map((versionStatus, i) => {
                 return (
                 return (
-                    <>
-                        <StyledStatusFooterTop key={i} expanded={expanded}>
+                    <div key={i}>
+                        <StyledStatusFooterTop expanded={expanded}>
                             <StyledContainer row spaced>
                             <StyledContainer row spaced>
                                 {match(versionStatus)
                                 {match(versionStatus)
                                     .with({ status: "failing" }, (vs) => {
                                     .with({ status: "failing" }, (vs) => {
@@ -127,7 +127,7 @@ const ServiceStatusFooter: React.FC<ServiceStatusFooterProps> = ({
                                 </StyledStatusFooter>
                                 </StyledStatusFooter>
                             </AnimateHeight>
                             </AnimateHeight>
                         )}
                         )}
-                    </>
+                    </div>
                 );
                 );
             })}
             })}
         </>
         </>

+ 25 - 42
dashboard/src/main/home/navbar/Help.tsx

@@ -1,35 +1,26 @@
-import React, { Component } from "react";
+import React, { useState } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
+import community from "assets/chat.svg";
+import { useIntercom } from "lib/hooks/useIntercom";
 
 
-import { Context } from "shared/Context";
-import discordLogo from "../../../assets/discord.svg";
+type HelpProps = {};
 
 
-type PropsType = {};
+const Help: React.FC<HelpProps> = () => {
+  const [showHelpDropdown, setShowHelpDropdown] = useState(false);
 
 
-type StateType = {
-  showHelpDropdown: boolean;
-};
-
-export default class Help extends Component<PropsType, StateType> {
-  state = {
-    showHelpDropdown: false,
-  };
+  const { showIntercomWithMessage } = useIntercom();
 
 
-  renderHelpDropdown = () => {
-    if (this.state.showHelpDropdown) {
+  const renderHelpDropdown = () => {
+    if (showHelpDropdown) {
       return (
       return (
         <>
         <>
           <CloseOverlay
           <CloseOverlay
-            onClick={() =>
-              this.setState({
-                showHelpDropdown: false,
-              })
-            }
+            onClick={() => setShowHelpDropdown(false)}
           />
           />
           <Dropdown dropdownWidth="155px" dropdownMaxHeight="300px">
           <Dropdown dropdownWidth="155px" dropdownMaxHeight="300px">
             <Option
             <Option
               onClick={() => {
               onClick={() => {
-                window.open("https://docs.porter.run", "_blank").focus();
+                window.open("https://docs.porter.run", "_blank")?.focus();
               }}
               }}
             >
             >
               <i className="material-icons-outlined">book</i>
               <i className="material-icons-outlined">book</i>
@@ -37,11 +28,11 @@ export default class Help extends Component<PropsType, StateType> {
             </Option>
             </Option>
             <Option
             <Option
               onClick={() => {
               onClick={() => {
-                window.open("https://discord.gg/Vbse9vJtPU", "_blank").focus();
+                showIntercomWithMessage({ message: "I need help with...", delaySeconds: 0 });
               }}
               }}
             >
             >
-              <Icon src={discordLogo} />
-              Community
+              <Icon src={community} />
+              Talk to us
             </Option>
             </Option>
           </Dropdown>
           </Dropdown>
         </>
         </>
@@ -49,26 +40,18 @@ export default class Help extends Component<PropsType, StateType> {
     }
     }
   };
   };
 
 
-  render() {
-    return (
-      <FeedbackButton selected={this.state.showHelpDropdown === true}>
-        <Flex
-          onClick={() =>
-            this.setState({
-              showHelpDropdown: !this.state.showHelpDropdown,
-            })
-          }
-        >
-          <i className="material-icons-outlined">help_outline</i>
-          Help
-        </Flex>
-        {this.renderHelpDropdown()}
-      </FeedbackButton>
-    );
-  }
-}
+  return (
+    <FeedbackButton selected={showHelpDropdown === true}>
+      <Flex onClick={() => setShowHelpDropdown(!showHelpDropdown)}>
+        <i className="material-icons-outlined">help_outline</i>
+        Help
+      </Flex>
+      {renderHelpDropdown()}
+    </FeedbackButton>
+  );
+};
 
 
-Help.contextType = Context;
+export default Help;
 
 
 const Option = styled.div`
 const Option = styled.div`
   margin-left: 12px;
   margin-left: 12px;

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

@@ -194,8 +194,8 @@ func getStackApplyActionYAML(opts *GetStackApplyActionYAMLOpts) ([]byte, error)
 			On: GithubActionYAMLOnPullRequest{
 			On: GithubActionYAMLOnPullRequest{
 				PullRequest: GithubActionYAMLOnPullRequestTypes{
 				PullRequest: GithubActionYAMLOnPullRequestTypes{
 					Paths: []string{
 					Paths: []string{
-						"*",
 						"!./github/workflows/porter-**",
 						"!./github/workflows/porter-**",
+						"**",
 					},
 					},
 					Branches: []string{
 					Branches: []string{
 						opts.DefaultBranch,
 						opts.DefaultBranch,