Skip to content

Commit 611b601

Browse files
authored
Fix bug in configure-continuous-deployments command and maintain developer CLI (#684)
### Summary & Motivation Fix an issue where the `configure-continuous-deployments` command failed when the `az` CLI threw an exception. Previously, these exceptions were handled correctly, but a recent update to how the CLI executes commands introduced an issue where errors were not always caught properly. This is now fixed. Additionally, Azure region identifiers have been improved. The previous region abbreviations (e.g., `gwc` for Germany West Central) were not always intuitive. These have been updated to use country-specific ISO codes where applicable (e.g., `us`, `ca`, `uk`), and regional codes for areas with shared data sovereignty rules (e.g., `eu` for the European Union, `as` for Southeast Asia). Other general maintenance updates have also been made to the developer CLI: - Ensure consistency in CLI descriptions and output messages by standardizing trailing periods. - Upgrade all NuGet packages, .NET SDK, and JetBrains tools to their latest versions. - Fix static code analysis warnings from SonarQube and JetBrains. - Remove `ConfigurationSettings.ConfigurationSetting`, which has been marked as obsolete for a while. - Add an explicit reference to System.IO.Pipelines Version=9.0.1 to avoid having to double build when the CLI detects changes. ### Checklist - [x] I have added tests, or done manual regression tests - [x] I have updated the documentation, if necessary
2 parents 229e390 + 8cf249f commit 611b601

File tree

11 files changed

+121
-144
lines changed

11 files changed

+121
-144
lines changed

developer-cli/Commands/CodeCoverageCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ private int Execute(string? solutionName)
3838
);
3939

4040
var codeCoverageReport = Path.Combine(Configuration.ApplicationFolder, "coverage", "dotCover.html");
41-
AnsiConsole.MarkupLine($"[green]Code Coverage Report[/] {codeCoverageReport}");
41+
AnsiConsole.MarkupLine($"[green]Code Coverage Report.[/] {codeCoverageReport}");
4242
ProcessHelper.StartProcess($"open {codeCoverageReport}", Configuration.ApplicationFolder);
4343

4444
return 0;

developer-cli/Commands/ConfigureContinuousDeploymentsCommand.cs

Lines changed: 83 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ public class ConfigureContinuousDeploymentsCommand : Command
1919

2020
private static readonly Dictionary<string, string> AzureLocations = GetAzureLocations();
2121

22-
private static List<ConfigureContinuousDeployments>? _configureContinuousDeploymentsExtensions;
22+
private List<ConfigureContinuousDeployments>? _configureContinuousDeploymentsExtensions;
2323

