diff --git a/src/lib/features/release-plans/release-plan-milestone.ts b/src/lib/features/release-plans/release-plan-milestone.ts index b13f92ddd0..2faa938d90 100644 --- a/src/lib/features/release-plans/release-plan-milestone.ts +++ b/src/lib/features/release-plans/release-plan-milestone.ts @@ -8,4 +8,5 @@ export interface ReleasePlanMilestone { startedAt?: string; transitionCondition?: { intervalMinutes: number }; strategies?: ReleasePlanMilestoneStrategy[]; + progressionExecutedAt?: string; } diff --git a/src/lib/features/release-plans/release-plan-read-model.test.ts b/src/lib/features/release-plans/release-plan-read-model.test.ts new file mode 100644 index 0000000000..dc3041d683 --- /dev/null +++ b/src/lib/features/release-plans/release-plan-read-model.test.ts @@ -0,0 +1,214 @@ +import { ulid } from 'ulidx'; +import dbInit, { + type ITestDb, +} from '../../../test/e2e/helpers/database-init.js'; +import getLogger from '../../../test/fixtures/no-logger.js'; +import { ReleasePlanReadModel } from './release-plan-read-model.js'; +import type { IReleasePlanReadModel } from './release-plan-read-model-type.js'; +import type { IFeatureToggleStore } from '../feature-toggle/types/feature-toggle-store-type.js'; +import type { ReleasePlanStore } from './release-plan-store.js'; +import type { ReleasePlanMilestoneStore } from './release-plan-milestone-store.js'; +import type { IFeatureEnvironmentStore } from '../../types/stores/feature-environment-store.js'; + +let db: ITestDb; +let releasePlanReadModel: IReleasePlanReadModel; +let featureToggleStore: IFeatureToggleStore; +let releasePlanStore: ReleasePlanStore; +let releasePlanMilestoneStore: ReleasePlanMilestoneStore; +let featureEnvironmentStore: IFeatureEnvironmentStore; + +beforeAll(async () => { + db = await dbInit('release_plan_read_model', getLogger); + releasePlanReadModel = new ReleasePlanReadModel(db.rawDatabase); + featureToggleStore = db.stores.featureToggleStore; + releasePlanStore = db.stores.releasePlanStore; + releasePlanMilestoneStore = db.stores.releasePlanMilestoneStore; + featureEnvironmentStore = db.stores.featureEnvironmentStore; +}); + +afterAll(async () => { + if (db) { + await db.destroy(); + } +}); + +beforeEach(async () => { + await releasePlanStore.deleteAll(); + await releasePlanMilestoneStore.deleteAll(); + await featureToggleStore.deleteAll(); + await featureEnvironmentStore.deleteAll(); + + await db.rawDatabase('milestone_progressions').del(); +}); + +const createReleasePlan = async (planData: { + name: string; + description: string; + featureName: string; + environment: string; + createdByUserId: number; + activeMilestoneId?: string; +}) => { + const id = ulid(); + await db.rawDatabase('release_plan_definitions').insert({ + id, + name: planData.name, + description: planData.description, + feature_name: planData.featureName, + environment: planData.environment, + created_by_user_id: planData.createdByUserId, + active_milestone_id: planData.activeMilestoneId || null, + discriminator: 'plan', + created_at: new Date(), + }); + return { + id, + name: planData.name, + description: planData.description, + featureName: planData.featureName, + environment: planData.environment, + createdByUserId: planData.createdByUserId, + activeMilestoneId: planData.activeMilestoneId || null, + }; +}; + +const createMilestone = async (milestoneData: { + name: string; + sortOrder: number; + releasePlanDefinitionId: string; + startedAt?: Date; +}) => { + const milestone = await releasePlanMilestoneStore.insert({ + name: milestoneData.name, + sortOrder: milestoneData.sortOrder, + releasePlanDefinitionId: milestoneData.releasePlanDefinitionId, + }); + + if (milestoneData.startedAt) { + await db + .rawDatabase('milestones') + .where('id', milestone.id) + .update('started_at', milestoneData.startedAt); + } + + return { + id: milestone.id, + name: milestone.name, + sortOrder: milestone.sortOrder, + releasePlanDefinitionId: milestone.releasePlanDefinitionId, + startedAt: milestoneData.startedAt || null, + }; +}; + +const createMilestoneProgression = async (progressionData: { + sourceMilestoneId: string; + targetMilestoneId: string; + transitionCondition?: object; + executedAt?: Date; +}) => { + await db.rawDatabase('milestone_progressions').insert({ + id: ulid(), + source_milestone: progressionData.sourceMilestoneId, + target_milestone: progressionData.targetMilestoneId, + transition_condition: progressionData.transitionCondition || null, + executed_at: progressionData.executedAt || null, + }); + return { + transitionCondition: progressionData.transitionCondition || null, + executedAt: progressionData.executedAt || null, + }; +}; + +test('should return release plans with complete milestone data', async () => { + await featureToggleStore.create('default', { + name: 'test-feature', + createdByUserId: 1, + }); + await featureEnvironmentStore.addEnvironmentToFeature( + 'test-feature', + 'development', + true, + ); + + const plan = await createReleasePlan({ + name: 'Test Plan', + description: 'Test plan', + featureName: 'test-feature', + environment: 'development', + createdByUserId: 1, + }); + + const startedAt = new Date('2024-01-10T08:00:00.000Z'); + const milestone1 = await createMilestone({ + name: 'Milestone 1', + sortOrder: 1, + releasePlanDefinitionId: plan.id, + startedAt: startedAt, + }); + + const milestone2 = await createMilestone({ + name: 'Milestone 2', + sortOrder: 2, + releasePlanDefinitionId: plan.id, + }); + + await releasePlanStore.update(plan.id, { + activeMilestoneId: milestone1.id, + }); + + const transitionCondition = { intervalMinutes: 60 }; + const executedAt = new Date('2024-01-15T10:00:00.000Z'); + const milestoneProgression = await createMilestoneProgression({ + sourceMilestoneId: milestone1.id, + targetMilestoneId: milestone2.id, + transitionCondition: transitionCondition, + executedAt: executedAt, + }); + + const releasePlans = await releasePlanReadModel.getReleasePlans( + 'test-feature', + ['development'], + ); + + expect(releasePlans).toMatchObject({ + development: [ + { + id: plan.id, + discriminator: 'plan', + name: plan.name, + description: plan.description, + featureName: plan.featureName, + environment: plan.environment, + createdByUserId: plan.createdByUserId, + createdAt: expect.any(Date), + activeMilestoneId: milestone1.id, + releasePlanTemplateId: null, + milestones: [ + { + id: milestone1.id, + name: milestone1.name, + sortOrder: milestone1.sortOrder, + releasePlanDefinitionId: + milestone1.releasePlanDefinitionId, + startedAt: milestone1.startedAt, + progressionExecutedAt: milestoneProgression.executedAt, + transitionCondition: + milestoneProgression.transitionCondition, + strategies: [], + }, + { + id: milestone2.id, + name: milestone2.name, + sortOrder: milestone2.sortOrder, + releasePlanDefinitionId: + milestone2.releasePlanDefinitionId, + startedAt: milestone2.startedAt, + progressionExecutedAt: null, + transitionCondition: null, + strategies: [], + }, + ], + }, + ], + }); +}); diff --git a/src/lib/features/release-plans/release-plan-read-model.ts b/src/lib/features/release-plans/release-plan-read-model.ts index 79e7e463c1..8ece5c3f87 100644 --- a/src/lib/features/release-plans/release-plan-read-model.ts +++ b/src/lib/features/release-plans/release-plan-read-model.ts @@ -20,6 +20,7 @@ const selectColumns = [ 'mi.sort_order AS milestoneSortOrder', 'mi.started_at AS milestoneStartedAt', 'mp.transition_condition AS milestoneTransitionCondition', + 'mp.executed_at AS milestoneProgressionExecutedAt', 'ms.id AS strategyId', 'ms.sort_order AS strategySortOrder', 'ms.title AS strategyTitle', @@ -50,6 +51,7 @@ const processReleasePlanRows = (templateRows): ReleasePlan[] => milestoneSortOrder, milestoneStartedAt, milestoneTransitionCondition, + milestoneProgressionExecutedAt, strategyId, strategySortOrder, strategyTitle, @@ -93,6 +95,7 @@ const processReleasePlanRows = (templateRows): ReleasePlan[] => sortOrder: milestoneSortOrder, startedAt: milestoneStartedAt, transitionCondition: milestoneTransitionCondition, + progressionExecutedAt: milestoneProgressionExecutedAt, strategies: [], releasePlanDefinitionId: planId, }; diff --git a/src/lib/openapi/spec/release-plan-milestone-schema.ts b/src/lib/openapi/spec/release-plan-milestone-schema.ts index a228af5053..f5e08c34b9 100644 --- a/src/lib/openapi/spec/release-plan-milestone-schema.ts +++ b/src/lib/openapi/spec/release-plan-milestone-schema.ts @@ -49,6 +49,14 @@ export const releasePlanMilestoneSchema = { additionalProperties: true, nullable: true, }, + progressionExecutedAt: { + type: 'string', + format: 'date-time', + description: + 'The date and time when the milestone progression was executed.', + example: '2024-01-01T00:00:00.000Z', + nullable: true, + }, strategies: { type: 'array', description: