فهرست منبع

[POR-1885] [POR-1864] Add links commit sha to events, clear up unnumbered revisions (#3810)

Feroze Mohideen 2 سال پیش
والد
کامیت
9cdd24a2ee

+ 5 - 2
cli/cmd/v2/app_events.go

@@ -13,7 +13,7 @@ import (
 	"github.com/porter-dev/porter/internal/telemetry"
 	"github.com/porter-dev/porter/internal/telemetry"
 )
 )
 
 
-func createBuildEvent(ctx context.Context, client api.Client, applicationName string, projectId uint, clusterId uint, deploymentTargetID string) (string, error) {
+func createBuildEvent(ctx context.Context, client api.Client, applicationName string, projectId uint, clusterId uint, deploymentTargetID string, commitSHA string) (string, error) {
 	ctx, span := telemetry.NewSpan(ctx, "create-build-event")
 	ctx, span := telemetry.NewSpan(ctx, "create-build-event")
 	defer span.End()
 	defer span.End()
 
 
@@ -53,6 +53,8 @@ func createBuildEvent(ctx context.Context, client api.Client, applicationName st
 		}
 		}
 	}
 	}
 
 
+	req.Metadata["commit_sha"] = commitSHA
+
 	event, err := client.CreateOrUpdatePorterAppEvent(ctx, projectId, clusterId, applicationName, req)
 	event, err := client.CreateOrUpdatePorterAppEvent(ctx, projectId, clusterId, applicationName, req)
 	if err != nil {
 	if err != nil {
 		fmt.Println("could not create build event")
 		fmt.Println("could not create build event")
@@ -62,7 +64,7 @@ func createBuildEvent(ctx context.Context, client api.Client, applicationName st
 	return event.ID, nil
 	return event.ID, nil
 }
 }
 
 
-func createPredeployEvent(ctx context.Context, client api.Client, applicationName string, projectId, clusterId uint, deploymentTargetID string, createdAt time.Time, appRevisionID string) (string, error) {
+func createPredeployEvent(ctx context.Context, client api.Client, applicationName string, projectId, clusterId uint, deploymentTargetID string, createdAt time.Time, appRevisionID string, commitSHA string) (string, error) {
 	ctx, span := telemetry.NewSpan(ctx, "create-predeploy-event")
 	ctx, span := telemetry.NewSpan(ctx, "create-predeploy-event")
 	defer span.End()
 	defer span.End()
 
 
@@ -74,6 +76,7 @@ func createPredeployEvent(ctx context.Context, client api.Client, applicationNam
 	}
 	}
 	req.Metadata["start_time"] = createdAt
 	req.Metadata["start_time"] = createdAt
 	req.Metadata["app_revision_id"] = appRevisionID
 	req.Metadata["app_revision_id"] = appRevisionID
+	req.Metadata["commit_sha"] = commitSHA
 
 
 	event, err := client.CreateOrUpdatePorterAppEvent(ctx, projectId, clusterId, applicationName, req)
 	event, err := client.CreateOrUpdatePorterAppEvent(ctx, projectId, clusterId, applicationName, req)
 	if err != nil {
 	if err != nil {

+ 2 - 2
cli/cmd/v2/apply.go

@@ -181,7 +181,7 @@ func Apply(ctx context.Context, inp ApplyInput) error {
 	if applyResp.CLIAction == porterv1.EnumCLIAction_ENUM_CLI_ACTION_BUILD {
 	if applyResp.CLIAction == porterv1.EnumCLIAction_ENUM_CLI_ACTION_BUILD {
 		color.New(color.FgGreen).Printf("Building new image...\n") // nolint:errcheck,gosec
 		color.New(color.FgGreen).Printf("Building new image...\n") // nolint:errcheck,gosec
 
 
-		eventID, _ := createBuildEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID)
+		eventID, _ := createBuildEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, commitSHA)
 
 
 		reportBuildFailureInput := reportBuildFailureInput{
 		reportBuildFailureInput := reportBuildFailureInput{
 			client:             client,
 			client:             client,
@@ -279,7 +279,7 @@ func Apply(ctx context.Context, inp ApplyInput) error {
 		color.New(color.FgGreen).Printf("Waiting for predeploy to complete...\n") // nolint:errcheck,gosec
 		color.New(color.FgGreen).Printf("Waiting for predeploy to complete...\n") // nolint:errcheck,gosec
 
 
 		now := time.Now().UTC()
 		now := time.Now().UTC()
-		eventID, _ := createPredeployEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, now, applyResp.AppRevisionId)
+		eventID, _ := createPredeployEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, now, applyResp.AppRevisionId, commitSHA)
 		metadata := make(map[string]interface{})
 		metadata := make(map[string]interface{})
 		eventStatus := types.PorterAppEventStatus_Success
 		eventStatus := types.PorterAppEventStatus_Success
 		for {
 		for {

+ 10 - 8
dashboard/src/components/porter/Link.tsx

@@ -10,6 +10,7 @@ type Props = {
   hasunderline?: boolean;
   hasunderline?: boolean;
   color?: string;
   color?: string;
   hoverColor?: string;
   hoverColor?: string;
+  showTargetBlankIcon?: boolean;
 };
 };
 
 
 const Link: React.FC<Props> = ({
 const Link: React.FC<Props> = ({
@@ -20,16 +21,15 @@ const Link: React.FC<Props> = ({
   hasunderline,
   hasunderline,
   color = "#ffffff",
   color = "#ffffff",
   hoverColor,
   hoverColor,
+  showTargetBlankIcon = true,
 }) => {
 }) => {
   return (
   return (
     <LinkWrapper hoverColor={hoverColor} color={color}>
     <LinkWrapper hoverColor={hoverColor} color={color}>
       {to ? (
       {to ? (
         <StyledLink to={to} target={target} color={color}>
         <StyledLink to={to} target={target} color={color}>
           {children}
           {children}
-          {target === "_blank" && (
-            <div>
-              <Svg data-testid="geist-icon" fill="none" height="1em" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round" strokeLinejoin="round" stroke-width="2" viewBox="0 0 24 24" width="1em" data-darkreader-inline-stroke="" data-darkreader-inline-color=""><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path><path d="M15 3h6v6"></path><path d="M10 14L21 3"></path></Svg>
-            </div>
+          {target === "_blank" && showTargetBlankIcon && (
+              <Svg color={color} hoverColor={hoverColor} data-testid="geist-icon" fill="none" height="1em" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round" strokeLinejoin="round" stroke-width="2" viewBox="0 0 24 24" width="1em" data-darkreader-inline-stroke="" data-darkreader-inline-color=""><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path><path d="M15 3h6v6"></path><path d="M10 14L21 3"></path></Svg>
           )}
           )}
         </StyledLink>
         </StyledLink>
       ) : (
       ) : (
@@ -44,11 +44,9 @@ const Link: React.FC<Props> = ({
 
 
 export default Link;
 export default Link;
 
 
-const Svg = styled.svg`
-  margin-bottom: -1px;
+const Svg = styled.svg<{ color: string, hoverColor?: string }>`
   margin-left: 5px;
   margin-left: 5px;
-  color: #ffffff;
-  stroke: #ffffff;
+  stroke: ${(props) => props.color};
   stroke-width: 2;
   stroke-width: 2;
 `;
 `;
 
 
@@ -93,5 +91,9 @@ const LinkWrapper = styled.span<{ hoverColor?: string, color: string }>`
     ${Underline} {
     ${Underline} {
       background-color: ${({ hoverColor, color }) => hoverColor ?? color};
       background-color: ${({ hoverColor, color }) => hoverColor ?? color};
     }
     }
+
+    svg {
+      stroke: ${({ hoverColor, color }) => hoverColor ?? color};
+    }
   };
   };
 `;
 `;

+ 47 - 13
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/BuildEventCard.tsx

@@ -12,29 +12,43 @@ import Spacer from "components/porter/Spacer";
 import Link from "components/porter/Link";
 import Link from "components/porter/Link";
 import Icon from "components/porter/Icon";
 import Icon from "components/porter/Icon";
 import { getDuration, getStatusColor, getStatusIcon, triggerWorkflow } from '../utils';
 import { getDuration, getStatusColor, getStatusIcon, triggerWorkflow } from '../utils';
-import { StyledEventCard } from "./EventCard";
+import { Code, ImageTagContainer, CommitIcon, StyledEventCard } from "./EventCard";
 import document from "assets/document.svg";
 import document from "assets/document.svg";
 import { PorterAppBuildEvent } from "../types";
 import { PorterAppBuildEvent } from "../types";
-import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
+import { match } from "ts-pattern";
+import pull_request_icon from "assets/pull_request_icon.svg";
+import { PorterAppRecord } from "main/home/app-dashboard/app-view/AppView";
 
 
 type Props = {
 type Props = {
   event: PorterAppBuildEvent;
   event: PorterAppBuildEvent;
   appName: string;
   appName: string;
   projectId: number;
   projectId: number;
   clusterId: number;
   clusterId: number;
+  gitCommitUrl: string;
+  displayCommitSha: string;
+  porterApp: PorterAppRecord;
 };
 };
 
 
-const BuildEventCard: React.FC<Props> = ({ event, appName, projectId, clusterId }) => {
-  const { porterApp } = useLatestRevision();
+const BuildEventCard: React.FC<Props> = ({ 
+  event, 
+  appName, 
+  projectId, 
+  clusterId,
+  gitCommitUrl,
+  displayCommitSha, 
+  porterApp,
+}) => {
   const renderStatusText = (event: PorterAppBuildEvent) => {
   const renderStatusText = (event: PorterAppBuildEvent) => {
-    switch (event.status) {
-      case "SUCCESS":
-        return <Text color={getStatusColor(event.status)}>Build succeeded</Text>;
-      case "FAILED":
-        return <Text color={getStatusColor(event.status)}>Build failed</Text>;
-      default:
-        return <Text color={getStatusColor(event.status)}>Build in progress...</Text>;
-    }
+    const color = getStatusColor(event.status);
+    return (
+      <StatusContainer color={color}>
+        {match(event.status)
+          .with("SUCCESS", () => "Build successful")
+          .with("FAILED", () => "Build failed")
+          .otherwise(() => "Build in progress...")
+        }
+      </StatusContainer>
+    );
   };
   };
 
 
   const renderInfoCta = (event: PorterAppBuildEvent) => {
   const renderInfoCta = (event: PorterAppBuildEvent) => {
@@ -48,7 +62,7 @@ const BuildEventCard: React.FC<Props> = ({ event, appName, projectId, clusterId
               <Container row>
               <Container row>
                 <Icon src={document} height="10px" />
                 <Icon src={document} height="10px" />
                 <Spacer inline width="5px" />
                 <Spacer inline width="5px" />
-                View details
+                View build logs
               </Container>
               </Container>
             </Link>
             </Link>
             <Spacer inline x={1} />
             <Spacer inline x={1} />
@@ -88,6 +102,17 @@ const BuildEventCard: React.FC<Props> = ({ event, appName, projectId, clusterId
           <Icon height="16px" src={build} />
           <Icon height="16px" src={build} />
           <Spacer inline width="10px" />
           <Spacer inline width="10px" />
           <Text>Application build</Text>
           <Text>Application build</Text>
+          {gitCommitUrl && displayCommitSha &&
+            <>
+              <Spacer inline x={0.5} />
+              <ImageTagContainer>
+                <Link to={gitCommitUrl} target="_blank" showTargetBlankIcon={false}>
+                  <CommitIcon src={pull_request_icon} />
+                  <Code>{displayCommitSha}</Code>
+                </Link>
+              </ImageTagContainer> 
+            </>
+          }
         </Container>
         </Container>
         <Container row>
         <Container row>
           <Icon height="14px" src={run_for} />
           <Icon height="14px" src={run_for} />
@@ -113,5 +138,14 @@ const BuildEventCard: React.FC<Props> = ({ event, appName, projectId, clusterId
 export default BuildEventCard;
 export default BuildEventCard;
 
 
 const Wrapper = styled.div`
 const Wrapper = styled.div`
+  display: flex;
+  height: 20px;
   margin-top: -3px;
   margin-top: -3px;
 `;
 `;
+
+const StatusContainer = styled.div<{ color: string }>`
+  display: flex;
+  align-items: center;
+  color: ${props => props.color};
+  font-size: 13px;
+`;

+ 77 - 88
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/DeployEventCard.tsx

@@ -5,15 +5,16 @@ import Container from "components/porter/Container";
 import Spacer from "components/porter/Spacer";
 import Spacer from "components/porter/Spacer";
 import Icon from "components/porter/Icon";
 import Icon from "components/porter/Icon";
 import { getStatusColor, getStatusIcon } from '../utils';
 import { getStatusColor, getStatusIcon } from '../utils';
-import { StyledEventCard } from "./EventCard";
+import { ImageTagContainer, CommitIcon, StyledEventCard } from "./EventCard";
 import styled from "styled-components";
 import styled from "styled-components";
 import Link from "components/porter/Link";
 import Link from "components/porter/Link";
 import { PorterAppDeployEvent } from "../types";
 import { PorterAppDeployEvent } from "../types";
 import AnimateHeight from "react-animate-height";
 import AnimateHeight from "react-animate-height";
 import ServiceStatusDetail from "./ServiceStatusDetail";
 import ServiceStatusDetail from "./ServiceStatusDetail";
-import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
 import { useRevisionList } from "lib/hooks/useRevisionList";
 import { useRevisionList } from "lib/hooks/useRevisionList";
 import RevisionDiffModal from "../modals/RevisionDiffModal";
 import RevisionDiffModal from "../modals/RevisionDiffModal";
+import pull_request_icon from "assets/pull_request_icon.svg";
+import { match } from "ts-pattern";
 
 
 type Props = {
 type Props = {
   event: PorterAppDeployEvent;
   event: PorterAppDeployEvent;
@@ -22,10 +23,20 @@ type Props = {
   deploymentTargetId: string;
   deploymentTargetId: string;
   projectId: number;
   projectId: number;
   clusterId: number;
   clusterId: number;
+  gitCommitUrl: string;
+  displayCommitSha: string;
 };
 };
 
 
-const DeployEventCard: React.FC<Props> = ({ event, appName, deploymentTargetId, projectId, clusterId, showServiceStatusDetail = false }) => {
-  const { latestRevision } = useLatestRevision();
+const DeployEventCard: React.FC<Props> = ({ 
+  event, 
+  appName, 
+  deploymentTargetId, 
+  projectId, 
+  clusterId, 
+  showServiceStatusDetail = false,
+  gitCommitUrl,
+  displayCommitSha, 
+}) => {
   const [diffModalVisible, setDiffModalVisible] = useState(false);
   const [diffModalVisible, setDiffModalVisible] = useState(false);
   const [revertModalVisible, setRevertModalVisible] = useState(false);
   const [revertModalVisible, setRevertModalVisible] = useState(false);
   const [serviceStatusVisible, setServiceStatusVisible] = useState(showServiceStatusDetail);
   const [serviceStatusVisible, setServiceStatusVisible] = useState(showServiceStatusDetail);
@@ -33,94 +44,54 @@ const DeployEventCard: React.FC<Props> = ({ event, appName, deploymentTargetId,
   const { revisionIdToNumber, numberToRevisionId } = useRevisionList({ appName, deploymentTargetId, projectId, clusterId });
   const { revisionIdToNumber, numberToRevisionId } = useRevisionList({ appName, deploymentTargetId, projectId, clusterId });
 
 
   const renderStatusText = () => {
   const renderStatusText = () => {
-    switch (event.status) {
-      case "SUCCESS":
-        return event.metadata.image_tag != null ?
-          event.metadata.service_deployment_metadata != null ?
-            <StatusTextContainer>
-              <Text color={getStatusColor(event.status)}>
-                Deployed <Code>{event.metadata.image_tag}</Code> to
-              </Text>
-              <Spacer inline x={0.25} />
-              {renderServiceDropdownCta(Object.keys(event.metadata.service_deployment_metadata).length, getStatusColor(event.status))}
-            </StatusTextContainer>
-            :
-            <Text color={getStatusColor(event.status)}>
-              Deployed <Code>{event.metadata.image_tag}</Code>
-            </Text>
-          :
-          <Text color={getStatusColor(event.status)}>
-            Deployment successful
-          </Text>;
-      case "FAILED":
-        if (event.metadata.service_deployment_metadata != null) {
-          let failedServices = 0;
-          for (const key in event.metadata.service_deployment_metadata) {
-            if (event.metadata.service_deployment_metadata[key].status === "FAILED") {
-              failedServices++;
-            }
-          }
-          return (
-            <StatusTextContainer>
-              <Text color={getStatusColor(event.status)}>
-                Failed to deploy <Code>{event.metadata.image_tag}</Code> to
-              </Text>
-              <Spacer inline x={0.25} />
-              {renderServiceDropdownCta(failedServices, getStatusColor(event.status))}
-            </StatusTextContainer>
-          );
-        } else {
-          return (
-            <Text color={getStatusColor(event.status)}>
-              Deployment failed
-            </Text>
-          );
+    const versionNumber = revisionIdToNumber[event.metadata.app_revision_id];
+    const serviceMetadata = event.metadata.service_deployment_metadata;
+  
+    const getStatusText = (status: string, text: string, numServices: number, addEllipsis?: boolean) => {
+      if (versionNumber) {
+        text += ` version ${versionNumber}`;
+      }
+  
+      return serviceMetadata != null ? (
+        <StatusTextContainer>
+          <Text color={getStatusColor(status)}>{text} to</Text>
+          <Spacer inline x={0.25} />
+          {renderServiceDropdownCta(numServices, getStatusColor(status))}
+        </StatusTextContainer>
+      ) : (
+        <Text color={getStatusColor(status)}>{text} {addEllipsis && "..."}</Text>
+      );
+    };
+  
+    let failedServices = 0;
+    let canceledServices = 0;
+    let successfulServices = 0;
+    let progressingServices = 0;
+  
+    if (serviceMetadata != null) {
+      for (const key in serviceMetadata) {
+        if (serviceMetadata[key].status === "FAILED") {
+          failedServices++;
         }
         }
-      case "CANCELED":
-        if (event.metadata.service_deployment_metadata != null) {
-          let canceledServices = 0;
-          for (const key in event.metadata.service_deployment_metadata) {
-            if (event.metadata.service_deployment_metadata[key].status === "CANCELED") {
-              canceledServices++;
-            }
-          }
-          return (
-            <StatusTextContainer>
-              <Text color={getStatusColor(event.status)}>
-                Canceled deploy of <Code>{event.metadata.image_tag}</Code> to
-              </Text>
-              <Spacer inline x={0.25} />
-              {renderServiceDropdownCta(canceledServices, getStatusColor(event.status))}
-            </StatusTextContainer>
-          );
-        } else {
-          return (
-            <Text color={getStatusColor(event.status)}>
-              Deployment canceled
-            </Text>
-          );
+        if (serviceMetadata[key].status === "CANCELED") {
+          canceledServices++;
         }
         }
-      default:
-        if (event.metadata.service_deployment_metadata != null) {
-          return (
-            <StatusTextContainer>
-              <Text color={getStatusColor(event.status)}>
-                Deploying <Code>{event.metadata.image_tag}</Code> to
-              </Text>
-              <Spacer inline x={0.25} />
-              {renderServiceDropdownCta(Object.keys(event.metadata.service_deployment_metadata).length, getStatusColor(event.status))}
-            </StatusTextContainer>
-          );
-        } else {
-          return (
-            <Text color={getStatusColor(event.status)}>
-              Deploying <Code>{event.metadata.image_tag}</Code>...
-            </Text>
-          );
+        if (serviceMetadata[key].status === "SUCCESS") {
+          successfulServices++;
         }
         }
+        if (serviceMetadata[key].status === "PROGRESSING") {
+          progressingServices++;
+        }
+      }
     }
     }
+  
+    return match(event.status)
+      .with("SUCCESS", () => getStatusText(event.status, "Deployed", successfulServices))
+      .with("FAILED", () => getStatusText(event.status, "Failed to deploy", failedServices))
+      .with("CANCELED", () => getStatusText(event.status, "Canceled deployment", canceledServices))
+      .otherwise(() => getStatusText(event.status, "Deploying", progressingServices, true));
   };
   };
-
+  
   const renderRevisionDiffModal = (event: PorterAppDeployEvent) => {
   const renderRevisionDiffModal = (event: PorterAppDeployEvent) => {
     const changedRevisionId = event.metadata.app_revision_id;
     const changedRevisionId = event.metadata.app_revision_id;
     const changedRevisionNumber = revisionIdToNumber[event.metadata.app_revision_id];
     const changedRevisionNumber = revisionIdToNumber[event.metadata.app_revision_id];
@@ -168,7 +139,25 @@ const DeployEventCard: React.FC<Props> = ({ event, appName, deploymentTargetId,
         <Container row>
         <Container row>
           <Icon height="16px" src={deploy} />
           <Icon height="16px" src={deploy} />
           <Spacer inline width="10px" />
           <Spacer inline width="10px" />
-          <Text>Application version no. {revisionIdToNumber[event.metadata.app_revision_id]}</Text>
+          <Text>Application deploy</Text>
+          {gitCommitUrl && displayCommitSha ?
+            <>
+              <Spacer inline x={0.5} />
+              <ImageTagContainer>
+                <Link to={gitCommitUrl} target="_blank" showTargetBlankIcon={false}>
+                  <CommitIcon src={pull_request_icon} />
+                  <Code>{displayCommitSha}</Code>
+                </Link>
+              </ImageTagContainer> 
+            </>
+            :
+            <>
+              <Spacer inline x={0.5} />
+              <ImageTagContainer hoverable={false}>
+                <Code>{event.metadata.image_tag}</Code>
+              </ImageTagContainer> 
+            </>
+          }
         </Container>
         </Container>
       </Container>
       </Container>
       <Spacer y={0.5} />
       <Spacer y={0.5} />

+ 112 - 5
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/EventCard.tsx

@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useMemo } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
 import BuildEventCard from "./BuildEventCard";
 import BuildEventCard from "./BuildEventCard";
@@ -7,6 +7,7 @@ import AppEventCard from "./AppEventCard";
 import DeployEventCard from "./DeployEventCard";
 import DeployEventCard from "./DeployEventCard";
 import { PorterAppEvent } from "../types";
 import { PorterAppEvent } from "../types";
 import { match } from "ts-pattern";
 import { match } from "ts-pattern";
+import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
 
 
 type Props = {
 type Props = {
   event: PorterAppEvent;
   event: PorterAppEvent;
@@ -18,11 +19,95 @@ type Props = {
 };
 };
 
 
 const EventCard: React.FC<Props> = ({ event, deploymentTargetId, isLatestDeployEvent, projectId, clusterId, appName }) => {
 const EventCard: React.FC<Props> = ({ event, deploymentTargetId, isLatestDeployEvent, projectId, clusterId, appName }) => {
+  const { porterApp } = useLatestRevision();
+
+  const gitCommitUrl = useMemo(() => {
+    if (!porterApp.repo_name) {
+      return "";
+    }
+
+    return match(event)
+      .with({ type: "APP_EVENT" }, () => "")
+      .with({ type: "BUILD" }, (event) =>
+        event.metadata.commit_sha
+          ? `https://www.github.com/${porterApp.repo_name}/commit/${event.metadata.commit_sha}`
+          : ""
+      )
+      .with({ type: "PRE_DEPLOY" }, (event) =>
+        event.metadata.commit_sha
+          ? `https://www.github.com/${porterApp.repo_name}/commit/${event.metadata.commit_sha}`
+          : ""
+      )
+      .with({ type: "DEPLOY" }, (event) =>
+        event.metadata.image_tag
+          ? `https://www.github.com/${porterApp.repo_name}/commit/${event.metadata.image_tag}`
+          : ""
+      )
+      .exhaustive();
+  }, [JSON.stringify(event), porterApp])
+
+  const displayCommitSha = useMemo(() => {
+    if (!porterApp.repo_name) {
+      return "";
+    }
+
+    return match(event)
+      .with({ type: "APP_EVENT" }, () => "")
+      .with({ type: "BUILD" }, (event) =>
+        event.metadata.commit_sha ? event.metadata.commit_sha.slice(0, 7) : ""
+      )
+      .with({ type: "PRE_DEPLOY" }, (event) =>
+        event.metadata.commit_sha ? event.metadata.commit_sha.slice(0, 7) : ""
+      )
+      .with({ type: "DEPLOY" }, (event) =>
+        event.metadata.image_tag ? event.metadata.image_tag.slice(0, 7) : ""
+      )
+      .exhaustive();
+  }, [JSON.stringify(event), porterApp]);
+
   return match(event)
   return match(event)
-    .with({ type: "APP_EVENT" }, (ev) => <AppEventCard event={ev} deploymentTargetId={deploymentTargetId} projectId={projectId} clusterId={clusterId} appName={appName} />)
-    .with({ type: "BUILD" }, (ev) => <BuildEventCard event={ev} projectId={projectId} clusterId={clusterId} appName={appName} />)
-    .with({ type: "DEPLOY" }, (ev) => <DeployEventCard event={ev} appName={appName} showServiceStatusDetail={isLatestDeployEvent} deploymentTargetId={deploymentTargetId} projectId={projectId} clusterId={clusterId} />)
-    .with({ type: "PRE_DEPLOY" }, (ev) => <PreDeployEventCard event={ev} appName={appName} projectId={projectId} clusterId={clusterId} />)
+    .with({ type: "APP_EVENT" }, (ev) => (
+      <AppEventCard
+        event={ev}
+        deploymentTargetId={deploymentTargetId}
+        projectId={projectId}
+        clusterId={clusterId}
+        appName={appName}
+      />
+    ))
+    .with({ type: "BUILD" }, (ev) => (
+      <BuildEventCard
+        event={ev}
+        projectId={projectId}
+        clusterId={clusterId}
+        appName={appName}
+        gitCommitUrl={gitCommitUrl}
+        displayCommitSha={displayCommitSha}
+        porterApp={porterApp}
+      />
+    ))
+    .with({ type: "DEPLOY" }, (ev) => (
+      <DeployEventCard
+        event={ev}
+        appName={appName}
+        showServiceStatusDetail={isLatestDeployEvent}
+        deploymentTargetId={deploymentTargetId}
+        projectId={projectId}
+        clusterId={clusterId}
+        gitCommitUrl={gitCommitUrl}
+        displayCommitSha={displayCommitSha}
+      />
+    ))
+    .with({ type: "PRE_DEPLOY" }, (ev) => (
+      <PreDeployEventCard
+        event={ev}
+        appName={appName}
+        projectId={projectId}
+        clusterId={clusterId}
+        gitCommitUrl={gitCommitUrl}
+        displayCommitSha={displayCommitSha}
+      />
+    ))
     .exhaustive();
     .exhaustive();
 };
 };
 
 
@@ -53,3 +138,25 @@ export const StyledEventCard = styled.div<{ row?: boolean }>`
     }
     }
   }
   }
 `;
 `;
+
+export const Code = styled.span`
+  font-family: monospace;
+`;
+
+export const CommitIcon = styled.img`
+  height: 12px;
+  margin-right: 3px;
+`;
+
+export const ImageTagContainer = styled.div<{ hoverable?: boolean }>`
+  display: flex;
+  justify-content: center;
+  padding: 3px 5px;
+  border-radius: 5px;
+  background: #ffffff22;
+  user-select: text;
+  ${({hoverable = true}) => hoverable && `:hover {
+    background: #ffffff44;
+    cursor: pointer;
+  }`}
+`;

+ 24 - 3
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/PreDeployEventCard.tsx

@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
 import pre_deploy from "assets/pre_deploy.png";
 import pre_deploy from "assets/pre_deploy.png";
@@ -12,20 +12,30 @@ import Spacer from "components/porter/Spacer";
 import Icon from "components/porter/Icon";
 import Icon from "components/porter/Icon";
 
 
 import { getDuration, getStatusColor, getStatusIcon, triggerWorkflow } from '../utils';
 import { getDuration, getStatusColor, getStatusIcon, triggerWorkflow } from '../utils';
-import { StyledEventCard } from "./EventCard";
+import { Code, ImageTagContainer, CommitIcon, StyledEventCard } from "./EventCard";
 import Link from "components/porter/Link";
 import Link from "components/porter/Link";
 import document from "assets/document.svg";
 import document from "assets/document.svg";
 import { PorterAppPreDeployEvent } from "../types";
 import { PorterAppPreDeployEvent } from "../types";
 import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
 import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
+import pull_request_icon from "assets/pull_request_icon.svg";
 
 
 type Props = {
 type Props = {
   event: PorterAppPreDeployEvent;
   event: PorterAppPreDeployEvent;
   appName: string;
   appName: string;
   projectId: number;
   projectId: number;
   clusterId: number;
   clusterId: number;
+  gitCommitUrl: string;
+  displayCommitSha: string;
 };
 };
 
 
-const PreDeployEventCard: React.FC<Props> = ({ event, appName, projectId, clusterId }) => {
+const PreDeployEventCard: React.FC<Props> = ({ 
+  event,
+  appName,
+  projectId, 
+  clusterId,
+  gitCommitUrl,
+  displayCommitSha, 
+}) => {
   const { porterApp } = useLatestRevision();
   const { porterApp } = useLatestRevision();
 
 
   const renderStatusText = (event: PorterAppPreDeployEvent) => {
   const renderStatusText = (event: PorterAppPreDeployEvent) => {
@@ -48,6 +58,17 @@ const PreDeployEventCard: React.FC<Props> = ({ event, appName, projectId, cluste
           <Icon height="16px" src={pre_deploy} />
           <Icon height="16px" src={pre_deploy} />
           <Spacer inline width="10px" />
           <Spacer inline width="10px" />
           <Text>Application pre-deploy</Text>
           <Text>Application pre-deploy</Text>
+          {gitCommitUrl && displayCommitSha &&
+            <>
+              <Spacer inline x={0.5} />
+              <ImageTagContainer>
+                <Link to={gitCommitUrl} target="_blank" showTargetBlankIcon={false}>
+                  <CommitIcon src={pull_request_icon} />
+                  <Code>{displayCommitSha}</Code>
+                </Link>
+              </ImageTagContainer> 
+            </>
+          }
         </Container>
         </Container>
         <Container row>
         <Container row>
           <Icon height="14px" src={run_for} />
           <Icon height="14px" src={run_for} />

+ 2 - 0
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts

@@ -26,11 +26,13 @@ const porterAppBuildEventMetadataValidator = z.object({
     action_run_id: z.number().optional(),
     action_run_id: z.number().optional(),
     github_account_id: z.number().optional(),
     github_account_id: z.number().optional(),
     end_time: z.string().optional(),
     end_time: z.string().optional(),
+    commit_sha: z.string().optional(),
 })
 })
 const porterAppPreDeployEventMetadataValidator = z.object({
 const porterAppPreDeployEventMetadataValidator = z.object({
     start_time: z.string(),
     start_time: z.string(),
     end_time: z.string().optional(),
     end_time: z.string().optional(),
     app_revision_id: z.string(),
     app_revision_id: z.string(),
+    commit_sha: z.string().optional(),
 });
 });
 export const porterAppEventValidator = z.discriminatedUnion("type", [
 export const porterAppEventValidator = z.discriminatedUnion("type", [
     z.object({
     z.object({

+ 5 - 6
dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx

@@ -1,4 +1,4 @@
-import React, { Dispatch, SetStateAction, useMemo } from "react";
+import React, { Dispatch, SetStateAction } from "react";
 import { PorterApp } from "@porter-dev/api-contracts";
 import { PorterApp } from "@porter-dev/api-contracts";
 import { AppRevision } from "lib/revisions/types";
 import { AppRevision } from "lib/revisions/types";
 import { match } from "ts-pattern";
 import { match } from "ts-pattern";
@@ -7,7 +7,6 @@ import styled from "styled-components";
 import { readableDate } from "shared/string_utils";
 import { readableDate } from "shared/string_utils";
 import Text from "components/porter/Text";
 import Text from "components/porter/Text";
 import { SourceOptions } from "lib/porter-apps";
 import { SourceOptions } from "lib/porter-apps";
-import api from "shared/api";
 
 
 type RevisionTableContentsProps = {
 type RevisionTableContentsProps = {
   latestRevisionNumber: number;
   latestRevisionNumber: number;
@@ -77,14 +76,14 @@ const RevisionTableContents: React.FC<RevisionTableContentsProps> = ({
 
 
   const getTableHeader = (latestRevision?: AppRevision) => {
   const getTableHeader = (latestRevision?: AppRevision) => {
     if (!latestRevision) {
     if (!latestRevision) {
-      return "Revisions";
+      return "Versions";
     }
     }
 
 
     if (previewRevision) {
     if (previewRevision) {
-      return "Previewing revision (not deployed) -";
+      return "Previewing version (not deployed) -";
     }
     }
 
 
-    return "Current revision - ";
+    return "Current version - ";
   };
   };
 
 
   const getSelectedRevisionNumber = (args: {
   const getSelectedRevisionNumber = (args: {
@@ -132,7 +131,7 @@ const RevisionTableContents: React.FC<RevisionTableContentsProps> = ({
           <RevisionsTable>
           <RevisionsTable>
             <tbody>
             <tbody>
               <Tr disableHover>
               <Tr disableHover>
-                <Th>Revision no.</Th>
+                <Th>Version no.</Th>
                 <Th>
                 <Th>
                   {revisionsWithProto[0]?.app_proto.build
                   {revisionsWithProto[0]?.app_proto.build
                     ? "Commit SHA"
                     ? "Commit SHA"