Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: detect potential solution merge conflicts #57

Merged
merged 10 commits into from
Feb 3, 2021
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ You must provide several package deployer settings parameters. Refer to the help

```powershell
$settings = [PSCustomObject]@{
ApprovalsConnectionName = '<the connection name of the Approvals connection>'
AzureDevOpsConnectionName = '<the connection name of the Azure DevOps connection>'
AzureDevOpsOrganisation = '<the name of the Azure DevOps organisation>'
SolutionPublisherPrefix = '<the prefix of the publisher (without leading underscore)>'
'ConnRef:devhub_sharedapprovals_6d3fc' = '<the connection name of the Approvals connection>'
'ConnRef:devhub_sharedvisualstudioteamservices_bf2bc' = '<the connection name of the Azure DevOps connection>'
'AzureDevOpsOrganisation' = '<the name of the Azure DevOps organisation>'
'SolutionPublisherPrefix' = '<the prefix of the publisher (without leading underscore)>'
}
$settingsArray = $obj.PSObject.Properties | ForEach-Object { "$($_.Name)=$($_.Value)" }
$settingsArray = $settings.PSObject.Properties | ForEach-Object { "$($_.Name)=$($_.Value)" }
$runtimePackageSettings = [string]::Join("|", $settingsArray)

Import-CrmPackage -PackageInformation $packages[0] -CrmConnection $conn -RuntimePackageSettings $runtimePackageSettings
Expand Down
2 changes: 1 addition & 1 deletion deploy/DevelopmentHub.Deployment.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Capgemini.PowerApps.PackageDeployerTemplate" Version="0.2.2" />
<PackageReference Include="Capgemini.PowerApps.PackageDeployerTemplate" Version="0.2.4" />
</ItemGroup>
<ItemGroup>
<Reference Include="PresentationFramework" />
Expand Down
2 changes: 1 addition & 1 deletion deploy/PackageTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public override bool AfterPrimaryImport()
/// <inheritdoc />
public override UserRequestedImportAction OverrideSolutionImportDecision(string solutionUniqueName, Version organizationVersion, Version packageSolutionVersion, Version inboundSolutionVersion, Version deployedSolutionVersion, ImportAction systemSelectedImportAction)
{
if (this.ForceImportOnSameVersion && systemSelectedImportAction == ImportAction.SkipSameVersion)
if (this.ForceImportOnSameVersion && (systemSelectedImportAction == ImportAction.SkipSameVersion || systemSelectedImportAction == ImportAction.SkipLowerVersion))
{
return UserRequestedImportAction.ForceUpdate;
}
Expand Down
1 change: 1 addition & 0 deletions deploy/PkgFolder/ImportConfig.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
<configsolutionfile overwriteunmanagedcustomizations="false" publishworkflowsandactivateplugins="true" solutionpackagefilename="devhub_DevelopmentHub_Develop.zip" />
<configsolutionfile overwriteunmanagedcustomizations="false" publishworkflowsandactivateplugins="true" solutionpackagefilename="devhub_DevelopmentHub_AzureDevOps.zip" />
</solutions>
<templateconfig />
</configdatastorage>
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,75 @@ namespace DevelopmentHub.Develop {
});
}

async function getActiveIssues(select: string[]) {
return (await Xrm.WebApi.online.retrieveMultipleRecords(
'devhub_issue',
`?$select=${select.join(',')}&$filter=statuscode eq 353400000 or statuscode eq 353400002`,
)).entities;
}

async function getConflictingSolutions(context: Xrm.FormContext): Promise<any[]> {
const issueAttr: Xrm.Attributes.LookupAttribute = context.getAttribute('devhub_issue');

const issueRef = issueAttr && issueAttr.getValue();
if (!issueRef) {
return [];
}

const activeIssues = await getActiveIssues(['devhub_developmentsolution', 'devhub_name']);
if (!activeIssues || activeIssues.length === 0) {
return [];
}

const thisIssue = activeIssues.find((i) => `{${i.devhub_issueid.toUpperCase()}}` === issueRef[0].id);
const filter = `(Microsoft.Dynamics.CRM.In(PropertyName='uniquename',PropertyValues=[${activeIssues.map((i) => `'${i.devhub_developmentsolution}'`)}]))`;
const solutions = (await Xrm.WebApi.online.retrieveMultipleRecords(
'solution',
`?$select=uniquename&$filter=${filter}&$expand=solution_solutioncomponent($select=objectid,componenttype,rootcomponentbehavior,rootsolutioncomponentid)`,
)).entities;

const thisSolutionIndex = solutions
.findIndex((s) => s.uniquename === thisIssue.devhub_developmentsolution);
const toMergeSol = solutions[thisSolutionIndex];
solutions.splice(thisSolutionIndex, 1);

const conflicts = solutions.filter((sol) => sol.solution_solutioncomponent.some((comp) => {
const match = toMergeSol.solution_solutioncomponent.find(
(toMergeComp) => comp.objectid === toMergeComp.objectid,
);

// Entity
if (comp.componenttype === 1) {
if (match && comp.rootcomponentbehavior < 2 && match.rootcomponentbehavior < 2) {
// Check for both entities including all subcomponents or metadata
return true;
}
if (comp.rootcomponentbehavior === 0) {
// Check for to merge solution containing a subcomponent of this entity
const entityComponent = toMergeSol.solution_solutioncomponent.find(
(c) => c.objectid === comp.objectid,
);
return toMergeSol.solution_solutioncomponent.some(
(toMergeComponent) => entityComponent.solutioncomponentid
=== toMergeComponent.rootsolutioncomponentid,
);
}
}

if (!match && comp.rootsolutioncomponentid) {
// Check for to merge solution containing the root entity with all subcomponents
return toMergeSol.solution_solutioncomponent.some(
(toMergeComponent) => toMergeComponent.objectid === comp.rootsolutioncomponentid
&& toMergeComponent.rootcomponentbehavior === 0,
);
}

return !!match;
}));

return conflicts;
}

export function isReviewEnabled(primaryControl: Xrm.FormContext): boolean {
return primaryControl.getAttribute('statuscode').getValue() === SolutionMergeStatusCode.AwaitingReview
&& primaryControl.ui.getFormType() !== XrmEnum.FormType.Create;
Expand All @@ -78,13 +147,26 @@ namespace DevelopmentHub.Develop {
});
}

export function approve(primaryControl: Xrm.FormContext): void {
export async function approve(primaryControl: Xrm.FormContext) {
const entity = primaryControl.data.entity.getEntityReference();

executeWebApiRequest(new ApproveRequest(entity), 'Approving solution merge.')
.then(async () => {
primaryControl.data.refresh(false);
Xrm.Utility.showProgressIndicator('Checking for conflicts.');
const conflictingSolutions = await getConflictingSolutions(primaryControl);
if (conflictingSolutions.length > 0) {
const confirmResult = await Xrm.Navigation.openConfirmDialog({
text: `Components in this solution were also found in the following solutions: ${conflictingSolutions.map((s) => s.uniquename).join(', ').toString()}. Please ensure that unintended changes have not been introduced changes to components in this solution.`,
title: 'Possible conflict detected.',
confirmButtonLabel: 'Approve',
});

if (!confirmResult.confirmed) {
Xrm.Utility.closeProgressIndicator();
return;
}
}
await executeWebApiRequest(new ApproveRequest(entity), 'Approving solution merge.');
await primaryControl.data.refresh(false);
Xrm.Utility.closeProgressIndicator();
}

export function reject(primaryControl: Xrm.FormContext): void {
Expand Down
1 change: 1 addition & 0 deletions templates/include-build-stage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ stages:
projectVersion: '$(GitVersion.SemVer)'
extraProperties: |
sonar.eslint.reportPaths=$(Build.SourcesDirectory)/src/solutions/devhub_DevelopmentHub_Develop/WebResources/Scripts/test_results/analysis/eslint.json
sonar.coverage.exclusions=src/solutions/**/*.ts, deploy/**/*.cs
- task: PowerShell@2
displayName: 'Build package'
inputs:
Expand Down