예제 #1
0
        private static int PublishClientPackage(MarvelMicroserviceConfig config, string nugetApiKey, string awsAccessKey, string awsAccessSecret, string branch, string gitSha, string buildNumber)
        {
            var clientDir = config.ClientPackageDirectory;

            if (!Directory.Exists(clientDir))
            {
                Output.Info($"No client package directory found at {clientDir}.");
                Output.Info($"Skipping client package publish.");
                return(0);
            }
            Output.Info("Publishing Client NuGet package.");
            var exitCode = CommandUtilities.ExecuteCommand("dotnet", $"restore", workingDirectory: clientDir);

            if (exitCode != 0)
            {
                return(exitCode);
            }

            exitCode = CommandUtilities.ExecuteCommand("dotnet", $"library publish " +
                                                       $"-b {branch} " +
                                                       $"-k {nugetApiKey} " +
                                                       $"-a {awsAccessKey} " +
                                                       $"-s {awsAccessSecret} " +
                                                       $"-n {buildNumber} " +
                                                       $"--tests-optional", workingDirectory: clientDir);
            if (exitCode != 0)
            {
                return(exitCode);
            }

            return(0);
        }
예제 #2
0
        private static List <string> GetCacheVolumeArgs()
        {
            var cacheDir = GetMarvelCacheDirectory();

            Output.Info($"Using cache directory: {cacheDir}");
            return(new List <string>
            {
                $"-v {Path.Combine(cacheDir, "nuget", "packages")}:{ContainerPaths.NuGetPackagesCacheDirectory}",
                $"-v {Path.Combine(cacheDir, "yarn")}:{ContainerPaths.YarnCacheDirectory}"
            });
        }
예제 #3
0
        private void TrySetTestAuthentication()
        {
            var accessKeyId     = Environment.GetEnvironmentVariable(TestEnvironmentVariables.AwsAccessKeyId);
            var secretAccessKey = Environment.GetEnvironmentVariable(TestEnvironmentVariables.AwsSecretAccessKey);

            if (!string.IsNullOrWhiteSpace(accessKeyId) && !string.IsNullOrWhiteSpace(secretAccessKey))
            {
                Output.Info("Setting AWS credentials from environment variables");
                Security.UseAwsCredentials(accessKeyId, secretAccessKey);
            }
        }
예제 #4
0
        protected override async Task <int> Run(MarvelMicroserviceConfig config)
        {
            var launchOptionsResult = await _launchOptions.ProcessOptions();

            if (!launchOptionsResult.AreValid)
            {
                return(1);
            }

            var exitCode = await LaunchCommand.Launch(config, launchOptionsResult.Value);

            if (exitCode != 0)
            {
                return(exitCode);
            }

            var containerName = config.DevContainerName;

            var container = await DockerCommands.GetContainerByName(containerName);

            if (container?.IsRunning != true)
            {
                Output.Info($"Could not find running container {containerName}");
                return(1);
            }

            // Call basic command to see if we're executing in a TTY
            var ttyTest = await CommandUtilities.RunCommandAsync("docker", $"exec -it {container.ContainerId} /bin/bash -c echo hi", throwOnErrorExitCode : false);

            var isTty = true;

            if (ttyTest.ExitCode != 0)
            {
                var stdErr = string.Join("\n", ttyTest.StandardError);
                if (!stdErr.Contains("input device is not a TTY"))
                {
                    throw new Exception($"Unexpected exception encounterd checking for TTY StandardError={stdErr}");
                }
                isTty = false;
            }

            Output.Info($"Attaching to container {containerName}");
            // Use TTY option when available so that Ctrl+C in the terminal kills the process inside the container as well. Option cannot be used during integration test runs.
            var ttyArg = isTty ? "t" : "";

            exitCode = CommandUtilities.ExecuteCommand("docker", $"exec -i{ttyArg} {container.ContainerId} /bin/bash {ContainerPaths.DockerTaskScriptPath} watchAndRun");

            return(exitCode);
        }
        protected override async Task <int> Run(MarvelMicroserviceConfig config)
        {
            var authenticated = await Security.EnsureAuthenticatedWithEcr();

            if (authenticated)
            {
                Output.Info("Authorization was successful.");
                return(0);
            }
            else
            {
                Output.Error("Authorization failed.");
                return(1);
            }
        }
