Răsfoiți Sursa

Build activity (#3071)

* Build Card Events

* clean up console logs

---------

Co-authored-by: Justin Rhee <jusrhee@Justins-MacBook-Air.local>
sdess09 3 ani în urmă
părinte
comite
c780bb3037

+ 87 - 0
api/server/handlers/gitinstallation/workflow_log_runid.go

@@ -0,0 +1,87 @@
+package gitinstallation
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+	"strings"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
+	"github.com/porter-dev/porter/api/server/shared/config"
+)
+
+type GetSpecificWorkflowLogsHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewGetSpecificWorkflowLogsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetSpecificWorkflowLogsHandler {
+	return &GetSpecificWorkflowLogsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *GetSpecificWorkflowLogsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
+	if !ok {
+		return
+	}
+
+	releaseName := r.URL.Query().Get("release_name")
+	filename := r.URL.Query().Get("filename")
+	runNumberStr := r.URL.Query().Get("run_id")
+	fmt.Println(runNumberStr)
+
+	if filename == "" && releaseName == "" {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("filename and release name are both empty")))
+		return
+	}
+
+	if filename == "" {
+		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)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// parse runNumber from string to int64
+	runNumber, err := strconv.ParseInt(runNumberStr, 10, 64)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	specificWorkflowRun, _, err := client.Actions.GetWorkflowRunByID(r.Context(), owner, name, runNumber)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	logsURL, _, err := client.Actions.GetWorkflowRunLogs(r.Context(), owner, name, specificWorkflowRun.GetID(), false)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	fmt.Printf("Fetched specific workflow logs URL: %v\n", logsURL.String())
+
+	c.WriteResult(w, r, logsURL.String())
+}

+ 34 - 0
api/server/router/git_installation.go

@@ -768,5 +768,39 @@ func getGitInstallationRoutes(
 		Handler:  getWorkflowLogsHandler,
 		Router:   r,
 	})
+
+	getWorkflowLogByIDEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/{%s}/{%s}/clusters/{cluster_id}/workflow_run_id",
+					relPath,
+					types.URLParamGitRepoOwner,
+					types.URLParamGitRepoName,
+				),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.GitInstallationScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getWorkflowLogByIDHandler := gitinstallation.NewGetSpecificWorkflowLogsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getWorkflowLogByIDEndpoint,
+		Handler:  getWorkflowLogByIDHandler,
+		Router:   r,
+	})
 	return routes, newPath
 }

+ 35 - 30
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -361,7 +361,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
               });
             }
           });
-          console.log(logs);
           setLogs(logs);
         }
       }
@@ -690,7 +689,13 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
       case "events":
         return <EventsTab currentChart={appData.chart} />;
       case "activity":
-        return <ActivityFeed chart={appData.chart} stackName={appData?.app?.name}/>;
+        return (
+          <ActivityFeed
+            chart={appData.chart}
+            stackName={appData?.app?.name}
+            appData={appData}
+          />
+        );
       case "logs":
         return <LogSection currentChart={appData.chart} />;
       case "metrics":
@@ -928,7 +933,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                             <GHALogsModal
                               appData={appData}
                               logs={logs}
-                              modalVisible={false}
+                              modalVisible={modalVisible}
                               setModalVisible={setModalVisible}
                             />
                           )}
@@ -995,8 +1000,34 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                   appData.app.git_repo_id
                     ? hasBuiltImage
                       ? [
+                          { label: "Overview", value: "overview" },
+                          { label: "Activity", value: "activity" },
+                          { label: "Events", value: "events" },
+                          { label: "Logs", value: "logs" },
+                          { label: "Metrics", value: "metrics" },
+                          { label: "Debug", value: "status" },
+                          { label: "Pre-deploy", value: "pre-deploy" },
+                          {
+                            label: "Environment",
+                            value: "environment-variables",
+                          },
+                          { label: "Build settings", value: "build-settings" },
+                          { label: "Settings", value: "settings" },
+                        ]
+                      : [
+                          { label: "Overview", value: "overview" },
+                          { label: "Activity", value: "activity" },
+                          { label: "Pre-deploy", value: "pre-deploy" },
+                          {
+                            label: "Environment",
+                            value: "environment-variables",
+                          },
+                          { label: "Build settings", value: "build-settings" },
+                          { label: "Settings", value: "settings" },
+                        ]
+                    : [
                         { label: "Overview", value: "overview" },
-                        // { label: "Activity", value: "activity" },
+                        { label: "Activity", value: "activity" },
                         { label: "Events", value: "events" },
                         { label: "Logs", value: "logs" },
                         { label: "Metrics", value: "metrics" },
@@ -1006,34 +1037,8 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                           label: "Environment",
                           value: "environment-variables",
                         },
