Skip to content

Commit 9782521

Browse files
prasadtalasilaMicrochesstCopilot
authored andcommitted
Split pipeline utilities into focused modules (INTO-CPS-Association#1272 and INTO-CPS-Association#1277)
Moves the pipeline execution files into the stable codebase and prepares it to run multiple executions --------- Co-authored-by: Microchesst <marcusnj123@gmail.com> Co-authored-by: Marcus Jensen <79929209+Microchesst@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 0ce26dd commit 9782521

File tree

14 files changed

+918
-65
lines changed

14 files changed

+918
-65
lines changed

client/src/model/backend/gitlab/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,5 @@ export const getRunnerTag = (): string => store.getState().settings.RUNNER_TAG;
5252

5353
// route/digitaltwins/execute/pipelineChecks.ts
5454
export const MAX_EXECUTION_TIME = 10 * 60 * 1000;
55+
56+
export const PIPELINE_POLL_INTERVAL = 5000; // 5 seconds - for pipeline status checks
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { JobLog } from 'model/backend/gitlab/types/executionHistory';
2+
import cleanLog from 'model/backend/gitlab/cleanLog';
3+
4+
interface GitLabJob {
5+
id?: number;
6+
name?: string;
7+
[key: string]: unknown;
8+
}
9+
10+
/**
11+
* Fetches job logs from GitLab for a specific pipeline
12+
* Pure business logic - no UI dependencies
13+
* @param gitlabInstance GitLab instance with API methods
14+
* @param pipelineId Pipeline ID to fetch logs for
15+
* @returns Promise resolving to array of job logs
16+
*/
17+
export const fetchJobLogs = async (
18+
gitlabInstance: {
19+
projectId?: number | null;
20+
getPipelineJobs: (
21+
projectId: number,
22+
pipelineId: number,
23+
) => Promise<unknown[]>;
24+
getJobTrace: (projectId: number, jobId: number) => Promise<string>;
25+
},
26+
pipelineId: number,
27+
): Promise<JobLog[]> => {
28+
const { projectId } = gitlabInstance;
29+
if (!projectId) {
30+
return [];
31+
}
32+
33+
const rawJobs = await gitlabInstance.getPipelineJobs(projectId, pipelineId);
34+
const jobs: GitLabJob[] = rawJobs.map((job) => job as GitLabJob);
35+
36+
const logPromises = jobs.map((job) => fetchSingleJobLog(gitlabInstance, job));
37+
return (await Promise.all(logPromises)).reverse();
38+
};
39+
40+
/**
41+
* Fetches the log for a single GitLab job.
42+
* @param gitlabInstance - An object containing the GitLab project ID and a method to fetch the job trace.
43+
* @param job - The GitLab job for which the log should be fetched.
44+
* @returns A promise that resolves to a `JobLog` object containing the job name and its log content.
45+
*/
46+
export const fetchSingleJobLog = async (
47+
gitlabInstance: {
48+
projectId?: number | null;
49+
getJobTrace: (projectId: number, jobId: number) => Promise<string>;
50+
},
51+
job: GitLabJob,
52+
): Promise<JobLog> => {
53+
const { projectId } = gitlabInstance;
54+
let result: JobLog;
55+
56+
if (!projectId || job?.id === undefined) {
57+
result = {
58+
jobName: typeof job?.name === 'string' ? job.name : 'Unknown',
59+
log: job?.id === undefined ? 'Job ID not available' : '',
60+
};
61+
} else {
62+
try {
63+
let log = await gitlabInstance.getJobTrace(projectId, job.id);
64+
65+
if (typeof log === 'string') {
66+
log = cleanLog(log);
67+
} else {
68+
log = '';
69+
}
70+
71+
result = {
72+
jobName: typeof job.name === 'string' ? job.name : 'Unknown',
73+
log,
74+
};
75+
} catch (_e) {
76+
result = {
77+
jobName: typeof job.name === 'string' ? job.name : 'Unknown',
78+
log: 'Error fetching log content',
79+
};
80+
}
81+
}
82+
83+
return result;
84+
};
85+
86+
/**
87+
* Validates if job logs contain meaningful content
88+
* @param logs Array of job logs to validate
89+
* @returns True if logs contain meaningful content
90+
*/
91+
export const validateLogs = (logs: JobLog[]): boolean => {
92+
if (!logs || logs.length === 0) return false;
93+
94+
return !logs.every((log) => !log.log || log.log.trim() === '');
95+
};
96+
97+
/**
98+
* Filters out empty or invalid job logs
99+
* @param logs Array of job logs to filter
100+
* @returns Filtered array of valid job logs
101+
*/
102+
export const filterValidLogs = (logs: JobLog[]): JobLog[] => {
103+
if (!logs) return [];
104+
105+
return logs.filter((log) => log.log && log.log.trim() !== '');
106+
};
107+
108+
/**
109+
* Combines multiple job logs into a single log entry
110+
* @param logs Array of job logs to combine
111+
* @param separator Separator between logs (default: '\n---\n')
112+
* @returns Combined log string
113+
*/
114+
export const combineLogs = (
115+
logs: JobLog[],
116+
separator: string = '\n---\n',
117+
): string => {
118+
if (!logs || logs.length === 0) return '';
119+
120+
return logs
121+
.filter((log) => log.log && log.log.trim() !== '')
122+
.map((log) => `[${log.jobName}]\n${log.log}`)
123+
.join(separator);
124+
};
125+
126+
/**
127+
* Extracts job names from job logs
128+
* @param logs Array of job logs
129+
* @returns Array of job names
130+
*/
131+
export const extractJobNames = (logs: JobLog[]): string[] => {
132+
if (!logs) return [];
133+
134+
return logs.map((log) => log.jobName).filter(Boolean);
135+
};
136+
137+
/**
138+
* Finds a specific job log by job name
139+
* @param logs Array of job logs to search
140+
* @param jobName Name of the job to find
141+
* @returns The job log if found, undefined otherwise
142+
*/
143+
export const findJobLog = (
144+
logs: JobLog[],
145+
jobName: string,
146+
): JobLog | undefined => {
147+
if (!logs || !jobName) return undefined;
148+
149+
return logs.find((log) => log.jobName === jobName);
150+
};
151+
152+
/**
153+
* Counts the number of successful jobs based on log content
154+
* @param logs Array of job logs to analyze
155+
* @returns Number of jobs that appear to have succeeded
156+
*/
157+
export const countSuccessfulJobs = (logs: JobLog[]): number =>
158+
Array.isArray(logs)
159+
? logs.filter(
160+
(log) =>
161+
typeof log.log === 'string' && /success|completed/i.test(log.log),
162+
).length
163+
: 0;
164+
165+
/**
166+
* Counts the number of failed jobs based on log content
167+
* @param logs Array of job logs to analyze
168+
* @returns Number of jobs that appear to have failed
169+
*/
170+
export const countFailedJobs = (logs: JobLog[]): number =>
171+
Array.isArray(logs)
172+
? logs.filter(
173+
(log) => typeof log.log === 'string' && /(error|failed)/i.test(log.log),
174+
).length
175+
: 0;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
MAX_EXECUTION_TIME,
3+
PIPELINE_POLL_INTERVAL,
4+
} from 'model/backend/gitlab/constants';
5+
6+
/**
7+
* Creates a delay promise for polling operations
8+
* @param ms Milliseconds to delay
9+
* @returns Promise that resolves after the specified time
10+
*/
11+
export const delay = (ms: number): Promise<void> =>
12+
new Promise((resolve) => {
13+
setTimeout(resolve, ms);
14+
});
15+
16+
/**
17+
* Checks if a pipeline execution has timed out
18+
* @param startTime Timestamp when execution started
19+
* @param maxTime Maximum allowed execution time (optional, defaults to MAX_EXECUTION_TIME)
20+
* @returns True if execution has timed out
21+
*/
22+
export const hasTimedOut = (
23+
startTime: number,
24+
maxTime: number = MAX_EXECUTION_TIME,
25+
): boolean => Date.now() - startTime > maxTime;
26+
27+
/**
28+
* Determines the appropriate pipeline ID for execution
29+
* @param executionPipelineId Pipeline ID from execution history (if available)
30+
* @param fallbackPipelineId Fallback pipeline ID from digital twin
31+
* @returns The pipeline ID to use
32+
*/
33+
export const determinePipelineId = (
34+
executionPipelineId?: number,
35+
fallbackPipelineId?: number,
36+
): number => {
37+
if (executionPipelineId) return executionPipelineId;
38+
if (fallbackPipelineId) return fallbackPipelineId;
39+
throw new Error('No pipeline ID available');
40+
};
41+
42+
/**
43+
* Determines the child pipeline ID (parent + 1)
44+
* @param parentPipelineId The parent pipeline ID
45+
* @returns The child pipeline ID
46+
*/
47+
export const getChildPipelineId = (parentPipelineId: number): number =>
48+
parentPipelineId + 1;
49+
50+
/**
51+
* Checks if a pipeline status indicates completion
52+
* @param status Pipeline status string
53+
* @returns True if pipeline is completed (success or failed)
54+
*/
55+
export const isPipelineCompleted = (status: string): boolean =>
56+
status === 'success' || status === 'failed';
57+
58+
/**
59+
* Checks if a pipeline status indicates it's still running
60+
* @param status Pipeline status string
61+
* @returns True if pipeline is still running
62+
*/
63+
export const isPipelineRunning = (status: string): boolean =>
64+
status === 'running' || status === 'pending';
65+
66+
/**
67+
* Determines if polling should continue based on status and timeout
68+
* @param status Current pipeline status
69+
* @param startTime When polling started
70+
* @returns True if polling should continue
71+
*/
72+
export const shouldContinuePolling = (
73+
status: string,
74+
startTime: number,
75+
): boolean => {
76+
if (isPipelineCompleted(status)) return false;
77+
if (hasTimedOut(startTime)) return false;
78+
return isPipelineRunning(status);
79+
};
80+
81+
/**
82+
* Gets the default polling interval for pipeline status checks
83+
* @returns Polling interval in milliseconds
84+
*/
85+
export const getPollingInterval = (): number => PIPELINE_POLL_INTERVAL;

0 commit comments

Comments
 (0)