예제 #6
0
 public AuthenticatedImageUri(Lazy <string> imageUri)
 {
     _authenticatedUri = new Lazy <Task <AuthenticationResult> >(async() =>
     {
         // TODO - Only run authentication if the image is not already available locally? That would eliminate excessive auth calls,
         // but could make it harder to spot when auth gets broken.
         var isAuthenticated = await Security.EnsureAuthenticatedWithEcr();
         if (isAuthenticated && IsRemoteUri(imageUri.Value))
         {
             Output.Info($"Pulling image {imageUri.Value}...");
             await DockerCommands.PullImage(imageUri.Value);
         }
         return(isAuthenticated ?
                new AuthenticationResult(true, imageUri.Value) :
                new AuthenticationResult(false, imageUri.Value));
     });
 }
예제 #7
0
            // Ipify is a service running on the thor vpc that will return the users thor vpn ip.
            private async Task <string> FetchIpifyAddress()
            {
                try
                {
                    var result = await _httpClient.GetAsync(IpifyUrl);

                    if (result.StatusCode != HttpStatusCode.OK)
                    {
                        Output.Info($"Failed to get VPN IP address. You're likely to need this.\nIpify returned a StatusCode of {result.StatusCode}");
                        return(DefaultIpAddress);
                    }
                    return(await result.Content.ReadAsStringAsync());
                }
                catch (Exception e)
                {
                    Output.Info($"Failed to get VPN IP address. You're likely to need this.\nExceptionMessage={e.Message}");
                    return(DefaultIpAddress);
                }
            }
예제 #8
0
        protected override async Task <int> Run(MarvelMicroserviceConfig config)
        {
            var exitCode = 0;

            var containerName = config.DevContainerName;

            var container = await DockerCommands.GetContainerByName(containerName);

            if (container?.IsRunning != true)
            {
                Output.Info($"Could not find running container {containerName}");
                return(1);
            }

            Output.Info($"Attaching to container {containerName}");

            exitCode = CommandUtilities.ExecuteCommand("docker", $"exec -i {container.ContainerId} /bin/bash {ContainerPaths.DockerTaskScriptPath} watchAndDebug");

            return(exitCode);
        }
예제 #9
0
        protected override async Task <int> Run(MarvelMicroserviceConfig config)
        {
            foreach (var kvp in new Dictionary <string, CommandOption> {
                { "branch", _branchOption },
                { "git SHA", _gitShaOption },
                { "build number", _buildNumberOption },
                { "NuGet API key", _nugetApiKeyOption },
                { "AWS access key id", _awsAccessKeyOption },
                { "AWS secret key", _awsAccessSecretOption },
                { "persist static assets", _persistStaticAssetsOption },
            })
            {
                Output.Verbose($"Option {kvp.Key}: {kvp.Value.Value()}");
            }

            var paramValidationErrors = ValidateParams();

            if (paramValidationErrors.Any())
            {
                paramValidationErrors.ForEach(Output.Error);
                return(1);
            }

            var persistStaticAssets = _persistStaticAssetsOption.HasValue();

            if (!persistStaticAssets)
            {
                Output.Info($"NOTE: Static assets will not be published to a persistent CDN. They will be temporarily available (minimum of 1 day) for testing with this build. " +
                            $"If this build is for a deployable app (e.g., a CI build for either prod or thor environments, or a locally-built emergency deploy), " +
                            $"you should specify the {_persistStaticAssetsOption.LongName} option. If this is a local build for manual testing or an automated testing run, you may ignore this.");
            }

            _assetHostConfigurationAccessor.SetValue(persistStaticAssets ? S3AssetHostConfiguration.ProductionAssets : S3AssetHostConfiguration.TestAssets);

            return(await Publisher.Publish(config, _buildConfigurationBuilder, _configurationFileMerger, _configurationFileUploader, _configurationFileValidator, _staticAssetProcessor, _nugetApiKeyOption.Value(), _awsAccessKeyOption.Value(), _awsAccessSecretOption.Value(), _branchOption.Value(), _gitShaOption.Value(), _buildNumberOption.Value(), _mergeAndUploadServiceConfig.HasValue(), _mergeServiceConfig.HasValue()));
        }
