Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions client/src/model/backend/gitlab/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export const RUNNER_TAG = 'linux';

// route/digitaltwins/execute/pipelineChecks.ts
export const MAX_EXECUTION_TIME = 10 * 60 * 1000;

export const PIPELINE_POLL_INTERVAL = 5000; // 5 seconds - for pipeline status checks
175 changes: 175 additions & 0 deletions client/src/model/backend/gitlab/execution/logFetching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { JobLog } from 'model/backend/gitlab/types/executionHistory';
import cleanLog from 'model/backend/gitlab/cleanLog';

interface GitLabJob {
id?: number;
name?: string;
[key: string]: unknown;
}

/**
* Fetches job logs from GitLab for a specific pipeline
* Pure business logic - no UI dependencies
* @param gitlabInstance GitLab instance with API methods
* @param pipelineId Pipeline ID to fetch logs for
* @returns Promise resolving to array of job logs
*/
export const fetchJobLogs = async (
gitlabInstance: {
projectId?: number | null;
getPipelineJobs: (
projectId: number,
pipelineId: number,
) => Promise<unknown[]>;
getJobTrace: (projectId: number, jobId: number) => Promise<string>;
},
pipelineId: number,
): Promise<JobLog[]> => {
const { projectId } = gitlabInstance;
if (!projectId) {
return [];
}

const rawJobs = await gitlabInstance.getPipelineJobs(projectId, pipelineId);
const jobs: GitLabJob[] = rawJobs.map((job) => job as GitLabJob);

const logPromises = jobs.map((job) => fetchSingleJobLog(gitlabInstance, job));
return (await Promise.all(logPromises)).reverse();
};

/**
* Fetches the log for a single GitLab job.
* @param gitlabInstance - An object containing the GitLab project ID and a method to fetch the job trace.
* @param job - The GitLab job for which the log should be fetched.
* @returns A promise that resolves to a `JobLog` object containing the job name and its log content.
*/
export const fetchSingleJobLog = async (
gitlabInstance: {
projectId?: number | null;
getJobTrace: (projectId: number, jobId: number) => Promise<string>;
},
job: GitLabJob,
): Promise<JobLog> => {
const { projectId } = gitlabInstance;
let result: JobLog;

if (!projectId || job?.id === undefined) {
result = {
jobName: typeof job?.name === 'string' ? job.name : 'Unknown',
log: job?.id === undefined ? 'Job ID not available' : '',
};
} else {
try {
let log = await gitlabInstance.getJobTrace(projectId, job.id);

if (typeof log === 'string') {
log = cleanLog(log);
} else {
log = '';

Check warning on line 68 in client/src/model/backend/gitlab/execution/logFetching.ts

View check run for this annotation

Codecov / codecov/patch

client/src/model/backend/gitlab/execution/logFetching.ts#L67-L68

Added lines #L67 - L68 were not covered by tests
}

result = {
jobName: typeof job.name === 'string' ? job.name : 'Unknown',
log,
};
} catch (_e) {
result = {
jobName: typeof job.name === 'string' ? job.name : 'Unknown',
log: 'Error fetching log content',
};
}
}

return result;
};

/**
* Validates if job logs contain meaningful content
* @param logs Array of job logs to validate
* @returns True if logs contain meaningful content
*/
export const validateLogs = (logs: JobLog[]): boolean => {
if (!logs || logs.length === 0) return false;

return !logs.every((log) => !log.log || log.log.trim() === '');
};

/**
* Filters out empty or invalid job logs
* @param logs Array of job logs to filter
* @returns Filtered array of valid job logs
*/
export const filterValidLogs = (logs: JobLog[]): JobLog[] => {
if (!logs) return [];

return logs.filter((log) => log.log && log.log.trim() !== '');
};

/**
* Combines multiple job logs into a single log entry
* @param logs Array of job logs to combine
* @param separator Separator between logs (default: '\n---\n')
* @returns Combined log string
*/
export const combineLogs = (
logs: JobLog[],
separator: string = '\n---\n',
): string => {
if (!logs || logs.length === 0) return '';

return logs
.filter((log) => log.log && log.log.trim() !== '')
.map((log) => `[${log.jobName}]\n${log.log}`)
.join(separator);
};