-                        { label: "Build settings", value: "build-settings" },
-                        { label: "Settings", value: "settings" },
-                      ]
-                      : [
-                        { label: "Overview", value: "overview" },
-                        // { label: "Activity", value: "activity" },
-                        { label: "Pre-deploy", value: "pre-deploy" },
-                        {
-                          label: "Environment",
-                          value: "environment-variables",
-                        },
-                        { label: "Build settings", value: "build-settings" },
                         { label: "Settings", value: "settings" },
                       ]
-                    : [
-                      { label: "Overview", value: "overview" },
-                      // { label: "Activity", value: "activity" },
-                      { label: "Events", value: "events" },
-                      { label: "Logs", value: "logs" },
-                      { label: "Metrics", value: "metrics" },
-                      { label: "Debug", value: "status" },
-                      { label: "Pre-deploy", value: "pre-deploy" },
-                      {
-                        label: "Environment",
-                        value: "environment-variables",
-                      },
-                      { label: "Settings", value: "settings" },
-                    ]
                 }
                 currentTab={tab}
                 setCurrentTab={(tab: string) => {

+ 11 - 13
dashboard/src/main/home/app-dashboard/expanded-app/GHABanner.tsx

@@ -44,19 +44,17 @@ const GHABanner: React.FC<Props> = ({
                 </RefreshButton>
               }
             >
-              <Container row spaced>
-                Your application will not be available until you merge
-                <Spacer inline width="5px" />
-                <Link
-                  to={pullRequestUrl}
-                  target="_blank"
-                  hasunderline
-                >
-                  this PR
-                </Link>
-                <Spacer inline width="5px" />
-                into your branch.
-              </Container>
+              Your application will not be available until you merge
+              <Spacer inline width="5px" />
+              <Link
+                to={pullRequestUrl}
+                target="_blank"
+                hasunderline
+              >
+                this PR
+              </Link>
+              <Spacer inline width="5px" />
+              into your branch.
             </Banner>
           ) : (
             <Banner

+ 14 - 17
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx

@@ -18,12 +18,10 @@ import Pagination from "components/porter/Pagination";
 type Props = {
   chart: any;
   stackName: string;
+  appData: string;
 };
 
-const ActivityFeed: React.FC<Props> = ({
-  chart,
-  stackName,
-}) => {
+const ActivityFeed: React.FC<Props> = ({ chart, stackName, appData }) => {
   const { currentProject, currentCluster } = useContext(Context);
 
   const [events, setEvents] = useState<any[]>([]);
@@ -42,7 +40,7 @@ const ActivityFeed: React.FC<Props> = ({
           cluster_id: currentCluster.id,
           project_id: currentProject.id,
           stack_name: stackName,
-          page
+          page,
         }
       );
       setNumPages(res.data.num_pages);
@@ -52,8 +50,8 @@ const ActivityFeed: React.FC<Props> = ({
       setError(err);
       setLoading(false);
     }
-  }
-  
+  };
+
   useEffect(() => {
     getEvents();
   }, [page]);
@@ -75,14 +73,16 @@ const ActivityFeed: React.FC<Props> = ({
         <Loading />
       </div>
     );
-  };
+  }
 
   if (events?.length === 0) {
     return (
       <Fieldset>
         <Text size={16}>No events found for "{stackName}"</Text>
         <Spacer height="15px" />
-        <Text color="helper">This application currently has no associated activity.</Text>
+        <Text color="helper">
+          This application currently has no associated activity.
+        </Text>
       </Fieldset>
     );
   }
@@ -91,18 +91,15 @@ const ActivityFeed: React.FC<Props> = ({
     <StyledActivityFeed>
       {events.map((event, i) => {
         return (
-          <EventWrapper 
-            isLast={i === events.length - 1} 
-            key={i}
-          >
-            {(i !== events.length - 1 && events.length > 1) && <Line />}
+          <EventWrapper isLast={i === events.length - 1} key={i}>
+            {i !== events.length - 1 && events.length > 1 && <Line />}
             <Dot />
             <Time>
               <Text>{feedDate(event.created_at).split(", ")[0]}</Text>
               <Spacer x={0.5} />
               <Text>{feedDate(event.created_at).split(", ")[1]}</Text>
             </Time>
-            <EventCard event={event} i={i} />
+            <EventCard appData={appData} event={event} i={i} />
           </EventWrapper>
         );
       })}
@@ -153,7 +150,7 @@ const EventWrapper = styled.div<{
   display: flex;
   align-items: center;
   position: relative;
-  margin-bottom: ${props => props.isLast ? "" : "25px"};
+  margin-bottom: ${(props) => (props.isLast ? "" : "25px")};
 `;
 
 const StyledActivityFeed = styled.div`
@@ -167,4 +164,4 @@ const StyledActivityFeed = styled.div`
       opacity: 1;
     }
   }
-`;
+`;

+ 202 - 62
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/EventCard.tsx

@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
 import styled from "styled-components";
 
 import app_event from "assets/app_event.png";
@@ -10,6 +10,7 @@ import healthy from "assets/status-healthy.png";
 import failure from "assets/failure.png";
 import run_for from "assets/run_for.png";
 import refresh from "assets/refresh.png";
+import Loading from "components/Loading";
 
 import Text from "components/porter/Text";
 import Container from "components/porter/Container";
@@ -17,17 +18,23 @@ import Spacer from "components/porter/Spacer";
 import Link from "components/porter/Link";
 import Icon from "components/porter/Icon";
 import Modal from "components/porter/Modal";
+import api from "shared/api";
+import { Log } from "main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs";
+import JSZip from "jszip";
+import Anser, { AnserJsonEntry } from "anser";
+import GHALogsModal from "../status/GHALogsModal";
 
 type Props = {
   event: any;
+  appData: any;
 };
 
-const EventCard: React.FC<Props> = ({
-  event,
-  i,
-}) => {
+const EventCard: React.FC<Props> = ({ event, i, appData }) => {
   const [showModal, setShowModal] = useState<boolean>(false);
   const [modalContent, setModalContent] = useState<React.ReactNode>(null);
+  const [logModalVisible, setLogModalVisible] = useState(false);
+  const [logs, setLogs] = useState<Log[]>(null);
+  const [loading, setLoading] = useState<boolean>(true);
 
   const getIcon = (eventType: string) => {
     switch (eventType) {
@@ -41,7 +48,7 @@ const EventCard: React.FC<Props> = ({
         return pre_deploy;
       default:
         return app_event;
-    };
+    }
   };
 
   const getTitle = (eventType: string) => {
@@ -56,7 +63,7 @@ const EventCard: React.FC<Props> = ({
         return "Application pre-deploy";
       default:
         return "";
-    };
+    }
   };
 
   const getStatusIcon = (status: string) => {
@@ -69,65 +76,164 @@ const EventCard: React.FC<Props> = ({
         return loading;
       default:
         return loading;
-    };
+    }
   };
 
   const renderStatusText = (event: any) => {
     if (event.type === "BUILD") {
       switch (event.status) {
         case "SUCCESS":
-          return <Text color="#68BF8B">Build succeeded</Text>
+          return <Text color="#68BF8B">Build succeeded</Text>;
         case "FAILED":
-          return <Text color="#FF6060">Build failed</Text>
+          return <Text color="#FF6060">Build failed</Text>;
         default:
-          return <Text color="#aaaabb66">Build in progress . . </Text>
-      };
-    };
-    
+          return <Text color="#aaaabb66">Build in progress . . </Text>;
+      }
+    }
+
     if (event.type === "DEPLOY") {
       switch (event.status) {
         case "SUCCESS":
-          return <Text color="#68BF8B">Deployed v100</Text>
+          return <Text color="#68BF8B">Deployed v100</Text>;
         case "FAILED":
-          return <Text color="#FF6060">Deploying v100 failed</Text>
+          return <Text color="#FF6060">Deploying v100 failed</Text>;
         default:
-          return <Text color="#aaaabb66">Deploying v100 . . .</Text>
-      };
-    };
-    
+          return <Text color="#aaaabb66">Deploying v100 . . .</Text>;
+      }
+    }
+
     if (event.type === "PRE_DEPLOY") {
       switch (event.status) {
         case "SUCCESS":
-          return <Text color="#68BF8B">Pre-deploy succeeded . . </Text>
+          return <Text color="#68BF8B">Pre-deploy succeeded . . </Text>;
         case "FAILED":
-          return <Text color="#FF6060">Pre-deploy failed . . </Text>
+          return <Text color="#FF6060">Pre-deploy failed . . </Text>;
         default:
-          return <Text color="#aaaabb66">Pre-deploy in progress . . </Text>
-      };
-    };
+          return <Text color="#aaaabb66">Pre-deploy in progress . . </Text>;
+      }
+    }
+  };
+  const triggerWorkflow = async () => {
+    try {
+      const res = await api.reRunGHWorkflow(
+        "",
+        {},
+        {
+          project_id: appData.app.project_id,
+          cluster_id: appData.app.cluster_id,
+          git_installation_id: appData.app.git_repo_id,
+          owner: appData.app.repo_name?.split("/")[0],
+          name: appData.app.repo_name?.split("/")[1],
+          branch: appData.app.branch_name,
+          filename: "porter_stack_" + appData.chart.name + ".yml",
+        }
+      );
+      if (res.data != null) {
+        window.open(res.data, "_blank", "noreferrer");
+      }
+    } catch (error) {
+      console.log(error);
+    }
   };
 
   const renderInfoCta = (event: any) => {
     if (event.type === "APP_EVENT") {
       return (
         <>
-          <Link hasunderline onClick={() => {
-            setModalContent(
-              <>
-                <Container row>
-                  <Icon height="20px" src={app_event} />
-                  <Spacer inline width="10px" />
-                  <Text size={16}>Event details</Text>
-                </Container>
-                <Spacer y={1} />
-                <Text>TODO: display event logs</Text>
-              </>
-            )
-            setShowModal(true);
-          }}>View details</Link>
+          <Link
+            hasunderline
+            onClick={() => {
+              setModalContent(
+                <>
+                  <Container row>
+                    <Icon height="20px" src={app_event} />
+                    <Spacer inline width="10px" />
+                    <Text size={16}>Event details</Text>
+                  </Container>
+                  <Spacer y={1} />
+                  <Text>TODO: display event logs</Text>
+                </>
+              );
+              setShowModal(true);
+            }}
+          >
+            View details
+          </Link>
           <Spacer inline x={1} />
         </>
       );
+    }
+
+    const getBuildLogs = async () => {
+      try {
+        setLogs([]);
+        setLogModalVisible(true);
+
+        const res = await api.getGHWorkflowLogById(
+          "",
+          {},
+          {
+            project_id: appData.app.project_id,
+            cluster_id: appData.app.cluster_id,
+            git_installation_id: appData.app.git_repo_id,
+            owner: appData.app.repo_name?.split("/")[0],
+            name: appData.app.repo_name?.split("/")[1],
+            filename: "porter_stack_" + appData.chart.name + ".yml",
+            run_id: event.metadata.action_run_id,
+          }
+        );
+        let logs: Log[] = [];
+        if (res.data != null) {
+          // Fetch the logs
+          const logsResponse = await fetch(res.data);
+
+          // Ensure that the response body is only read once
+          const logsBlob = await logsResponse.blob();
+
+          if (logsResponse.headers.get("Content-Type") === "application/zip") {
+            const zip = await JSZip.loadAsync(logsBlob);
+            const promises: any[] = [];
+
+            zip.forEach(function (relativePath, zipEntry) {
+              promises.push(
+                (async function () {
+                  const fileData = await zip
+                    .file(relativePath)
+                    ?.async("string");
+
+                  if (
+                    fileData &&
+                    fileData.includes("Run porter-dev/porter-cli-action@v0.1.0")
+                  ) {
+                    const lines = fileData.split("\n");
+
+                    lines.forEach((line, index) => {
+                      const anserLine: AnserJsonEntry[] = Anser.ansiToJson(
+                        line
+                      );
+                      const log: Log = {
+                        line: anserLine,
+                        lineNumber: index + 1,
+                        timestamp: line.match(
+                          /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z/
+                        )?.[0],
+                      };
+
+                      logs.push(log);
+                    });
+                  }
+                })()
+              );
+            });
+
+            await Promise.all(promises);
+            setLogs(logs);
+          }
+        }
+      } catch (error) {
+        console.log(appData);
+        console.log(error);
+      }
     };
 
     if (event.type === "BUILD") {
@@ -135,48 +241,88 @@ const EventCard: React.FC<Props> = ({
         case "SUCCESS":
           return (
             <>
-              <Link hasunderline onClick={() => alert("TODO: open GHA logs modal")}>View logs</Link>
+              <Link hasunderline onClick={() => getBuildLogs()}>
+                View logs
+              </Link>
+
+              {logModalVisible && (
+                <GHALogsModal
+                  appData={appData}
+                  logs={logs}
+                  modalVisible={logModalVisible}
+                  setModalVisible={setLogModalVisible}
+                  actionRunId={event.metadata?.action_run_id}
+                />
+              )}
               <Spacer inline x={1} />
             </>
           );
         case "FAILED":
           return (
             <>
-              <Link hasunderline onClick={() => alert("TODO: open GHA logs modal")}>View logs</Link>
+              <Link hasunderline onClick={() => getBuildLogs()}>
+                View logs
+              </Link>
+
+              {logModalVisible && (
+                <GHALogsModal
+                  appData={appData}
+                  logs={logs}
+                  modalVisible={logModalVisible}
+                  setModalVisible={setLogModalVisible}
+                  actionRunId={event.metadata?.action_run_id}
+                />
+              )}
               <Spacer inline x={1} />
             </>
           );
         default:
           return (
             <>
-              <Link hasunderline onClick={() => alert("TODO: link to GHA")}>View live logs</Link>
+              <Link
+                hasunderline
+                target="_blank"
+                to={`https://github.com/${appData.app.repo_name}/actions/runs/${event.metadata?.action_run_id}`}
+              >
+                View live logs
+              </Link>
               <Spacer inline x={1} />
             </>
           );
-      };
-    };
-    
+      }
+    }
+    useEffect(() => {
+      getBuildLogs();
+    }, []);
+
     if (event.type === "DEPLOY") {
       if (event.type === "FAILED") {
         return (
           <>
-            <Link hasunderline onClick={() => alert("TODO: open deploy logs modal")}>View logs</Link>
+            <Link
+              hasunderline
+              onClick={() => alert("TODO: open deploy logs modal")}
+            >
+              View logs
+            </Link>
             <Spacer inline x={1} />
           </>
         );
       } else {
         return;
-      };
-    };
-    
+      }
+    }
+
     if (event.type === "PRE_DEPLOY") {
       return (
         <>
-          <Link hasunderline onClick={() => alert("TODO: open logs modal")}>View logs</Link>
+          <Link hasunderline onClick={() => alert("TODO: open logs modal")}>
+            View logs
+          </Link>
           <Spacer inline x={1} />
         </>
       );
-    };
+    }
   };
 
   return (
@@ -203,13 +349,11 @@ const EventCard: React.FC<Props> = ({
             </>
           )}
           {renderStatusText(event)}
-          {event.type !== "APP_EVENT" && (
-            <Spacer inline x={1} />
-          )}
+          {event.type !== "APP_EVENT" && <Spacer inline x={1} />}
           {renderInfoCta(event)}
           {event.status === "FAILED" && event.type !== "APP_EVENT" && (
             <>
-              <Link hasunderline>
+              <Link hasunderline onClick={() => triggerWorkflow()}>
                 <Container row>
                   <Icon height="10px" src={refresh} />
                   <Spacer inline width="5px" />
@@ -219,14 +363,10 @@ const EventCard: React.FC<Props> = ({
             </>
           )}
         </Container>
-        {false && (
-          <Text color="helper">user@email.com</Text>
-        )}
+        {false && <Text color="helper">user@email.com</Text>}
       </Container>
       {showModal && (
-        <Modal closeModal={() => setShowModal(false)}>
-          {modalContent}
-        </Modal>
+        <Modal closeModal={() => setShowModal(false)}>{modalContent}</Modal>
       )}
     </StyledEventCard>
   );
@@ -259,4 +399,4 @@ const StyledEventCard = styled.div`
       margin-right: 0;
     }
   }
-`;
+`;

+ 8 - 2
dashboard/src/main/home/app-dashboard/expanded-app/status/GHALogsModal.tsx

@@ -17,6 +17,7 @@ type Props = {
   logs: Log[];
   modalVisible: boolean;
   setModalVisible: (x: boolean) => void;
+  actionRunId?: string;
 };
 
 interface ExpandedIncidentLogsProps {
@@ -28,6 +29,7 @@ const GHALogsModal: React.FC<Props> = ({
   logs,
   modalVisible,
   setModalVisible,
+  actionRunId,
 }) => {
   const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
   const scrollToBottomRef = useRef<HTMLDivElement | undefined>(undefined);
@@ -104,9 +106,13 @@ const GHALogsModal: React.FC<Props> = ({
       <Link
         hasunderline
         target="_blank"
-        to={`https://github.com/${appData.app.repo_name}/actions`}
+        to={
+          actionRunId
+            ? `https://github.com/${appData.app.repo_name}/actions/runs/${actionRunId}`
+            : `https://github.com/${appData.app.repo_name}/actions`
+        }
       >
-        Check Full Build Logs
+        View full build logs
       </Link>
     </Modal>
   );

+ 44 - 1
dashboard/src/shared/api.tsx

@@ -221,7 +221,9 @@ const getFeedEvents = baseApi<
   }
 >("GET", (pathParams) => {
   let { project_id, cluster_id, stack_name, page } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${stack_name}/events?page=${page || 1}`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${stack_name}/events?page=${
+    page || 1
+  }`;
 });
 
 const createEnvironment = baseApi<