예제 #10
0
        public void Register(CommandLineApplication app, string currentDirectory, CommandOption verboseOption)
        {
            app.Command(CommandName, command =>
            {
                command.Description = Description;

                AddHelpOption(command);
                CreateOptions(command);
                var commandVerboseOption = AddVerboseOption(command);

                command.OnExecute(() =>
                {
                    // Allow `-v` to be added before or after the subcommand, i.e., `dotnet marvel -v build` or `dotnet marvel build -v`
                    Output.UseVerbose = verboseOption.HasValue() || commandVerboseOption.HasValue();

                    var config = new MarvelMicroserviceConfig(currentDirectory);
                    Output.Info($"Running `dotnet {Program.CliCommandName} {CommandName}`...");
                    Output.Verbose("Using verbose logging");
                    Output.Info($"Using solution at {config.BaseDirectory}");

                    TrySetTestAuthentication();
                    var task = Task.Run(async() => await Run(config));
                    var code = task.GetAwaiter().GetResult();

                    if (code == 0)
                    {
                        Output.Success("Success.");
                    }
                    else
                    {
                        Output.Error("Failed.");
                    }
                    return(code);
                });
            });
        }
예제 #11
0
        public static async Task <GetOrCreateContainerResult> GetOrCreateContainerWithName(string containerName, string imageName, List <string> options, string commandAndArgs)
        {
            const string argsHashLabelName = "createArgumentsHash";

            var createArguments = options
                                  .Append($"--name {containerName}")
                                  .Append(imageName)
                                  .Append(commandAndArgs)
                                  .ToList();

            // Hash label argument is not part of the hash itself
            var argsHash   = Hash(string.Join(" ", createArguments));
            var createArgs = string.Join(" ", createArguments.Prepend($"-l {argsHashLabelName}={argsHash}"));


            // var createArguments = $"--name {containerName} -p 5000:5000 -e \"{ProjectNameEnvVariable}={config.ProjectName}\" -v {config.BaseDirectory}:{ContainerPaths.MountedSourceDirectory} {buildImage} /bin/bash {dockerTaskScriptPath} hang";
            // var createArguments = $"--name {containerName} -p 5000:5000 -e \"{ProjectNameEnvVariable}={config.ProjectName}\" -l creationArgsSignature={argsHashPlaceholder} -l another=hey -v {config.BaseDirectory}:{ContainerPaths.MountedSourceDirectory} {buildImage} /bin/bash {dockerTaskScriptPath} hang";

            // # Ensure build image has been built locally (temporary until image is published)
            // buildImageCount=$(docker images | grep "$buildImage" -c)
            // if [[ "$buildImageCount" -eq "0" ]]; then
            //     echo "Building base image "
            //     cd ../marvel-build-container/dev/
            //     ./build.sh
            //     cd "$dir"
            // fi

            var container = await GetContainerByName(containerName);

            var isNewContainer = false;

            // This is (primarily?) for development of the tools package. Prevents needing to manually remove the containers after building a new base image or change in arguments.
            if (container != null)
            {
                if (await IsContainerUpToDateWithImage(container, imageName))
                {
                    string containerHash;
                    if (container.Labels.TryGetValue(argsHashLabelName, out containerHash) && containerHash == argsHash)
                    {
                        Output.Info($"Container is up to date with latest base image and arguments");
                    }
                    else
                    {
                        Output.Info($"Removing container created with outdated arguments");
                        await RemoveContainer(container);

                        container = null;
                    }
                }
                else
                {
                    Output.Info($"Removing container created from outdated image.");
                    await RemoveContainer(container);

                    container = null;
                }
            }

            if (container == null)
            {
                Output.Info($"Creating new container {containerName}");

                container = await CreateContainer(createArgs);

                isNewContainer = true;
            }

            return(new GetOrCreateContainerResult
            {
                Container = container,
                IsNewContainer = isNewContainer,
            });
        }
