Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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