Skip to content
Closed
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
158 changes: 158 additions & 0 deletions client/src/model/backend/gitlab/execution/logFetching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
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(async (job) => {
if (!job || typeof job.id === 'undefined') {
return { jobName: 'Unknown', log: 'Job ID not available' };
}

try {
let log = await gitlabInstance.getJobTrace(projectId, job.id);

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

Check warning on line 47 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#L46-L47

Added lines #L46 - L47 were not covered by tests
}

return {
jobName: typeof job.name === 'string' ? job.name : 'Unknown',
log,
};
} catch (_e) {
return {
jobName: typeof job.name === 'string' ? job.name : 'Unknown',
log: 'Error fetching log content',
};
}
});
return (await Promise.all(logPromises)).reverse();
};

/**
* 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 => {
if (!logs) return 0;

return logs.filter((log) => {
if (!log.log) return false;
const logContent = log.log.toLowerCase();
return logContent.includes('success') || logContent.includes('completed');
}).length;
};

/**
* 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 => {
if (!logs) return 0;

return logs.filter((log) => {
if (!log.log) return false;
const logContent = log.log.toLowerCase();
return logContent.includes('error') || logContent.includes('failed');
}).length;
};
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;
123 changes: 123 additions & 0 deletions client/src/model/backend/gitlab/execution/statusChecking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { ExecutionStatus } from 'model/backend/gitlab/types/executionHistory';

/**
* Maps GitLab pipeline status to internal execution status
* @param gitlabStatus Status string from GitLab API
* @returns Internal execution status
*/
export const mapGitlabStatusToExecutionStatus = (
gitlabStatus: string,
): ExecutionStatus => {
switch (gitlabStatus.toLowerCase()) {
case 'success':
return ExecutionStatus.COMPLETED;
case 'failed':
return ExecutionStatus.FAILED;
case 'running':
case 'pending':
return ExecutionStatus.RUNNING;
case 'canceled':
case 'cancelled':
return ExecutionStatus.CANCELED;
case 'skipped':
return ExecutionStatus.FAILED; // Treat skipped as failed
default:
return ExecutionStatus.RUNNING; // Default to running for unknown statuses
}
};

/**
* Determines if a GitLab status indicates success
* @param status GitLab pipeline status (can be null/undefined)
* @returns True if status indicates success
*/
export const isSuccessStatus = (status: string | null | undefined): boolean =>
status?.toLowerCase() === 'success';

/**
* Determines if a GitLab status indicates failure
* @param status GitLab pipeline status (can be null/undefined)
* @returns True if status indicates failure
*/
export const isFailureStatus = (status: string | null | undefined): boolean => {
if (!status) return false;
const lowerStatus = status.toLowerCase();
return lowerStatus === 'failed' || lowerStatus === 'skipped';
};

/**
* Determines if a GitLab status indicates the pipeline is still running
* @param status GitLab pipeline status (can be null/undefined)
* @returns True if status indicates pipeline is running
*/
export const isRunningStatus = (status: string | null | undefined): boolean => {
if (!status) return false;
const lowerStatus = status.toLowerCase();
return lowerStatus === 'running' || lowerStatus === 'pending';
};

/**
* Determines if a GitLab status indicates the pipeline was canceled
* @param status GitLab pipeline status (can be null/undefined)
* @returns True if status indicates cancellation
*/
export const isCanceledStatus = (
status: string | null | undefined,
): boolean => {
if (!status) return false;
const lowerStatus = status.toLowerCase();
return lowerStatus === 'canceled' || lowerStatus === 'cancelled';
};

/**
* Determines if a status indicates the pipeline has finished (success or failure)
* @param status GitLab pipeline status (can be null/undefined)
* @returns True if pipeline has finished
*/
export const isFinishedStatus = (status: string | null | undefined): boolean =>
isSuccessStatus(status) ||
isFailureStatus(status) ||
isCanceledStatus(status);

/**
* Gets a human-readable description of the pipeline status
* @param status GitLab pipeline status (can be null/undefined)
* @returns Human-readable status description
*/
export const getStatusDescription = (
status: string | null | undefined,
): string => {
if (!status) return 'Pipeline status: unknown';

switch (status.toLowerCase()) {
case 'success':
return 'Pipeline completed successfully';
case 'failed':
return 'Pipeline failed';
case 'running':
return 'Pipeline is running';
case 'pending':
return 'Pipeline is pending';
case 'canceled':
case 'cancelled':
return 'Pipeline was canceled';
case 'skipped':
return 'Pipeline was skipped';
default:
return `Pipeline status: ${status}`;
}
};

/**
* Determines the severity level of a status for UI display
* @param status GitLab pipeline status (can be null/undefined)
* @returns Severity level ('success', 'error', 'warning', 'info')
*/
export const getStatusSeverity = (
status: string | null | undefined,
): 'success' | 'error' | 'warning' | 'info' => {
if (isSuccessStatus(status)) return 'success';
if (isFailureStatus(status)) return 'error';
if (isCanceledStatus(status)) return 'warning';
return 'info'; // For running, pending, etc.
};
15 changes: 15 additions & 0 deletions client/src/model/backend/gitlab/types/executionHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type JobName = string;
export type LogContent = string;

export enum ExecutionStatus {
RUNNING = 'running',
COMPLETED = 'completed',
FAILED = 'failed',
CANCELED = 'canceled',
TIMEOUT = 'timeout',
}

export interface JobLog {
jobName: JobName;
log: LogContent;
}
Loading
Loading