예제 #12
0
        private static async Task <int> EnsureRegistratorContainerIsRunning(string eurekaServerUrl, string ipAddress)
        {
            var registratorImageUri = await EcrResources.RegistratorLatestImageUrl.EnsureImageIsPulled();

            if (!registratorImageUri.WasSuccessful)
            {
                return(1);
            }

            var registratorContainerName = "registrator-marvel";

            // Build arguments list for `docker create` call so we can hash them and ensure an existing container is compatible. Argument pairs are combined into one argument for readability.
            var dockerCreateArgs = new List <string>
            {
                "--net=host",
                "--volume=/var/run/docker.sock:/tmp/docker.sock"
            };


            var createContainerResult = await DockerCommands.GetOrCreateContainerWithName(
                registratorContainerName,
                registratorImageUri.Value,
                dockerCreateArgs,
                $"-ttl 30 -ttl-refresh 15 -ip {ipAddress} -require-label {FixEurekaServerUrlScheme(eurekaServerUrl ?? DefaultEurekaServerUrl)}");

            var container      = createContainerResult.Container;
            var isNewContainer = createContainerResult.IsNewContainer;

            if (!container.IsRunning)
            {
                Output.Info($"Starting container {registratorContainerName}");
                var result = await CommandUtilities.RunCommandAsync("docker", $"start {container.ContainerId}", throwOnErrorExitCode : false);

                if (result.ExitCode != 0)
                {
                    var stdErr = string.Join("\n", result.StandardError);
                    // Message is `Bind for 0.0.0.0:5000 failed: port is already allocated`. trimming the port portion in case the port changes.
                    // if (stdErr.Contains("failed: port is already allocated"))
                    // {
                    //     Output.Info("Webapp port is already in use. Attempting to stop other container using port.");
                    //     // Find other containers using a partial match on suffix. This corresponds to the naming scheme defined in MarvelMicroserviceConfig.
                    //     var containers = await DockerCommands.GetContainersByName("marvel-dev");
                    //     var otherContainer = containers.FirstOrDefault(c => c.IsRunning);
                    //     if (otherContainer == null)
                    //     {
                    //         Output.Error("Unable to find running container using same port.");
                    //         Output.Error($"Failed to start container {containerName}. StandardError={stdErr}");
                    //         return 1;
                    //     }
                    //     Output.Info($"Stopping container {otherContainer.ContainerId}");
                    //     await DockerCommands.StopContainer(otherContainer);

                    //     Output.Info($"Starting container {containerName} again");
                    //     var restartResult = await ProcessEx.RunAsync("docker", $"start {container.ContainerId}");
                    //     if (restartResult.ExitCode != 0)
                    //     {
                    //         Output.Error($"Failed to restart container {containerName}. StandardError={stdErr}");
                    //         return result.ExitCode;
                    //     }
                    // }
                    // else
                    // {
                    Output.Error($"Failed to start container {registratorContainerName}. StandardError={stdErr}");
                    return(result.ExitCode);
                    // }
                }

                // Ensure the container doesn't immediately exit.
                // TODO Bumped this up for registrator specifically to ensure eureka host is valid. Might want to verify by scanning logs
                // that this did in fact start up properly.
                Thread.Sleep(500);

                var runningContainer = await DockerCommands.GetContainerByName(registratorContainerName);

                if (!runningContainer.IsRunning)
                {
                    Output.Error($"Container {registratorContainerName} stopped unexpectedly. Check container logs by running {DockerCommands.GetLogsForContainerCommand(runningContainer)}.");
                    return(1);
                }
            }

            // TODO - ensure registrator is ready? pause?

            Output.Info($"Container {registratorContainerName} is running.");

            return(0);
        }
