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

fix: clone environments (#10755)

https://linear.app/unleash/issue/2-3932/cloned-environments-enable-disabled-strategies-unexpectedly

Cloning environments didn't work as expected. This fixes a few of
issues:
 - Disabled strategies remain disabled after cloning
 - All strategy properties are cloned (including e.g. title)
 - Strategy cloning respects the selected projects
 - Release plans and their milestones are now correctly cloned
This commit is contained in:
Nuno Góis 2025-10-08 09:48:40 +01:00 committed by GitHub
parent 8072bc6706
commit 9948e577ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 96 additions and 55 deletions

View File

@ -475,71 +475,81 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore {
async cloneStrategies( async cloneStrategies(
sourceEnvironment: string, sourceEnvironment: string,
destinationEnvironment: string, destinationEnvironment: string,
projects: string[],
): Promise<void> { ): Promise<void> {
const stopTimer = this.timer('cloneStrategies'); const stopTimer = this.timer('cloneStrategies');
const sourceFeatureStrategies = await this.db(
'feature_strategies',
).where({
environment: sourceEnvironment,
});
const clonedStrategyRows = sourceFeatureStrategies.map( await this.db.transaction(async (trx) => {
(featureStrategy) => { const sourceFeatureStrategies = await trx(
return { 'feature_strategies as fs',
)
.join('features as f', 'f.name', 'fs.feature_name')
.select('fs.*')
.where('fs.environment', sourceEnvironment)
.whereIn('f.project', projects);
if (sourceFeatureStrategies.length === 0) {
return;
}
const clonedStrategyRows = sourceFeatureStrategies.map(
(featureStrategy) => ({
...featureStrategy,
id: uuidv4(), id: uuidv4(),
feature_name: featureStrategy.feature_name,
project_name: featureStrategy.project_name,
environment: destinationEnvironment, environment: destinationEnvironment,
strategy_name: featureStrategy.strategy_name,
parameters: JSON.stringify(featureStrategy.parameters), parameters: JSON.stringify(featureStrategy.parameters),
constraints: JSON.stringify(featureStrategy.constraints), constraints: JSON.stringify(featureStrategy.constraints),
sort_order: featureStrategy.sort_order,
variants: JSON.stringify(featureStrategy.variants), variants: JSON.stringify(featureStrategy.variants),
}; }),
},
);
if (clonedStrategyRows.length === 0) {
stopTimer();
return Promise.resolve();
}
await this.db('feature_strategies').insert(clonedStrategyRows);
const newStrategyMapping = new Map();
sourceFeatureStrategies.forEach((sourceStrategy, index) => {
newStrategyMapping.set(
sourceStrategy.id,
clonedStrategyRows[index].id,
); );
await trx('feature_strategies').insert(clonedStrategyRows);
const newStrategyIdByOld = new Map<string, string>();
sourceFeatureStrategies.forEach((s, i) => {
newStrategyIdByOld.set(s.id, clonedStrategyRows[i].id);
});
const segmentsToClone = await trx('feature_strategy_segment as fss')
.join(
'feature_strategies as fs',
'fss.feature_strategy_id',
'fs.id',
)
.join('features as f', 'f.name', 'fs.feature_name')
.select('fss.feature_strategy_id', 'fss.segment_id')
.where('fs.environment', sourceEnvironment)
.whereIn('f.project', projects);
if (segmentsToClone.length) {
const clonedSegmentRows = segmentsToClone
.map((row) => {
const mappedId = newStrategyIdByOld.get(
row.feature_strategy_id,
);
if (!mappedId) return null;
return {
feature_strategy_id: mappedId,
segment_id: row.segment_id,
};
})
.filter(
(
r,
): r is {
feature_strategy_id: string;
segment_id: number;
} => Boolean(r),
);
if (clonedSegmentRows.length) {
await trx('feature_strategy_segment').insert(
clonedSegmentRows,
);
}
}
}); });
const segmentsToClone: ISegmentRow[] = await this.db(
'feature_strategy_segment as fss',
)
.select(['id', 'segment_id'])
.join(
'feature_strategies AS fs',
'fss.feature_strategy_id',
'fs.id',
)
.where('environment', sourceEnvironment);
const clonedSegmentIdRows = segmentsToClone.map(
(existingSegmentRow) => {
return {
feature_strategy_id: newStrategyMapping.get(
existingSegmentRow.id,
),
segment_id: existingSegmentRow.segment_id,
};
},
);
if (clonedSegmentIdRows.length > 0) {
await this.db('feature_strategy_segment').insert(
clonedSegmentIdRows,
);
}
stopTimer(); stopTimer();
} }

View File

@ -370,4 +370,32 @@ export class ReleasePlanStore extends CRUDStore<
endTimer(); endTimer();
return present; return present;
} }
async getByEnvironmentAndProjects(
environment: string,
projects: string[],
): Promise<ReleasePlan[]> {
const endTimer = this.timer('getByEnvironmentAndProjects');
const rows = await this.db(`${this.tableName} AS rpd`)
.join('features AS f', 'f.name', 'rpd.feature_name')
.where('rpd.discriminator', 'plan')
.andWhere('rpd.environment', environment)
.whereIn('f.project', projects)
.leftJoin(
'milestones AS mi',
'mi.release_plan_definition_id',
'rpd.id',
)
.leftJoin('milestone_strategies AS ms', 'ms.milestone_id', 'mi.id')
.leftJoin(
'milestone_strategy_segments AS mss',
'mss.milestone_strategy_id',
'ms.id',
)
.orderBy('mi.sort_order', 'asc')
.orderBy('ms.sort_order', 'asc')
.select(selectColumns);
endTimer();
return processReleasePlanRows(rows);
}
} }

View File

@ -68,6 +68,7 @@ export interface IFeatureEnvironmentStore
cloneStrategies( cloneStrategies(
sourceEnvironment: string, sourceEnvironment: string,
destinationEnvironment: string, destinationEnvironment: string,
projects: string[],
): Promise<void>; ): Promise<void>;
addVariantsToFeatureEnvironment( addVariantsToFeatureEnvironment(
featureName: string, featureName: string,

View File

@ -165,7 +165,9 @@ test('Copying strategies also copies strategy variants', async () => {
}); });
await featureEnvironmentStore.connectProject('clone-2', 'default'); await featureEnvironmentStore.connectProject('clone-2', 'default');
await featureEnvironmentStore.cloneStrategies(envName, 'clone-2'); await featureEnvironmentStore.cloneStrategies(envName, 'clone-2', [
'default',
]);
const clonedStrategy = const clonedStrategy =
await featureStrategiesStore.getStrategiesForFeatureEnv( await featureStrategiesStore.getStrategiesForFeatureEnv(