2424
public ConfigureContinuousDeploymentsCommand() : base(
2525
"configure-continuous-deployments",
26-
"Set up trust between Azure and GitHub for passwordless deployments using OpenID Connect."
26+
"Set up trust between Azure and GitHub for passwordless deployments using OpenID Connect"
2727
)
2828
{
2929
AddOption(new Option<bool>(["--verbose-logging"], "Print Azure and GitHub CLI commands and output"));
@@ -242,9 +242,9 @@ private void SelectAzureSubscriptions()
242242
Config.StagingSubscription = SelectSubscription("Staging");
243243
Config.ProductionSubscription = SelectSubscription("Production");
244244

245-
if(Config.StagingSubscription.TenantId != Config.ProductionSubscription.TenantId)
245+
if (Config.StagingSubscription.TenantId != Config.ProductionSubscription.TenantId)
246246
{
247-
AnsiConsole.MarkupLine($"[red]ERROR:[/] Please select two subscriptions from the same tenant, and try again.");
247+
AnsiConsole.MarkupLine("[red]ERROR:[/] Please select two subscriptions from the same tenant, and try again.");
248248
Environment.Exit(1);
249249
}
250250

@@ -468,55 +468,55 @@ private void ConfirmChangesPrompt()
468468
[bold]Please review planned changes before continuing.[/]
469469
470470
1. The following will be created or updated in Azure:
471-
471+
472472
[bold]Active Directory App Registrations/Service Principals:[/]
473473
* [blue]{Config.StagingSubscription.AppRegistration.Name}[/] with access to the [blue]{Config.StagingSubscription.Name}[/] subscription.
474474
* [blue]{Config.ProductionSubscription.AppRegistration.Name}[/] with access to the [blue]{Config.ProductionSubscription.Name}[/] subscription.
475-
475+
476476
[yellow]** The Service Principals will get 'Contributor' and 'User Access Administrator' role on the Azure Subscriptions.[/]
477-
477+
478478
[bold]Active Directory Security Groups:[/]
479479
* [blue]{Config.StagingSubscription.SqlAdminsGroup.Name}[/]
480480
* [blue]{Config.ProductionSubscription.SqlAdminsGroup.Name}[/]
481-
481+
482482
[yellow]** The SQL Admins Security Groups are used to grant Managed Identities and CI/CD permissions to SQL Databases.[/]
483483
484484
2. The following GitHub environments will be created if not exists:
485485
* [blue]staging[/]
486486
* [blue]production[/]
487-
487+
488488
[yellow]** Environments are used to require approval when infrastructure is deployed. In private GitHub repositories, this requires a paid plan.[/]
489489
490490
3. The following GitHub repository variables will be created:
491-
491+
492492
[bold]Shared Variables:[/]
493493
* TENANT_ID: [blue]{Config.TenantId}[/]
494494
* UNIQUE_PREFIX: [blue]{Config.UniquePrefix}[/]
495-
495+
496496
[bold]Staging Shared Variables:[/]
497497
* STAGING_SUBSCRIPTION_ID: [blue]{Config.StagingSubscription.Id}[/]
498498
* STAGING_SHARED_LOCATION: [blue]{Config.StagingLocation.SharedLocation}[/]
499499
* STAGING_SERVICE_PRINCIPAL_ID: [blue]{stagingServicePrincipal}[/]
500500
* STAGING_SQL_ADMIN_OBJECT_ID: [blue]{stagingSqlAdminObject}[/]
501501
* STAGING_DOMAIN_NAME: [blue]-[/] ([yellow]Manually changed this and triggered deployment to set up the domain[/])
502-
502+
503503
[bold]Staging Cluster Variables:[/]
504504
* STAGING_CLUSTER_ENABLED: [blue]true[/]
505505
* STAGING_CLUSTER_LOCATION: [blue]{Config.StagingLocation.ClusterLocation}[/]
506506
* STAGING_CLUSTER_LOCATION_ACRONYM: [blue]{Config.StagingLocation.ClusterLocationAcronym}[/]
507-
507+
508508
[bold]Production Shared Variables:[/]
509509
* PRODUCTION_SUBSCRIPTION_ID: [blue]{Config.ProductionSubscription.Id}[/]
510510
* PRODUCTION_SHARED_LOCATION: [blue]{Config.ProductionLocation.SharedLocation}[/]
511511
* PRODUCTION_SERVICE_PRINCIPAL_ID: [blue]{productionServicePrincipal}[/]
512512
* PRODUCTION_SQL_ADMIN_OBJECT_ID: [blue]{productionSqlAdminObject}[/]
513513
* PRODUCTION_DOMAIN_NAME: [blue]-[/] ([yellow]Manually changed this and triggered deployment to set up the domain[/])
514-
514+
515515
[bold]Production Cluster 1 Variables:[/]
516516
* PRODUCTION_CLUSTER1_ENABLED: [blue]false[/] ([yellow]Change this to 'true' when ready to deploy to production[/])
517517
* PRODUCTION_CLUSTER1_LOCATION: [blue]{Config.ProductionLocation.ClusterLocation}[/]
518518
* PRODUCTION_CLUSTER1_LOCATION_ACRONYM: [blue]{Config.ProductionLocation.ClusterLocationAcronym}[/]
519-
519+
520520
[yellow]** All variables can be changed on the GitHub Settings page. For example, if you want to deploy production or staging to different locations.[/]
521521
522522
4. Disable the reusable GitHub workflows [blue]Deploy Container[/] and [blue]Plan and Deploy Infrastructure[/].
@@ -619,22 +619,24 @@ void CreateFederatedCredential(string appRegistrationId, string displayName, str
619619
{
620620
var parameters = JsonSerializer.Serialize(new
621621
{
622-
name = displayName,
623-
issuer = "https://token.actions.githubusercontent.com",
624-
subject = $"""repo:{Config.GithubInfo!.Path}:{refRefsHeadsMain}""",
625-
audiences = new[] { "api://AzureADTokenExchange" }
626-
}
622+
name = displayName,
623+
issuer = "https://token.actions.githubusercontent.com",
624+
subject = $"""repo:{Config.GithubInfo!.Path}:{refRefsHeadsMain}""",
625+
audiences = new[] { "api://AzureADTokenExchange" }
626+
}
627627
);
628628

629629
ProcessHelper.StartProcess(new ProcessStartInfo
630630
{
631-
FileName = Configuration.IsWindows ? "cmd.exe" : "az",
632-
Arguments =
633-
$"{(Configuration.IsWindows ? "/C az" : string.Empty)} ad app federated-credential create --id {appRegistrationId} --parameters @-",
634-
RedirectStandardInput = true,
635-
RedirectStandardOutput = !Configuration.VerboseLogging,
636-
RedirectStandardError = !Configuration.VerboseLogging
637-
}, parameters
631+
FileName = Configuration.IsWindows ? "cmd.exe" : "az",
632+
Arguments =
633+
$"{(Configuration.IsWindows ? "/C az" : string.Empty)} ad app federated-credential create --id {appRegistrationId} --parameters @-",
634+
RedirectStandardInput = true,
635+
RedirectStandardOutput = !Configuration.VerboseLogging,
636+
RedirectStandardError = !Configuration.VerboseLogging
637+
},
638+
parameters,
639+
exitOnError: false
638640
);
639641
}
640642
}
@@ -773,6 +775,7 @@ void DisableActiveWorkflow(string workflowName)
773775
}
774776
}
775777