예제 #13
0
        public static async Task <int> Launch(MarvelMicroserviceConfig config, LaunchOptions launchOptions)
        {
            var registratorRunResultTask = EnsureRegistratorContainerIsRunning(launchOptions.EurekaServer, launchOptions.LocalIpAddress);
            var buildImageUriTask        = EcrResources.DotnetMicroserviceBuildImageUrl.EnsureImageIsPulled();

            var registratorRunResult = await registratorRunResultTask;

            if (registratorRunResult != 0)
            {
                return(registratorRunResult);
            }

            var buildImageUri = await buildImageUriTask;

            if (!buildImageUri.WasSuccessful)
            {
                return(1);
            }

            // # TODO include a hash of the tools version in the container name to ensure containers are recreated after tools update(?)
            var containerName        = config.DevContainerName;
            var dockerTaskScriptPath = ContainerPaths.DockerTaskScriptPath;

            // Build arguments list for `docker create` call so we can hash them and ensure an existing container is compatible. Argument pairs are combined into one argument for readability.
            var hostname = launchOptions.Host ?? await NetworkUtility.GetFriendlyHostName();

            var labelArgs        = LabelUtilities.FormatLabelsAsArguments(await new LabelBuilder(config).GetLabelsForLocalDev(hostname));
            var dockerCreateArgs = labelArgs.Concat(new List <string>
            {
                "-p 5000:5000",
                "--dns-search=agilesports.local",
                $"-e \"{ProjectNameEnvVariable}={config.ProjectName}\"",
                $"-e \"{SolutionNameEnvVariable}={config.SolutionName}\"",
                $"-e \"{WebappDirEnvVariable}={ContainerPaths.GetWebappDirectory(config)}\"",
                $"-e \"{BrowserAppDirEnvVariable}={ContainerPaths.GetBrowserAppDirectory(config)}\"",
                // This could probably live in the image
                $"-e ASPNETCORE_ENVIRONMENT=Development",
                $"-v {config.BaseDirectory}:{ContainerPaths.MountedSourceDirectory}",
            });

            if (launchOptions.UseSharedCache)
            {
                dockerCreateArgs = dockerCreateArgs.Concat(GetCacheVolumeArgs());
            }

            var createContainerResult = await DockerCommands.GetOrCreateContainerWithName(
                containerName,
                buildImageUri.Value,
                dockerCreateArgs.ToList(),
                $"/bin/bash {dockerTaskScriptPath} hang");

            var container      = createContainerResult.Container;
            var isNewContainer = createContainerResult.IsNewContainer;

            if (!container.IsRunning)
            {
                Output.Info($"Starting container {containerName}");
                var result = await CommandUtilities.RunCommandAsync("docker", $"start {container.ContainerId}", throwOnErrorExitCode : false);

                // Output.Info("StdErr=" + string.Join("\n", result.StandardError));
                // Output.Info("StdOut=" + string.Join("\n", result.StandardOutput));
                if (result.ExitCode != 0)
                {
                    var stdErr = string.Join("\n", result.StandardError);
                    // Message is `Bind for 0.0.0.0:5000 failed: port is already allocated`. trimming the port portion in case the port changes.
                    if (stdErr.Contains("failed: port is already allocated"))
                    {
                        Output.Info("Webapp port is already in use. Attempting to stop other container using port.");
                        // Find other containers using a partial match on suffix. This corresponds to the naming scheme defined in MarvelMicroserviceConfig.
                        var containers = await DockerCommands.GetContainersByName("marvel-dev");

                        var otherContainer = containers.FirstOrDefault(c => c.IsRunning);
                        if (otherContainer == null)
                        {
                            Output.Error("Unable to find running container using same port.");
                            Output.Error($"Failed to start container {containerName}. StandardError={stdErr}");
                            return(1);
                        }
                        Output.Info($"Stopping container {otherContainer.ContainerId}");
                        await DockerCommands.StopContainer(otherContainer);

                        Output.Info($"Starting container {containerName} again");
                        var restartResult = await ProcessEx.RunAsync("docker", $"start {container.ContainerId}");

                        if (restartResult.ExitCode != 0)
                        {
                            Output.Error($"Failed to restart container {containerName}. StandardError={stdErr}");
                            return(result.ExitCode);
                        }
                    }
                    else
                    {
                        Output.Error($"Failed to start container {containerName}. StandardError={stdErr}");
                        return(result.ExitCode);
                    }
                }

                // TODO only perform this check after failed `docker exec` commands, for better reporting?
                // Ensure the container doesn't immediately exit.
                Thread.Sleep(10);

                var runningContainer = await DockerCommands.GetContainerByName(containerName);

                if (!runningContainer.IsRunning)
                {
                    Output.Error($"Container {containerName} stopped unexpectedly. Check container logs.");
                    return(1);
                }
            }

            if (isNewContainer)
            {
                Output.Info($"Attaching to container {containerName} to run first time launch command on new container");
                var code = CommandUtilities.ExecuteCommand("docker", $"exec -i {container.ContainerId} /bin/bash {dockerTaskScriptPath} firstTimeLaunch");
                if (code != 0)
                {
                    Output.Info($"First time startup command failed. Removing container.");
                    await DockerCommands.RemoveContainer(container);

                    return(code);
                }
            }
            else
            {
                Output.Info($"Attaching to container {containerName} to run relaunch command on existing container");
                var code = CommandUtilities.ExecuteCommand("docker", $"exec -i {container.ContainerId} /bin/bash {dockerTaskScriptPath} relaunch");
                if (code != 0)
                {
                    return(code);
                }
            }

            Output.Info($"Container {containerName} launched and ready to run");
            Output.Info($"Using hostname: {hostname}");
            Output.Info($"If debugging from VS Code, switch to the Debug Console (Cmd+Shift+Y / Ctrl+Shift+Y) for app and watch process logs");

            return(0);
        }