@@ -2135,6 +2137,46 @@ const getGHWorkflowLogs = baseApi<
   }
 );
 
+const getGHWorkflowLogById = baseApi<
+  {},
+  {
+    project_id: number;
+    cluster_id: number;
+    git_installation_id: number;
+    owner: string;
+    name: string;
+    filename?: string;
+    run_id: string;
+    release_name?: string;
+  }
+>(
+  "GET",
+  ({
+    project_id,
+    git_installation_id,
+    owner,
+    name,
+    cluster_id,
+    filename,
+    run_id,
+    release_name,
+  }) => {
+    const queryParams = new URLSearchParams();
+
+    if (release_name) {
+      queryParams.set("release_name", release_name);
+    }
+    if (filename) {
+      queryParams.set("filename", filename);
+    }
+    if (run_id) {
+      queryParams.set("run_id", run_id);
+    }
+
+    return `/api/projects/${project_id}/gitrepos/${git_installation_id}/${owner}/${name}/clusters/${cluster_id}/workflow_run_id?${queryParams.toString()}`;
+  }
+);
+
 const triggerPreviewEnvWorkflow = baseApi<
   {},
   { project_id: number; cluster_id: number; deployment_id: number }
@@ -2757,6 +2799,7 @@ export default {
   updateGitActionConfig,
   reRunGHWorkflow,
   getGHWorkflowLogs,
+  getGHWorkflowLogById,
   triggerPreviewEnvWorkflow,
   getTagsByProjectId,
   createTag,