778+
// ReSharper disable once MemberCanBeMadeStatic.Local
776779
private void TriggerAndMonitorWorkflows()
777780
{
778781
AnsiConsole.Status().Start("Begin deployment.", ctx =>
@@ -872,9 +875,9 @@ private void ShowSuccessMessage()
872875
- To add a step for manual approval during infrastructure deployment to the staging and production environments, set up required reviewers on GitHub environments. Visit [blue]{Config.GithubInfo!.Url}/settings/environments[/] and enable [blue]Required reviewers[/] for the [bold]staging[/] and [bold]production[/] environments. Requires a paid GitHub plan for private repositories.
873876
874877
- Configure the Domain Name for the staging and production environments. This involves two steps:
875-
878+
876879
a. Go to [blue]{Config.GithubInfo!.Url}/settings/variables/actions[/] to set the [blue]DOMAIN_NAME_STAGING[/] and [blue]DOMAIN_NAME_PRODUCTION[/] variables. E.g. [blue]staging.your-saas-company.com[/] and [blue]your-saas-company.com[/].
877-
880+
878881
b. Run the [blue]Cloud Infrastructure - Deployment[/] workflow again. Note that it might fail with an error message to set up a DNS TXT and CNAME record. Once done, re-run the failed jobs.
879882
880883
- Set up SonarCloud for code quality and security analysis. This service is free for public repositories. Visit [blue]https://sonarcloud.io[/] to connect your GitHub account. Add the [blue]SONAR_TOKEN[/] secret, and the [blue]SONAR_ORGANIZATION[/] and [blue]SONAR_PROJECT_KEY[/] variables to the GitHub repository. The workflows are already configured for SonarCloud analysis.
@@ -905,7 +908,7 @@ private string RunAzureCliCommand(string arguments, bool redirectOutput = true)
905908
{
906909
var azureCliCommand = Configuration.IsWindows ? "cmd.exe /C az" : "az";
907910

908-
return ProcessHelper.StartProcess($"{azureCliCommand} {arguments}", redirectOutput: redirectOutput);
911+
return ProcessHelper.StartProcess($"{azureCliCommand} {arguments}", redirectOutput: redirectOutput, exitOnError: false);
909912
}
910913

911914
private static Dictionary<string, string> GetAzureLocations()
@@ -915,51 +918,51 @@ private static Dictionary<string, string> GetAzureLocations()
915918
// Location Acronyms are taken from here https://learn.microsoft.com/en-us/azure/backup/scripts/geo-code-list
916919
return new Dictionary<string, string>
917920
{
918-
{ "Australia Central", "acl" },
919-
{ "Australia Central 2", "acl2" },
920-
{ "Australia East", "ae" },
921-
{ "Australia Southeast", "ase" },
922-
{ "Brazil South", "brs" },
923-
{ "Brazil Southeast", "bse" },
924-
{ "Canada Central", "cnc" },
925-
{ "Canada East", "cne" },
926-
{ "Central India", "inc" },
927-
{ "Central US", "cus" },
928-
{ "East Asia", "ea" },
929-
{ "East US", "eus" },
930-
{ "East US 2", "eus2" },
931-
{ "France Central", "frc" },
932-
{ "France South", "frs" },
933-
{ "Germany North", "gn" },
934-
{ "Germany West Central", "gwc" },
935-
{ "Japan East", "jpe" },
936-
{ "Japan West", "jpw" },
937-
{ "Jio India Central", "jic" },
938-
{ "Jio India West", "jiw" },
939-
{ "Korea Central", "krc" },
940-
{ "Korea South", "krs" },
941-
{ "North Central US", "ncus" },
942-
{ "North Europe", "ne" },
943-
{ "Norway East", "nwe" },
944-
{ "Norway West", "nww" },
945-
{ "South Africa North", "san" },
946-
{ "South Africa West", "saw" },
947-
{ "South Central US", "scus" },
948-
{ "South India", "ins" },
949-
{ "Southeast Asia", "sea" },
950-
{ "Sweden Central", "sdc" },
951-
{ "Switzerland North", "szn" },
952-
{ "Switzerland West", "szw" },
953-
{ "UAE Central", "uac" },
954-
{ "UAE North", "uan" },
955-
{ "UK South", "uks" },
956-
{ "UK West", "ukw" },
957-
{ "West Central US", "wcus" },
958-
{ "West Europe", "we" },
959-
{ "West India", "inw" },
960-
{ "West US", "wus" },
961-
{ "West US 2", "wus2" },
962-
{ "West US 3", "wus3" }
921+
{ "Australia Central", "au" },
922+
{ "Australia Central 2", "au" },
923+
{ "Australia East", "au" },
924+
{ "Australia Southeast", "au" },
925+
{ "Brazil South", "br" },
926+
{ "Brazil Southeast", "br" },
927+
{ "Canada Central", "ca" },
928+
{ "Canada East", "ca" },
929+
{ "Central India", "in" },
930+
{ "Central US", "us" },
931+
{ "East Asia", "as" },
932+
{ "East US", "us" },
933+
{ "East US 2", "us" },
934+
{ "France Central", "eu" },
935+
{ "France South", "eu" },
936+
{ "Germany North", "eu" },
937+
{ "Germany West Central", "eu" },
938+
{ "Japan East", "jp" },
939+
{ "Japan West", "jp" },
940+
{ "Jio India Central", "in" },
941+
{ "Jio India West", "in" },
942+
{ "Korea Central", "kr" },
943+
{ "Korea South", "kr" },
944+
{ "North Central US", "us" },
945+
{ "North Europe", "eu" },
946+
{ "Norway East", "no" },
947+
{ "Norway West", "no" },
948+
{ "South Africa North", "za" },
949+
{ "South Africa West", "za" },
950+
{ "South Central US", "us" },
951+
{ "South India", "in" },
952+
{ "Southeast Asia", "as" },
953+
{ "Sweden Central", "eu" },
954+
{ "Switzerland North", "ch" },
955+
{ "Switzerland West", "ch" },
956+
{ "UAE Central", "ae" },
957+
{ "UAE North", "ae" },
958+
{ "UK South", "uk" },
959+
{ "UK West", "uk" },
960+
{ "West Central US", "us" },
961+
{ "West Europe", "eu" },
962+
{ "West India", "in" },
963+
{ "West US", "us" },
964+
{ "West US 2", "us" },
965+
{ "West US 3", "us" }
963966
};
964967
}
965968
}
@@ -968,17 +971,17 @@ public class Config
968971
{
969972
public string TenantId => StagingSubscription.TenantId;
970973

971-
public string UniquePrefix { get; set; } = default!;
974+
public string UniquePrefix { get; set; } = null!;
972975

973976
public GithubInfo? GithubInfo { get; private set; }
974977

975-
public Subscription StagingSubscription { get; set; } = default!;
978+
public Subscription StagingSubscription { get; set; } = null!;
976979

977-
public Location StagingLocation { get; set; } = default!;
980+
public Location StagingLocation { get; set; } = null!;
978981

979-
public Subscription ProductionSubscription { get; set; } = default!;
982+
public Subscription ProductionSubscription { get; set; } = null!;
980983

981-
public Location ProductionLocation { get; set; } = default!;
984+
public Location ProductionLocation { get; set; } = null!;
982985

983986
public Dictionary<string, string> GithubVariables { get; set; } = new();
984987

developer-cli/Commands/InstallCommand.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Each command is one C# class that can be customized to automate your own workflo
3838

3939
public InstallCommand() : base(
4040
"install",
41-
$"This will register the alias {Configuration.AliasName} so it will be available everywhere."
41+
$"This will register the alias {Configuration.AliasName} so it will be available everywhere"
4242
)
4343
{
4444
Handler = CommandHandler.Create(Execute);
@@ -53,7 +53,8 @@ private void Execute()
5353
var installedAliasPath = Configuration.GetConfigurationSetting().CliSourceCodeFolder!;
5454
AnsiConsole.MarkupLine(Environment.ProcessPath!.StartsWith(installedAliasPath)
5555
? $"[yellow]The CLI is already installed please run {Configuration.AliasName} to use it.[/]"
56-
: $"[yellow]There is already a CLI with the alias '{Configuration.AliasName}' installed in {installedAliasPath}.[/]");
56+
: $"[yellow]There is already a CLI with the alias '{Configuration.AliasName}' installed in {installedAliasPath}.[/]"
57+
);
5758

5859
return;
5960
}