예제 #14
0
        public static async Task <int> Build(MarvelMicroserviceConfig config, string publishedRuntimeImageName, BuildConfig buildConfig, IStaticAssetProcessor staticAssetProcessor)
        {
            var buildImageUri = await EcrResources.DotnetMicroserviceBuildImageUrl.EnsureImageIsPulled();

            if (!buildImageUri.WasSuccessful)
            {
                return(1);
            }

            var taskTimer = Stopwatch.StartNew();

            // Clear the old temp dir to ensure a fresh publish if running locally
            var outputPath = config.DotnetPublishOutputPath;

            if (Directory.Exists(outputPath))
            {
                Directory.Delete(outputPath, true);
            }
            Directory.CreateDirectory(outputPath);

            var dockerRunOptions = new List <string>
            {
                $"--rm",
                $"-e \"{LaunchCommand.ProjectNameEnvVariable}={config.ProjectName}\"",
                $"-e \"{LaunchCommand.SolutionNameEnvVariable}={config.SolutionName}\"",
                $"-e \"{WebappDirEnvVariable}={ContainerPaths.GetWebappDirectory(config)}\"",
                $"-e \"{BrowserAppDirEnvVariable}={ContainerPaths.GetBrowserAppDirectory(config)}\"",
                $"-v {config.BaseDirectory}:{ContainerPaths.MountedSourceDirectory}",
                $"-v {outputPath}:{ContainerPaths.BuildOutputDirectory}",
            };

            var runtimeImageLabelsTask = new LabelBuilder(config).GetLabels(buildConfig);

            var exitCode = 0;

            var dotnetBuildTimer = Stopwatch.StartNew();

            Output.Info($"Building dotnet webapp.");

            // Run container to build app and copy published resources to mounted output directory
            exitCode = CommandUtilities.ExecuteCommand("docker", $"run {string.Join(" ", dockerRunOptions)} {buildImageUri.Value} /bin/bash {ContainerPaths.DockerTaskScriptPath} buildWithoutCompose");
            if (exitCode != 0)
            {
                return(exitCode);
            }
            Output.Info($"dotnet webapp build completed {dotnetBuildTimer.Elapsed}");

            // Run static asset build from output directory
            await staticAssetProcessor.ProcessStaticAssets(Path.Combine(config.DotnetPublishOutputPath, "wwwroot"));

            // Build the image from the source output
            var dockerBuildTimer = Stopwatch.StartNew();

            Output.Info($"Building docker image {publishedRuntimeImageName}.");

            var labelArgs = LabelUtilities.FormatLabelsAsArguments(await runtimeImageLabelsTask);

            var buildArgs = labelArgs.Concat(new List <string> {
                $"-t {publishedRuntimeImageName}",
                $"--build-arg webappAssemblyPath={config.PublishedWebappAssemblyPath}",
                config.DotnetPublishOutputPath,
            });

            exitCode = CommandUtilities.ExecuteCommand("docker", $"build {string.Join(" ", buildArgs)}");
            if (exitCode != 0)
            {
                return(exitCode);
            }
            Output.Info($"Docker build completed {dockerBuildTimer.Elapsed}");

            Output.Info($"Build time elapsed {taskTimer.Elapsed}");

            return(exitCode);
        }