/**
* Extracts job names from job logs
* @param logs Array of job logs
* @returns Array of job names
*/
export const extractJobNames = (logs: JobLog[]): string[] => {
if (!logs) return [];

return logs.map((log) => log.jobName).filter(Boolean);
};

/**
* Finds a specific job log by job name
* @param logs Array of job logs to search
* @param jobName Name of the job to find
* @returns The job log if found, undefined otherwise
*/
export const findJobLog = (
logs: JobLog[],
jobName: string,
): JobLog | undefined => {
if (!logs || !jobName) return undefined;

return logs.find((log) => log.jobName === jobName);
};

/**
* Counts the number of successful jobs based on log content
* @param logs Array of job logs to analyze
* @returns Number of jobs that appear to have succeeded
*/
export const countSuccessfulJobs = (logs: JobLog[]): number =>
Array.isArray(logs)
? logs.filter(
(log) =>
typeof log.log === 'string' && /success|completed/i.test(log.log),
).length
: 0;

Check warning on line 163 in client/src/model/backend/gitlab/execution/logFetching.ts

View check run for this annotation

Codecov / codecov/patch

client/src/model/backend/gitlab/execution/logFetching.ts#L163

Added line #L163 was not covered by tests

/**
* Counts the number of failed jobs based on log content
* @param logs Array of job logs to analyze
* @returns Number of jobs that appear to have failed
*/
export const countFailedJobs = (logs: JobLog[]): number =>
Array.isArray(logs)
? logs.filter(
(log) => typeof log.log === 'string' && /(error|failed)/i.test(log.log),
).length
: 0;

Check warning on line 175 in client/src/model/backend/gitlab/execution/logFetching.ts

View check run for this annotation

Codecov / codecov/patch

client/src/model/backend/gitlab/execution/logFetching.ts#L175

Added line #L175 was not covered by tests
85 changes: 85 additions & 0 deletions client/src/model/backend/gitlab/execution/pipelineCore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
MAX_EXECUTION_TIME,
PIPELINE_POLL_INTERVAL,
} from 'model/backend/gitlab/constants';

/**
* Creates a delay promise for polling operations
* @param ms Milliseconds to delay
* @returns Promise that resolves after the specified time
*/
export const delay = (ms: number): Promise<void> =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});

/**
* Checks if a pipeline execution has timed out
* @param startTime Timestamp when execution started
* @param maxTime Maximum allowed execution time (optional, defaults to MAX_EXECUTION_TIME)
* @returns True if execution has timed out
*/
export const hasTimedOut = (
startTime: number,
maxTime: number = MAX_EXECUTION_TIME,
): boolean => Date.now() - startTime > maxTime;

/**
* Determines the appropriate pipeline ID for execution
* @param executionPipelineId Pipeline ID from execution history (if available)
* @param fallbackPipelineId Fallback pipeline ID from digital twin
* @returns The pipeline ID to use
*/
export const determinePipelineId = (
executionPipelineId?: number,
fallbackPipelineId?: number,
): number => {
if (executionPipelineId) return executionPipelineId;
if (fallbackPipelineId) return fallbackPipelineId;
throw new Error('No pipeline ID available');
};

/**
* Determines the child pipeline ID (parent + 1)
* @param parentPipelineId The parent pipeline ID
* @returns The child pipeline ID
*/
export const getChildPipelineId = (parentPipelineId: number): number =>
parentPipelineId + 1;

/**
* Checks if a pipeline status indicates completion
* @param status Pipeline status string
* @returns True if pipeline is completed (success or failed)
*/
export const isPipelineCompleted = (status: string): boolean =>
status === 'success' || status === 'failed';

/**
* Checks if a pipeline status indicates it's still running
* @param status Pipeline status string
* @returns True if pipeline is still running
*/
export const isPipelineRunning = (status: string): boolean =>
status === 'running' || status === 'pending';

/**
* Determines if polling should continue based on status and timeout
* @param status Current pipeline status
* @param startTime When polling started
* @returns True if polling should continue
*/
export const shouldContinuePolling = (
status: string,
startTime: number,
): boolean => {
if (isPipelineCompleted(status)) return false;
if (hasTimedOut(startTime)) return false;
return isPipelineRunning(status);
};

/**
* Gets the default polling interval for pipeline status checks
* @returns Polling interval in milliseconds
*/
export const getPollingInterval = (): number => PIPELINE_POLL_INTERVAL;
Loading
Loading