From c79feafa154bbaef74e63bf81172144edd631837 Mon Sep 17 00:00:00 2001 From: Eloy Coto Date: Tue, 5 May 2026 17:16:36 +0200 Subject: [PATCH] refactor: add JobStatus value object replacing raw string comparisons Replace a few raw string status checks across x2a-common, x2a-node, x2a-backend, and x2a frontend with a flyweight JobStatus class following the Phase pattern from PR #3002. - Add JobStatus class with predicates: isActive(), isFinished(), isPending(), isRunning(), isSuccess(), isError(), isCancelled() - Replace ['pending','running'].includes() with isActive() - Replace 3-way || chains with isFinished() - Keep raw strings for DB writes and API responses (data, not logic) Signed-off-by: Eloy Coto --- .../src/router/collectArtifacts.ts | 21 +- .../plugins/x2a-backend/src/router/jobs.ts | 7 +- .../plugins/x2a-backend/src/router/modules.ts | 31 ++- .../x2a-backend/src/router/projects.ts | 8 +- .../X2ADatabaseService/projectStatus.ts | 46 +++-- .../x2a/plugins/x2a-common/report.api.md | 44 +++++ .../x2a-common/src/domain/JobStatus.test.ts | 180 ++++++++++++++++++ .../x2a-common/src/domain/JobStatus.ts | 106 +++++++++++ .../plugins/x2a-common/src/domain/index.ts | 1 + workspaces/x2a/plugins/x2a-node/report.api.md | 4 +- .../x2a/plugins/x2a-node/src/moduleStatus.ts | 9 +- .../plugins/x2a-node/src/modulesReconcile.ts | 3 +- workspaces/x2a/plugins/x2a-node/src/utils.ts | 10 +- .../src/components/tools/canCancelPhase.ts | 7 +- .../tools/isEligibleForRetriggerInit.ts | 7 +- 15 files changed, 415 insertions(+), 69 deletions(-) create mode 100644 workspaces/x2a/plugins/x2a-common/src/domain/JobStatus.test.ts create mode 100644 workspaces/x2a/plugins/x2a-common/src/domain/JobStatus.ts diff --git a/workspaces/x2a/plugins/x2a-backend/src/router/collectArtifacts.ts b/workspaces/x2a/plugins/x2a-backend/src/router/collectArtifacts.ts index 8f3d171340..ec44c5e349 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/router/collectArtifacts.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/router/collectArtifacts.ts @@ -24,7 +24,7 @@ import { import { MigrationPhase, Artifact, - JobStatusEnum, + JobStatus, Telemetry, Phase, } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; @@ -335,12 +335,13 @@ async function processJobCompletion( logger: RouterDeps['logger'], job: JobWithToken, ): Promise<{ message: string }> { - let status: JobStatusEnum = - validatedRequest.status === 'success' ? 'success' : 'error'; + let jobStatus = JobStatus.from( + validatedRequest.status === 'success' ? 'success' : 'error', + ); let errorDetails = validatedRequest.errorDetails || null; - if (status === 'success') { - status = await executePhaseActionsWithErrorHandling( + if (jobStatus.isSuccess()) { + jobStatus = await executePhaseActionsWithErrorHandling( phase, projectId, validatedRequest, @@ -348,7 +349,7 @@ async function processJobCompletion( logger, ); - if (status === 'error') { + if (jobStatus.isError()) { errorDetails = 'Phase actions failed'; } } @@ -357,7 +358,7 @@ async function processJobCompletion( await x2aDatabase.updateJob({ id: validatedRequest.jobId, - status, + status: jobStatus.value, finishedAt: new Date(), errorDetails, log: logs, @@ -375,7 +376,7 @@ async function executePhaseActionsWithErrorHandling( validatedRequest: CollectArtifactsRequestBody, x2aDatabase: RouterDeps['x2aDatabase'], logger: RouterDeps['logger'], -): Promise { +): Promise { try { await executePhaseActions(phase, { projectId, @@ -383,12 +384,12 @@ async function executePhaseActionsWithErrorHandling( x2aDatabase, logger, }); - return 'success'; + return JobStatus.SUCCESS; } catch (error) { logger.error( `Phase actions failed for job ${validatedRequest.jobId}: ${error instanceof Error ? error.message : String(error)}`, ); - return 'error'; + return JobStatus.ERROR; } } diff --git a/workspaces/x2a/plugins/x2a-backend/src/router/jobs.ts b/workspaces/x2a/plugins/x2a-backend/src/router/jobs.ts index d602917a50..4e6d11fbf0 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/router/jobs.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/router/jobs.ts @@ -20,6 +20,7 @@ import { InputError, NotFoundError } from '@backstage/errors'; import { ModulePhase, Job, + JobStatus, Phase, } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; @@ -37,11 +38,7 @@ async function sendJobLogs( const { x2aDatabase, kubeService, logger } = deps; res.setHeader('Content-Type', 'text/plain; charset=utf-8'); - if ( - job.status === 'success' || - job.status === 'error' || - job.status === 'cancelled' - ) { + if (JobStatus.from(job.status).isFinished()) { logger.info( `Job ${job.id} is finished (status: ${job.status}), returning logs from database`, ); diff --git a/workspaces/x2a/plugins/x2a-backend/src/router/modules.ts b/workspaces/x2a/plugins/x2a-backend/src/router/modules.ts index e99f8dedf5..f2b56794f2 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/router/modules.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/router/modules.ts @@ -20,6 +20,7 @@ import { InputError, NotFoundError } from '@backstage/errors'; import { type ModulePhase, + JobStatus, Phase, } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; @@ -261,27 +262,25 @@ export function registerModuleRoutes( }); // Reconcile jobs that appear active against K8s + const activeJobs = existingJobs.filter(job => + JobStatus.from(job.status).isActive(), + ); const reconciledJobs = await Promise.all( - existingJobs - .filter(job => ['pending', 'running'].includes(job.status)) - .map(job => - reconcileJobStatus(job, { kubeService, x2aDatabase, logger }), - ), + activeJobs.map(job => + reconcileJobStatus(job, { kubeService, x2aDatabase, logger }), + ), ); - const hasActiveJob = reconciledJobs.some(job => - ['pending', 'running'].includes(job.status), + const activeJob = reconciledJobs.find(job => + JobStatus.from(job.status).isActive(), ); - if (hasActiveJob) { - const activeJob = existingJobs.find(job => - ['pending', 'running'].includes(job.status), - ); + if (activeJob) { return res.status(409).json({ error: 'JobAlreadyRunning', - message: `A ${activeJob!.phase} job is already running for this module`, + message: `A ${activeJob.phase} job is already running for this module`, details: 'Please wait for the current job to complete or cancel it', - activeJobId: activeJob!.id, - activeJobPhase: activeJob!.phase, + activeJobId: activeJob.id, + activeJobPhase: activeJob.phase, }); } @@ -328,7 +327,7 @@ export function registerModuleRoutes( // Re-read the job to detect cancellation during the K8s creation window const freshJob = await x2aDatabase.getJob({ id: job.id }); - if (freshJob?.status === 'cancelled') { + if (freshJob && JobStatus.from(freshJob.status).isCancelled()) { try { await kubeService.deleteJob(k8sJobName); } catch (e) { @@ -422,7 +421,7 @@ export function registerModuleRoutes( const job = jobs[0]; - if (!['pending', 'running'].includes(job.status)) { + if (!JobStatus.from(job.status).isActive()) { return res.status(409).json({ error: 'JobNotCancellable', message: `The ${phase} job is in "${job.status}" state and cannot be cancelled.`, diff --git a/workspaces/x2a/plugins/x2a-backend/src/router/projects.ts b/workspaces/x2a/plugins/x2a-backend/src/router/projects.ts index 5fe811c939..30cdc8d1ae 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/router/projects.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/router/projects.ts @@ -19,6 +19,7 @@ import express from 'express'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { InputError, NotAllowedError, NotFoundError } from '@backstage/errors'; import { + JobStatus, x2aAdminWritePermission, x2aUserPermission, } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; @@ -209,7 +210,7 @@ export function registerProjectRoutes( // Cancel any active k8s jobs before deleting DB records const jobs = await x2aDatabase.listJobsForProject({ projectId }); const activeJobs = jobs.filter( - job => ['pending', 'running'].includes(job.status) && job.k8sJobName, + job => JobStatus.from(job.status).isActive() && job.k8sJobName, ); await Promise.all( activeJobs.map(job => { @@ -308,8 +309,7 @@ export function registerProjectRoutes( // Check for existing running init job const existingJobs = await x2aDatabase.listJobsForProject({ projectId }); const activeInitJobs = existingJobs.filter( - job => - job.phase === 'init' && ['pending', 'running'].includes(job.status), + job => job.phase === 'init' && JobStatus.from(job.status).isActive(), ); const reconciledInitJobs = await Promise.all( activeInitJobs.map(job => @@ -317,7 +317,7 @@ export function registerProjectRoutes( ), ); const hasActiveInitJob = reconciledInitJobs.some(job => - ['pending', 'running'].includes(job.status), + JobStatus.from(job.status).isActive(), ); if (hasActiveInitJob) { diff --git a/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/projectStatus.ts b/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/projectStatus.ts index e703930e48..2759cafde4 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/projectStatus.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/projectStatus.ts @@ -16,6 +16,7 @@ import { Job, + JobStatus, Module, ProjectStatus, ProjectStatusState, @@ -48,34 +49,39 @@ export function calculateProjectStatus( }; } - const error = projectModules.filter( - module => module.status === 'error', - ).length; - const finished = projectModules.filter( - module => - module.status === 'success' && module.publish?.status === 'success', - ).length; - const waiting = projectModules.filter( - module => - module.status === 'success' && - (!module.publish || module.publish.status === 'cancelled'), - ).length; - const pending = projectModules.filter( - module => module.status === 'pending', + const modulesWithStatus = projectModules.map(module => ({ + module, + status: module.status ? JobStatus.from(module.status) : undefined, + publishStatus: module.publish?.status + ? JobStatus.from(module.publish.status) + : undefined, + })); + + const error = modulesWithStatus.filter(m => m.status?.isError()).length; + const finished = modulesWithStatus.filter( + m => m.status?.isSuccess() && m.publishStatus?.isSuccess(), ).length; - const running = projectModules.filter( - module => module.status === 'running', + const waiting = modulesWithStatus.filter( + m => + m.status?.isSuccess() && + (!m.module.publish || m.publishStatus?.isCancelled()), ).length; - const cancelled = projectModules.filter( - module => module.status === 'cancelled', + const pending = modulesWithStatus.filter(m => m.status?.isPending()).length; + const running = modulesWithStatus.filter(m => m.status?.isRunning()).length; + const cancelled = modulesWithStatus.filter(m => + m.status?.isCancelled(), ).length; + const initStatus = initJob?.status + ? JobStatus.from(initJob.status) + : undefined; + let state: ProjectStatusState; if (error > 0) { state = 'failed'; // At least one module is in error state - } else if (['pending', 'running'].includes(initJob?.status ?? '')) { + } else if (initStatus?.isActive()) { state = 'initializing'; // Project's init job is running or scheduling - } else if (initJob?.status === 'success') { + } else if (initStatus?.isSuccess()) { if (total > 0 && finished === total) { state = 'completed'; // All modules are in success state } else if (total === 0 || pending + cancelled === total) { diff --git a/workspaces/x2a/plugins/x2a-common/report.api.md b/workspaces/x2a/plugins/x2a-common/report.api.md index b4d58cb374..b08cbb2537 100644 --- a/workspaces/x2a/plugins/x2a-common/report.api.md +++ b/workspaces/x2a/plugins/x2a-common/report.api.md @@ -139,6 +139,50 @@ export interface Job { telemetry?: Telemetry; } +// @public (undocumented) +export class JobStatus { + // (undocumented) + static activeStatuses(): readonly JobStatus[]; + // (undocumented) + static all(): readonly JobStatus[]; + // (undocumented) + static readonly CANCELLED: JobStatus; + // (undocumented) + equals(other: JobStatus): boolean; + // (undocumented) + static readonly ERROR: JobStatus; + // (undocumented) + static finishedStatuses(): readonly JobStatus[]; + // (undocumented) + static from(raw: string): JobStatus; + // (undocumented) + isActive(): boolean; + // (undocumented) + isCancelled(): boolean; + // (undocumented) + isError(): boolean; + // (undocumented) + isFinished(): boolean; + // (undocumented) + isPending(): boolean; + // (undocumented) + isRunning(): boolean; + // (undocumented) + isSuccess(): boolean; + // (undocumented) + static readonly PENDING: JobStatus; + // (undocumented) + static readonly RUNNING: JobStatus; + // (undocumented) + static readonly SUCCESS: JobStatus; + // (undocumented) + toString(): string; + // (undocumented) + readonly value: JobStatusEnum; + // (undocumented) + static values(): readonly JobStatusEnum[]; +} + // @public (undocumented) export type JobStatusEnum = 'pending' | 'running' | 'success' | 'error' | 'cancelled'; diff --git a/workspaces/x2a/plugins/x2a-common/src/domain/JobStatus.test.ts b/workspaces/x2a/plugins/x2a-common/src/domain/JobStatus.test.ts new file mode 100644 index 0000000000..54d5794114 --- /dev/null +++ b/workspaces/x2a/plugins/x2a-common/src/domain/JobStatus.test.ts @@ -0,0 +1,180 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JobStatus } from './JobStatus'; + +describe('JobStatus', () => { + describe('from', () => { + it('returns JobStatus.PENDING for "pending"', () => { + expect(JobStatus.from('pending')).toBe(JobStatus.PENDING); + }); + + it('returns JobStatus.RUNNING for "running"', () => { + expect(JobStatus.from('running')).toBe(JobStatus.RUNNING); + }); + + it('returns JobStatus.SUCCESS for "success"', () => { + expect(JobStatus.from('success')).toBe(JobStatus.SUCCESS); + }); + + it('returns JobStatus.ERROR for "error"', () => { + expect(JobStatus.from('error')).toBe(JobStatus.ERROR); + }); + + it('returns JobStatus.CANCELLED for "cancelled"', () => { + expect(JobStatus.from('cancelled')).toBe(JobStatus.CANCELLED); + }); + + it('throws for an invalid status', () => { + expect(() => JobStatus.from('invalid')).toThrow( + 'Invalid job status: "invalid". Valid: pending, running, success, error, cancelled', + ); + }); + }); + + describe('all', () => { + it('returns 5 statuses in defined order', () => { + const all = JobStatus.all(); + expect(all).toHaveLength(5); + expect(all).toEqual([ + JobStatus.PENDING, + JobStatus.RUNNING, + JobStatus.SUCCESS, + JobStatus.ERROR, + JobStatus.CANCELLED, + ]); + }); + }); + + describe('values', () => { + it('returns raw string values for all statuses', () => { + expect(JobStatus.values()).toEqual([ + 'pending', + 'running', + 'success', + 'error', + 'cancelled', + ]); + }); + }); + + describe('activeStatuses', () => { + it('returns pending and running', () => { + expect(JobStatus.activeStatuses()).toEqual([ + JobStatus.PENDING, + JobStatus.RUNNING, + ]); + }); + }); + + describe('finishedStatuses', () => { + it('returns success, error, and cancelled', () => { + expect(JobStatus.finishedStatuses()).toEqual([ + JobStatus.SUCCESS, + JobStatus.ERROR, + JobStatus.CANCELLED, + ]); + }); + }); + + describe('isActive / isFinished', () => { + it('PENDING is active', () => { + expect(JobStatus.PENDING.isActive()).toBe(true); + expect(JobStatus.PENDING.isFinished()).toBe(false); + }); + + it('RUNNING is active', () => { + expect(JobStatus.RUNNING.isActive()).toBe(true); + expect(JobStatus.RUNNING.isFinished()).toBe(false); + }); + + it('SUCCESS is finished', () => { + expect(JobStatus.SUCCESS.isFinished()).toBe(true); + expect(JobStatus.SUCCESS.isActive()).toBe(false); + }); + + it('ERROR is finished', () => { + expect(JobStatus.ERROR.isFinished()).toBe(true); + expect(JobStatus.ERROR.isActive()).toBe(false); + }); + + it('CANCELLED is finished', () => { + expect(JobStatus.CANCELLED.isFinished()).toBe(true); + expect(JobStatus.CANCELLED.isActive()).toBe(false); + }); + }); + + describe('individual predicates', () => { + it('isPending', () => { + expect(JobStatus.PENDING.isPending()).toBe(true); + expect(JobStatus.RUNNING.isPending()).toBe(false); + }); + + it('isRunning', () => { + expect(JobStatus.RUNNING.isRunning()).toBe(true); + expect(JobStatus.PENDING.isRunning()).toBe(false); + }); + + it('isSuccess', () => { + expect(JobStatus.SUCCESS.isSuccess()).toBe(true); + expect(JobStatus.ERROR.isSuccess()).toBe(false); + }); + + it('isError', () => { + expect(JobStatus.ERROR.isError()).toBe(true); + expect(JobStatus.SUCCESS.isError()).toBe(false); + }); + + it('isCancelled', () => { + expect(JobStatus.CANCELLED.isCancelled()).toBe(true); + expect(JobStatus.PENDING.isCancelled()).toBe(false); + }); + }); + + describe('toString', () => { + it('returns the raw string value', () => { + expect(JobStatus.PENDING.toString()).toBe('pending'); + expect(JobStatus.RUNNING.toString()).toBe('running'); + expect(JobStatus.SUCCESS.toString()).toBe('success'); + expect(JobStatus.ERROR.toString()).toBe('error'); + expect(JobStatus.CANCELLED.toString()).toBe('cancelled'); + }); + }); + + describe('equals', () => { + it('returns true for same instance', () => { + expect(JobStatus.PENDING.equals(JobStatus.PENDING)).toBe(true); + }); + + it('returns true for JobStatus.from result (flyweight identity)', () => { + expect(JobStatus.PENDING.equals(JobStatus.from('pending'))).toBe(true); + }); + + it('returns false for different statuses', () => { + expect(JobStatus.PENDING.equals(JobStatus.RUNNING)).toBe(false); + }); + }); + + describe('flyweight identity', () => { + it('from() returns the exact same instance', () => { + expect(JobStatus.from('pending')).toBe(JobStatus.PENDING); + expect(JobStatus.from('running')).toBe(JobStatus.RUNNING); + expect(JobStatus.from('success')).toBe(JobStatus.SUCCESS); + expect(JobStatus.from('error')).toBe(JobStatus.ERROR); + expect(JobStatus.from('cancelled')).toBe(JobStatus.CANCELLED); + }); + }); +}); diff --git a/workspaces/x2a/plugins/x2a-common/src/domain/JobStatus.ts b/workspaces/x2a/plugins/x2a-common/src/domain/JobStatus.ts new file mode 100644 index 0000000000..4dada6eef0 --- /dev/null +++ b/workspaces/x2a/plugins/x2a-common/src/domain/JobStatus.ts @@ -0,0 +1,106 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { JobStatusEnum } from '../../client/src/schema/openapi'; + +/** @public */ +export class JobStatus { + static readonly PENDING = new JobStatus('pending'); + static readonly RUNNING = new JobStatus('running'); + static readonly SUCCESS = new JobStatus('success'); + static readonly ERROR = new JobStatus('error'); + static readonly CANCELLED = new JobStatus('cancelled'); + + private static readonly BY_VALUE = new Map( + [ + JobStatus.PENDING, + JobStatus.RUNNING, + JobStatus.SUCCESS, + JobStatus.ERROR, + JobStatus.CANCELLED, + ].map(s => [s.value, s]), + ); + + private constructor(readonly value: JobStatusEnum) {} + + static from(raw: string): JobStatus { + const status = JobStatus.BY_VALUE.get(raw); + if (!status) { + throw new Error( + `Invalid job status: "${raw}". Valid: ${JobStatus.values().join(', ')}`, + ); + } + return status; + } + + static all(): readonly JobStatus[] { + return [ + JobStatus.PENDING, + JobStatus.RUNNING, + JobStatus.SUCCESS, + JobStatus.ERROR, + JobStatus.CANCELLED, + ]; + } + + static values(): readonly JobStatusEnum[] { + return JobStatus.all().map(s => s.value); + } + + static activeStatuses(): readonly JobStatus[] { + return [JobStatus.PENDING, JobStatus.RUNNING]; + } + + static finishedStatuses(): readonly JobStatus[] { + return [JobStatus.SUCCESS, JobStatus.ERROR, JobStatus.CANCELLED]; + } + + isActive(): boolean { + return this === JobStatus.PENDING || this === JobStatus.RUNNING; + } + + isFinished(): boolean { + return !this.isActive(); + } + + isPending(): boolean { + return this === JobStatus.PENDING; + } + + isRunning(): boolean { + return this === JobStatus.RUNNING; + } + + isSuccess(): boolean { + return this === JobStatus.SUCCESS; + } + + isError(): boolean { + return this === JobStatus.ERROR; + } + + isCancelled(): boolean { + return this === JobStatus.CANCELLED; + } + + equals(other: JobStatus): boolean { + return this.value === other.value; + } + + toString(): string { + return this.value; + } +} diff --git a/workspaces/x2a/plugins/x2a-common/src/domain/index.ts b/workspaces/x2a/plugins/x2a-common/src/domain/index.ts index 893e7bd961..0dcce40e54 100644 --- a/workspaces/x2a/plugins/x2a-common/src/domain/index.ts +++ b/workspaces/x2a/plugins/x2a-common/src/domain/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ +export { JobStatus } from './JobStatus'; export { Phase } from './Phase'; diff --git a/workspaces/x2a/plugins/x2a-node/report.api.md b/workspaces/x2a/plugins/x2a-node/report.api.md index 4cd4633d8c..d76c3e8b16 100644 --- a/workspaces/x2a/plugins/x2a-node/report.api.md +++ b/workspaces/x2a/plugins/x2a-node/report.api.md @@ -8,12 +8,12 @@ import type { Artifact } from '@red-hat-developer-hub/backstage-plugin-x2a-commo import type { BackstageCredentials } from '@backstage/backend-plugin-api'; import type { BackstageUserPrincipal } from '@backstage/backend-plugin-api'; import type { CatalogService } from '@backstage/plugin-catalog-node'; -import type { Job } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; +import { Job } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; import type { JobStatusEnum } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; import type { LoggerService } from '@backstage/backend-plugin-api'; import type { MigrationPhase } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; import { Module } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; -import type { ModuleStatus } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; +import { ModuleStatus } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; import type { PermissionsService } from '@backstage/backend-plugin-api'; import type { Project } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; import type { ProjectsGet } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; diff --git a/workspaces/x2a/plugins/x2a-node/src/moduleStatus.ts b/workspaces/x2a/plugins/x2a-node/src/moduleStatus.ts index 9774c49161..343fc931ef 100644 --- a/workspaces/x2a/plugins/x2a-node/src/moduleStatus.ts +++ b/workspaces/x2a/plugins/x2a-node/src/moduleStatus.ts @@ -14,9 +14,10 @@ * limitations under the License. */ -import type { - Job, - ModuleStatus, +import { + type Job, + JobStatus, + type ModuleStatus, } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; /** @@ -39,7 +40,7 @@ export function calculateModuleStatus({ publish?: Job; }): { status: ModuleStatus; errorDetails?: string } { const latestPhaseJob = publish ?? migrate ?? analyze; - if (latestPhaseJob?.status === 'cancelled') { + if (latestPhaseJob && JobStatus.from(latestPhaseJob.status).isCancelled()) { return { status: 'cancelled', errorDetails: undefined }; } diff --git a/workspaces/x2a/plugins/x2a-node/src/modulesReconcile.ts b/workspaces/x2a/plugins/x2a-node/src/modulesReconcile.ts index 56a4a77092..493b9842ef 100644 --- a/workspaces/x2a/plugins/x2a-node/src/modulesReconcile.ts +++ b/workspaces/x2a/plugins/x2a-node/src/modulesReconcile.ts @@ -16,6 +16,7 @@ import { type Module, + JobStatus, Phase, } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; @@ -35,7 +36,7 @@ export async function reconcileModuleJobs( const phases = Phase.modulePhaseValues(); for (const phase of phases) { const job = module[phase]; - if (job && ['pending', 'running'].includes(job.status)) { + if (job && JobStatus.from(job.status).isActive()) { const reconciled = await reconcileJobStatus(job, deps); module[phase] = removeSensitiveFromJob(reconciled); } diff --git a/workspaces/x2a/plugins/x2a-node/src/utils.ts b/workspaces/x2a/plugins/x2a-node/src/utils.ts index 6d7fa3ab1a..4b2ed8bcb4 100644 --- a/workspaces/x2a/plugins/x2a-node/src/utils.ts +++ b/workspaces/x2a/plugins/x2a-node/src/utils.ts @@ -21,7 +21,10 @@ import type { } from '@backstage/backend-plugin-api'; import { RELATION_MEMBER_OF } from '@backstage/catalog-model'; import type { CatalogService } from '@backstage/plugin-catalog-node'; -import type { Job } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; +import { + type Job, + JobStatus, +} from '@red-hat-developer-hub/backstage-plugin-x2a-common'; import type { ReconcileJobDeps } from './services'; @@ -121,7 +124,7 @@ export async function reconcileJobStatus( job: Job, deps: ReconcileJobDeps, ): Promise { - if (!['pending', 'running'].includes(job.status)) { + if (!JobStatus.from(job.status).isActive()) { return job; } if (!job.k8sJobName) { @@ -133,7 +136,8 @@ export async function reconcileJobStatus( ); const k8sStatus = await deps.kubeService.getJobStatus(job.k8sJobName); - if (k8sStatus.status === 'success' || k8sStatus.status === 'error') { + const k8sJobStatus = JobStatus.from(k8sStatus.status); + if (k8sJobStatus.isSuccess() || k8sJobStatus.isError()) { let log: string | null = null; try { log = (await deps.kubeService.getJobLogs(job.k8sJobName)) as string; diff --git a/workspaces/x2a/plugins/x2a/src/components/tools/canCancelPhase.ts b/workspaces/x2a/plugins/x2a/src/components/tools/canCancelPhase.ts index b54e46f793..6f49e18e3d 100644 --- a/workspaces/x2a/plugins/x2a/src/components/tools/canCancelPhase.ts +++ b/workspaces/x2a/plugins/x2a/src/components/tools/canCancelPhase.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { JobStatusEnum } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; +import { + JobStatus, + JobStatusEnum, +} from '@red-hat-developer-hub/backstage-plugin-x2a-common'; export const canCancelPhase = (phaseStatus?: JobStatusEnum): boolean => - phaseStatus === 'pending' || phaseStatus === 'running'; + !!phaseStatus && JobStatus.from(phaseStatus).isActive(); diff --git a/workspaces/x2a/plugins/x2a/src/components/tools/isEligibleForRetriggerInit.ts b/workspaces/x2a/plugins/x2a/src/components/tools/isEligibleForRetriggerInit.ts index 6e54602595..229ab349b6 100644 --- a/workspaces/x2a/plugins/x2a/src/components/tools/isEligibleForRetriggerInit.ts +++ b/workspaces/x2a/plugins/x2a/src/components/tools/isEligibleForRetriggerInit.ts @@ -13,7 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Project } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; +import { + JobStatus, + Project, +} from '@red-hat-developer-hub/backstage-plugin-x2a-common'; /** * A project is eligible for init-phase retrigger when it has no modules @@ -22,7 +25,7 @@ import { Project } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; export const isEligibleForRetriggerInit = (project: Project): boolean => { const initJobStatus = project.initJob?.status; const initRunning = - initJobStatus === 'running' || initJobStatus === 'pending'; + !!initJobStatus && JobStatus.from(initJobStatus).isActive(); const hasModules = !!project.status?.modulesSummary && project.status.modulesSummary.total > 0; return !hasModules && !initRunning;