예제 #15
0
        internal static async Task <int> Publish(MarvelMicroserviceConfig config, IBuildConfigurationBuilder configBuilder, IConfigurationFileMerger configMerger, IConfigurationFileUploader configUploader, IConfigurationFileValidator configValidator, IStaticAssetProcessor staticAssetProcessor, string nugetApiKey, string awsAccessKey, string awsAccessSecret, string branch, string gitSha, string buildNumber, bool mergeAndUploadServiceConfig, bool mergeServiceConfig)
        {
            Security.UseAwsCredentials(awsAccessKey, awsAccessSecret);

            var publishImage = ImageNameBuilder.CreateImageNameAndTag(
                config.ServiceName,
                branch,
                gitSha,
                DateTime.UtcNow,
                buildNumber);

            string[] serviceConfigFiles = null;
            if (mergeAndUploadServiceConfig || mergeServiceConfig)
            {
                GenerateBuildFile(configBuilder, config.BuildConfigFilePath, gitSha, branch, publishImage.FullPath, buildNumber);
                serviceConfigFiles = await MergeAllServiceConfigFiles(configMerger, config.SourceDirectory, config.ServiceConfigFileName, config.BuildConfigFilePath);

                var configIsValid = await ValidateAllServiceConfigFiles(configValidator, config.SourceDirectory, serviceConfigFiles);

                if (!configIsValid)
                {
                    Output.Error("Invalid service configuration.");
                    return(1);
                }
            }

            var exitCode = await BuildCommand.Build(config, publishImage.FullPath, new BuildConfig
            {
                BranchName  = branch,
                BuildNumber = buildNumber,
            }, staticAssetProcessor);

            if (exitCode != 0)
            {
                return(exitCode);
            }

            try
            {
                exitCode = PublishClientPackage(config, nugetApiKey, awsAccessKey, awsAccessSecret, branch, gitSha, buildNumber);
                if (exitCode != 0)
                {
                    return(exitCode);
                }

                // Publish to ECR
                Output.Info($"Publishing {publishImage.FullPath}");
                await Security.EnsureAuthenticatedWithEcr();

                exitCode = CommandUtilities.ExecuteCommand("docker", $"push {publishImage.FullPath}");
                if (exitCode != 0)
                {
                    return(exitCode);
                }
            }
            finally
            {
                // TODO always remove image, even on publish failure
                await CommandUtilities.RunCommandAsync("docker", $"rmi {publishImage.FullPath}", errorMessage : $"Failed to remove image {publishImage.FullPath}.");

                Output.Info($"Removed local image {publishImage.FullPath}");
            }

            try
            {
                if (mergeAndUploadServiceConfig && serviceConfigFiles != null)
                {
                    await UploadAllServiceConfigFiles(configUploader, config.SourceDirectory, serviceConfigFiles, publishImage.Tag);
                }
            }
            catch (Exception ex)
            {
                Output.Error($"Unable to upload service configuration files. Error: {ex.Message}");
                return(1);
            }

            File.WriteAllText(Path.Combine(config.WebappDirectory, "PublishedImageUrl.txt"), publishImage.FullPath);
            Output.Info("Publish successful");
            return(0);
        }