developer-cli/Commands/PullPlatformPlatformChangesCommand.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private static int Execute(bool verboseLogging, bool autoConfirm, bool resume, b
7272

7373
if (pullRequestCommits.Length == 0 && newCommits.Length == 0)
7474
{
75-
AnsiConsole.MarkupLine("[yellow]Everything look up to date.[/]");
75+
AnsiConsole.MarkupLine("[yellow]Everything looks up to date.[/]");
7676
Environment.Exit(0);
7777
}
7878

@@ -296,7 +296,7 @@ private static void BuildTestAndCleanupCode(bool runCodeCleanup)
296296

297297
void BuildSolution()
298298
{
299-
AnsiConsole.MarkupLine("[green]Building backend and frontend backend[/]");
299+
AnsiConsole.MarkupLine("[green]Building backend and frontend backend.[/]");
300300
while (true)
301301
{
302302
try
@@ -325,7 +325,7 @@ void RunTests()
325325
{
326326
try
327327
{
328-
AnsiConsole.MarkupLine("[green]Running tests[/]");
328+
AnsiConsole.MarkupLine("[green]Running tests.[/]");
329329
ProcessHelper.StartProcess("dotnet test", Configuration.ApplicationFolder, throwOnError: true);
330330
break;
331331
}
@@ -347,7 +347,7 @@ void CleanupBackendCode()
347347
{
348348
try
349349
{
350-
AnsiConsole.MarkupLine("[green]Running cleanup[/]");
350+
AnsiConsole.MarkupLine("[green]Running cleanup.[/]");
351351

352352
var solutionFile = Directory.GetFiles(Configuration.ApplicationFolder, "*.sln", SearchOption.TopDirectoryOnly).Single();
353353
ProcessHelper.StartProcess(
@@ -374,7 +374,7 @@ void CheckFrontendCode()
374374
{
375375
try
376376
{
377-
AnsiConsole.MarkupLine("[green]Validating frontend code[/]");
377+
AnsiConsole.MarkupLine("[green]Validating frontend code.[/]");
378378
ProcessHelper.StartProcess("npm run format", Configuration.ApplicationFolder, throwOnError: true);
379379
ProcessHelper.StartProcess("npm run lint", Configuration.ApplicationFolder, throwOnError: true);
380380
ProcessHelper.StartProcess("npm run check", Configuration.ApplicationFolder, throwOnError: true);
@@ -413,7 +413,7 @@ private static void ValidateGitStatus()
413413

414414
void CreateChangeCommit(string message)
415415
{
416-
AnsiConsole.MarkupLine("[green]Adding changes[/]");
416+
AnsiConsole.MarkupLine("[green]Adding changes.[/]");
417417
ProcessHelper.StartProcess("git add .", Configuration.SourceCodeFolder);
418418
ProcessHelper.StartProcess($"git commit -m \"{message}\"", Configuration.SourceCodeFolder);
419419
}
@@ -542,7 +542,7 @@ private static bool HasOriginRemote()
542542

543543
private static void AmendCommit()
544544
{
545-
AnsiConsole.MarkupLine("[green]Amending changes[/]");
545+
AnsiConsole.MarkupLine("[green]Amending changes.[/]");
546546
ProcessHelper.StartProcess("git add .", Configuration.SourceCodeFolder);
547547
ProcessHelper.StartProcess("git commit --amend --no-edit", Configuration.SourceCodeFolder);
548548
}
@@ -591,7 +591,7 @@ private static void EnsureBranchIsUpToDate()
591591

592592
if (localCommits.Contains(latestOriginCommit)) return;
593593

594-
AnsiConsole.MarkupLine($"[red]Branch is not up to date with '{DefaultRemote}/{activeBranchName}'[/]");
594+
AnsiConsole.MarkupLine($"[red]Branch is not up to date with '{DefaultRemote}/{activeBranchName}'.[/]");
595595
Environment.Exit(0);
596596
}
597597

developer-cli/Commands/UninstallCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public class UninstallCommand : Command
99
{
1010
public UninstallCommand() : base(
1111
"uninstall",
12-
$"Will remove the {Configuration.AliasName} CLI alias."
12+
$"Will remove the {Configuration.AliasName} CLI alias"
1313
)
1414
{
1515
Handler = CommandHandler.Create(Execute);

0 commit comments

Comments
 (0)