1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-11-10 01:19:53 +01:00

feat: milestone progression executed at in read model (#10771)

This commit is contained in:
Mateusz Kwasniewski 2025-10-10 09:00:15 +02:00 committed by GitHub
parent 938d25828f
commit fce4c5bbab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 226 additions and 0 deletions

View File

@ -8,4 +8,5 @@ export interface ReleasePlanMilestone {
startedAt?: string;
transitionCondition?: { intervalMinutes: number };
strategies?: ReleasePlanMilestoneStrategy[];
progressionExecutedAt?: string;
}

View File

@ -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: [],
},
],
},
],
});
});

View File

@ -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,
};

View File

@ -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: