├── version.txt ├── .octopus ├── schema_version.ocl ├── deployment_settings.ocl └── channels.ocl ├── BuildAssets ├── octo.cmd ├── icon.png ├── OctoWrapper.sh ├── init.ps1 ├── VERIFICATION.txt ├── LICENSE.txt ├── octo ├── OctopusTools.nuspec ├── repos │ ├── README.md │ ├── test-linux-package-from-feed-in-dists.sh │ └── test-linux-package-from-feed.sh ├── test-linux-package.sh └── create-octopuscli-linux-packages.sh ├── .github ├── CODEOWNERS ├── workflows │ ├── release.yml │ └── codeql-analysis.yml ├── ISSUE_TEMPLATE.md └── CONTRIBUTING.md ├── source ├── Octo │ ├── runtimeconfig.template.json │ ├── Properties │ │ ├── Icon.ico │ │ └── AssemblyInfo.cs │ ├── init.ps1 │ ├── Program.cs │ └── Octo.csproj ├── Octopus.Cli │ ├── icon.png │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Model │ │ ├── DeploymentRelatedResources.cs │ │ ├── ProjectExport.cs │ │ └── ChannelVersionRuleTestResult.cs │ ├── Util │ │ ├── LazyExtensions.cs │ │ ├── CommandOutputJsonSerializer.cs │ │ ├── AssemblyExtensions.cs │ │ ├── NumericExtensions.cs │ │ ├── UriExtensions.cs │ │ ├── StringExtensions.cs │ │ ├── FeedCustomExpressionHelper.cs │ │ ├── ReplaceStatus.cs │ │ ├── ListExtensions.cs │ │ ├── LineSplitter.cs │ │ ├── DeletionOptions.cs │ │ ├── IOctopusFileSystem.cs │ │ ├── FeatureDetectionExtensions.cs │ │ ├── Humanize.cs │ │ ├── ResourceCollectionExtensions.cs │ │ ├── SerilogLogProvider.cs │ │ └── CommandSuggester.cs │ ├── Diagnostics │ │ ├── LoggingModule.cs │ │ └── LogUtilities.cs │ ├── Commands │ │ ├── Releases │ │ │ ├── IChannelVersionRuleTester.cs │ │ │ ├── IReleasePlanBuilder.cs │ │ │ ├── ChannelVersionRuleTester.cs │ │ │ ├── ReleasePlanItem.cs │ │ │ ├── IPackageVersionResolver.cs │ │ │ ├── AllowReleaseProgressionCommand.cs │ │ │ ├── ListReleasesCommand.cs │ │ │ └── PreventReleaseProgressionCommand.cs │ │ ├── ExportCommand.cs │ │ ├── Package │ │ │ ├── IPackageBuilder.cs │ │ │ ├── NuGetPackageBuilder.cs │ │ │ ├── DeletePackageCommand.cs │ │ │ ├── BuildInformationCommand.cs │ │ │ ├── PushMetadataCommand.cs │ │ │ └── PushCommand.cs │ │ ├── VersionCommand.cs │ │ ├── ImportCommand.cs │ │ ├── WorkerPool │ │ │ ├── ListWorkerPoolsCommand.cs │ │ │ └── CreateWorkerPoolCommand.cs │ │ ├── Project │ │ │ ├── ListProjectsCommand.cs │ │ │ ├── DeleteProjectCommand.cs │ │ │ ├── DisableProjectCommand.cs │ │ │ └── CreateProjectCommand.cs │ │ ├── Environment │ │ │ ├── ListEnvironmentsCommand.cs │ │ │ └── CreateEnvironmentCommand.cs │ │ ├── Tenant │ │ │ └── ListTenantsCommand.cs │ │ └── HealthStatusProvider.cs │ ├── Repositories │ │ ├── IActionTemplateRepository.cs │ │ ├── OctopusRepositoryFactory.cs │ │ └── ActionTemplateRepository.cs │ ├── Extensions │ │ ├── AttributeExtensions.cs │ │ └── TimeSpanExtensions.cs │ ├── Infrastructure │ │ ├── AutofacExtensions.cs │ │ └── CouldNotFindException.cs │ └── Octopus.Cli.csproj ├── Octopus.DotNet.Cli │ ├── icon.png │ ├── Program.cs │ └── Octopus.DotNet.Cli.csproj ├── Octo.Tests │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Commands │ │ ├── Resources │ │ │ └── CreateRelease.config.txt │ │ ├── ListReleasesCommandFixture.ShouldGetListOfReleases.approved.txt │ │ ├── ListReleasesCommandFixture.JsonFormat_ShouldBeWellFormed.approved.txt │ │ ├── VersionTestFixture.cs │ │ ├── ListWorkerPoolsCommandFixture.cs │ │ ├── ListEnvironmentsCommandFixture.cs │ │ ├── DummyApiCommand.cs │ │ ├── ListProjectsCommandFixture.cs │ │ ├── CreateWorkerPoolCommandFixture.cs │ │ ├── ListTenantsFixtures.cs │ │ ├── CreateEnvironmentCommandFixture.cs │ │ ├── DeletePackageCommandFixture.cs │ │ ├── ChannelVersionRuleTesterFixture.cs │ │ ├── CommandConventionFixture.cs │ │ ├── SupportFormattedOutputFixture.cs │ │ ├── ListLatestDeploymentsCommandFixture.cs │ │ └── ListDeploymentsCommandFixture.cs │ ├── Helpers │ │ ├── TestCommandExtensions.cs │ │ ├── ConsoleWriter.cs │ │ └── ApprovalScrubberExtensions.cs │ ├── Util │ │ ├── UriExtensionsFixture.cs │ │ └── CommandSuggesterFixture.cs │ ├── Extensions │ │ ├── TimeSpanExtensionsFixture.cs │ │ └── StringExtensionsFixture.cs │ ├── Diagnostics │ │ └── LogUtilitiesFixture.cs │ └── Octo.Tests.csproj └── NuGet.Config ├── .nuke └── parameters.json ├── certificates ├── signtool.exe └── OctopusDevelopment.pfx ├── global.json ├── tools └── packages.config ├── NuGet.Config ├── LICENSE.txt ├── Dockerfiles ├── alpine │ └── Dockerfile └── Readme.md ├── .gitattributes ├── .gitignore ├── readme.md └── CHANGELOG.md /version.txt: -------------------------------------------------------------------------------- 1 | 9.1.7 2 | -------------------------------------------------------------------------------- /.octopus/schema_version.ocl: -------------------------------------------------------------------------------- 1 | version = 6 -------------------------------------------------------------------------------- /BuildAssets/octo.cmd: -------------------------------------------------------------------------------- 1 | dotnet "%~dp0/octo.dll" %* -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @OctopusDeploy/team-integrations-fnm -------------------------------------------------------------------------------- /source/Octo/runtimeconfig.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "rollForward": "Major" 3 | } -------------------------------------------------------------------------------- /BuildAssets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OctopusDeploy/OctopusCLI/HEAD/BuildAssets/icon.png -------------------------------------------------------------------------------- /.nuke/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./build.schema.json", 3 | "Solution": "source/OctopusCli.sln" 4 | } -------------------------------------------------------------------------------- /certificates/signtool.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OctopusDeploy/OctopusCLI/HEAD/certificates/signtool.exe -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "6.0.101", 4 | "rollForward": "latestFeature" 5 | } 6 | } -------------------------------------------------------------------------------- /source/Octopus.Cli/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OctopusDeploy/OctopusCLI/HEAD/source/Octopus.Cli/icon.png -------------------------------------------------------------------------------- /source/Octo/Properties/Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OctopusDeploy/OctopusCLI/HEAD/source/Octo/Properties/Icon.ico -------------------------------------------------------------------------------- /source/Octopus.DotNet.Cli/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OctopusDeploy/OctopusCLI/HEAD/source/Octopus.DotNet.Cli/icon.png -------------------------------------------------------------------------------- /certificates/OctopusDevelopment.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OctopusDeploy/OctopusCLI/HEAD/certificates/OctopusDevelopment.pfx -------------------------------------------------------------------------------- /tools/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /BuildAssets/OctoWrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo 'Deprecated: The Octo command has been renamed to octo.' >&2 3 | "$(dirname "$0")/octo" "$@" 4 | -------------------------------------------------------------------------------- /source/Octo.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | [assembly: AssemblyTitle("OctopusTools.Tests")] 5 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | [assembly: InternalsVisibleTo("Octo.Tests")] 5 | -------------------------------------------------------------------------------- /BuildAssets/init.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | $path = $env:PATH 4 | if (!$path.Contains($toolsPath)) { 5 | $env:PATH += ";$toolsPath" 6 | } -------------------------------------------------------------------------------- /source/Octo/init.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | $path = $env:PATH 4 | if (!$path.Contains($toolsPath)) { 5 | $env:PATH += ";$toolsPath" 6 | } -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/Resources/CreateRelease.config.txt: -------------------------------------------------------------------------------- 1 | server=https://test-server-url/api/ 2 | apikey=API-test 3 | project=Test Project 4 | releaseNumber=1.0.0 5 | releasenotes=Test config file. -------------------------------------------------------------------------------- /BuildAssets/VERIFICATION.txt: -------------------------------------------------------------------------------- 1 | VERIFICATION 2 | Verification is intended to assist the Chocolatey moderators and community 3 | in verifying that this package's contents are trustworthy. 4 | 5 | This package is published by the Octopus Deploy team itself. -------------------------------------------------------------------------------- /source/Octo/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Octopus.Cli; 3 | 4 | namespace Octo 5 | { 6 | public class Program 7 | { 8 | public static int Main(string[] args) 9 | { 10 | return new CliProgram().Execute(args); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /source/Octopus.DotNet.Cli/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Octopus.Cli; 3 | 4 | namespace Octopus.DotNet.Cli 5 | { 6 | public static class Program 7 | { 8 | public static int Main(string[] args) 9 | { 10 | return new CliProgram().Execute(args); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Model/DeploymentRelatedResources.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Octopus.Client.Model; 3 | 4 | namespace Octopus.Cli.Model 5 | { 6 | public class DeploymentRelatedResources 7 | { 8 | public ChannelResource ChannelResource { get; set; } 9 | public ReleaseResource ReleaseResource { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: release-please 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: google-github-actions/release-please-action@v3 11 | with: 12 | release-type: simple 13 | token: ${{ secrets.INTEGRATIONS_FNM_BOT_TOKEN }} 14 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/LazyExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // ReSharper disable CheckNamespace 4 | namespace Octopus.Cli.Util 5 | { 6 | public static class LazyExtensions 7 | // ReSharper restore CheckNamespace 8 | { 9 | public static T LoadValue(this Lazy lazy) 10 | { 11 | return lazy.Value; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Diagnostics/LoggingModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Autofac; 3 | using Serilog; 4 | 5 | namespace Octopus.Cli.Diagnostics 6 | { 7 | public class LoggingModule : Module 8 | { 9 | protected override void Load(ContainerBuilder builder) 10 | { 11 | builder.RegisterInstance(Log.Logger).As().SingleInstance(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.octopus/deployment_settings.ocl: -------------------------------------------------------------------------------- 1 | deployment_changes_template = <<-EOT 2 | #{each release in Octopus.Deployment.Changes} 3 | #{release.ReleaseNotes} 4 | #{/each} 5 | EOT 6 | 7 | connectivity_policy { 8 | } 9 | 10 | versioning_strategy { 11 | 12 | donor_package { 13 | package = "PackageToUpload" 14 | step = "upload-octopustools-to-s3-public-with-hashes" 15 | } 16 | } -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/CommandOutputJsonSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Octopus.Client.Serialization; 3 | using Octopus.CommandLine; 4 | 5 | namespace Octopus.Cli.Util 6 | { 7 | public class CommandOutputJsonSerializer : ICommandOutputJsonSerializer 8 | { 9 | public string SerializeObjectToJson(object o) 10 | { 11 | return JsonSerialization.SerializeObject(o); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /source/Octo.Tests/Helpers/TestCommandExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Octopus.CommandLine.Commands; 3 | 4 | // ReSharper disable CheckNamespace 5 | namespace Octopus.Cli.Tests.Helpers 6 | { 7 | public static class TestCommandExtensions 8 | // ReSharper restore CheckNamespace 9 | { 10 | public static void Execute(this ICommand command, params string[] args) 11 | { 12 | command.Execute(args).GetAwaiter().GetResult(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Releases/IChannelVersionRuleTester.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Octopus.Cli.Model; 4 | using Octopus.Client; 5 | using Octopus.Client.Model; 6 | 7 | namespace Octopus.Cli.Commands.Releases 8 | { 9 | public interface IChannelVersionRuleTester 10 | { 11 | Task Test(IOctopusAsyncRepository repository, ChannelVersionRuleResource rule, string packageVersion, string feedId); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /source/Octo/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | [assembly: AssemblyDescription("Tools for Octopus, an opinionated deployment solution for .NET applications")] 5 | [assembly: AssemblyCompany("Octopus Deploy")] 6 | [assembly: AssemblyProduct("Octopus, an opinionated deployment solution for .NET applications")] 7 | [assembly: AssemblyCopyright("Copyright © Octopus Deploy 2011")] 8 | [assembly: AssemblyCulture("")] 9 | [assembly: AssemblyTitle("OctopusTools")] 10 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Releases/IReleasePlanBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Octopus.Client; 4 | using Octopus.Client.Model; 5 | 6 | namespace Octopus.Cli.Commands.Releases 7 | { 8 | public interface IReleasePlanBuilder 9 | { 10 | Task Build(IOctopusAsyncRepository repository, 11 | ProjectResource project, 12 | ChannelResource channel, 13 | string versionPreReleaseTag, 14 | string gitReference, 15 | string gitCommit); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Repositories/IActionTemplateRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Octopus.Client.Model; 4 | 5 | namespace Octopus.Cli.Repositories 6 | { 7 | public interface IActionTemplateRepository 8 | { 9 | Task Get(string idOrHref); 10 | Task Create(ActionTemplateResource resource); 11 | Task Modify(ActionTemplateResource resource); 12 | 13 | Task FindByName(string name); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/AssemblyExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace Octopus.Cli.Util 5 | { 6 | public static class AssemblyExtensions 7 | { 8 | public static string GetInformationalVersion(this Type type) 9 | { 10 | return type.GetTypeInfo().Assembly.GetCustomAttribute().InformationalVersion; 11 | } 12 | 13 | public static string GetExecutableName() 14 | { 15 | return Assembly.GetEntryAssembly()?.GetName().Name ?? "octo"; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Octopus Deploy and contributors. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Repositories/OctopusRepositoryFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Octopus.Client; 3 | 4 | namespace Octopus.Cli.Repositories 5 | { 6 | public interface IOctopusAsyncRepositoryFactory 7 | { 8 | IOctopusAsyncRepository CreateRepository(IOctopusAsyncClient client, RepositoryScope scope = null); 9 | } 10 | 11 | public class OctopusRepositoryFactory : IOctopusAsyncRepositoryFactory 12 | { 13 | public IOctopusAsyncRepository CreateRepository(IOctopusAsyncClient client, RepositoryScope scope = null) 14 | { 15 | return client.CreateRepository(scope); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.octopus/channels.ocl: -------------------------------------------------------------------------------- 1 | channel "Default" { 2 | is_default = true 3 | 4 | rules { 5 | tag = "^$" 6 | 7 | deployment_action_packages { 8 | step = "Push Octopus.Cli to NuGet Gallery" 9 | package = "Octopus.Cli" 10 | } 11 | } 12 | } 13 | 14 | channel "Pre-Release" { 15 | description = "" 16 | lifecycle = "Components Internal only" 17 | 18 | rules { 19 | tag = ".+" 20 | version_range = "" 21 | 22 | deployment_action_packages { 23 | step = "Push Octopus.Cli to NuGet Gallery" 24 | package = "Octopus.Cli" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Extensions/AttributeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | 5 | namespace Octopus.Cli.Extensions 6 | { 7 | public static class AttributeExtensions 8 | { 9 | public static TValue GetAttributeValue(this Type type, Func valueSelector) where TAttribute : Attribute 10 | { 11 | var att = type.GetTypeInfo() 12 | .GetCustomAttributes(typeof(TAttribute), true) 13 | .FirstOrDefault() as TAttribute; 14 | if (att != null) 15 | return valueSelector(att); 16 | return default; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BuildAssets/LICENSE.txt: -------------------------------------------------------------------------------- 1 | From: https://github.com/OctopusDeploy/OctopusCLI/blob/master/LICENSE.txt 2 | 3 | LICENSE 4 | 5 | Copyright (c) Octopus Deploy and contributors. All rights reserved. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 8 | these files except in compliance with the License. You may obtain a copy of the 9 | License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software distributed 14 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 15 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations under the License. 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thank you for taking the time to log an issue. While we do monitor issues posted to this repository, we do not do so every day, so it may be some time before we respond. 2 | 3 | If you've found a bug or something isn't working, please [contact support](http://octopusdeploy.com/support), who can help you find a workaround or make sure the problem gets prioritized properly. 4 | 5 | If you are planning on sending a pull request, please see our [Contribution](Contributing.md) guide, and if appropriate clear this text out and submit your issue. 6 | 7 | If you have an idea or a feature request, and it is directly related to this product in this repository, clear out this text and create an issue. Otherwise, please post it to [our UserVoice site](http://octopusdeploy.uservoice.com) so others can vote for it. 8 | -------------------------------------------------------------------------------- /BuildAssets/octo: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SOURCE="${BASH_SOURCE[0]}" 3 | while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink 4 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 5 | SOURCE="$(readlink "$SOURCE")" 6 | [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located 7 | done 8 | OCTO_PATH="$( dirname "$SOURCE")/octo.dll" 9 | if [ "$(basename "$0")" == "Octo" ]; then 10 | echo 'Deprecated: The Octo command has been renamed to octo..' >&2 11 | fi 12 | # LTTNG_UST_REGISTER_TIMEOUT=0 is there to work around a bug in docker that causes an assertion violation in dotnet on first launch 13 | # See https://github.com/dotnet/cli/issues/1582 14 | LTTNG_UST_REGISTER_TIMEOUT=0 dotnet "$OCTO_PATH" "$@" 15 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Infrastructure/AutofacExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Autofac; 3 | using Autofac.Builder; 4 | using Octopus.CommandLine; 5 | using Octopus.CommandLine.Commands; 6 | 7 | namespace Octopus.Cli.Infrastructure 8 | { 9 | public static class AutofacExtensions 10 | { 11 | public static IRegistrationBuilder RegisterCommand(this ContainerBuilder builder, string name, string description, params string[] aliases) 12 | where TCommand : ICommand 13 | { 14 | return builder.RegisterType() 15 | .As() 16 | .WithMetadata(m => m.For(x => x.Name, name).For(x => x.Aliases, aliases).For(x => x.Description, description)); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /source/Octo.Tests/Util/UriExtensionsFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using Octopus.Cli.Util; 4 | 5 | namespace Octo.Tests.Util 6 | { 7 | [TestFixture] 8 | public class UriExtensionsFixture 9 | { 10 | [Test] 11 | public void ShouldAppendSuffixIfThereIsNoOverlap() 12 | { 13 | var result = new Uri("http://www.mysite.com").EnsureEndsWith("suffix"); 14 | 15 | Assert.AreEqual(result.ToString(), "http://www.mysite.com/suffix"); 16 | } 17 | 18 | [Test] 19 | public void ShouldRemoveAnyOverlapBetweenBaseAddresAndSuffix() 20 | { 21 | var result = new Uri("http://www.mysite.com/virtual").EnsureEndsWith("/virtual/suffix"); 22 | 23 | Assert.AreEqual(result.ToString(), "http://www.mysite.com/virtual/suffix"); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /source/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/ExportCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Octopus.Cli.Util; 4 | using Octopus.CommandLine; 5 | using Octopus.CommandLine.Commands; 6 | 7 | namespace Octopus.Cli.Commands 8 | { 9 | [Command("export", Description = "Exports an object to a JSON file. Deprecated. Please see https://g.octopushq.com/DataMigration for alternative options.")] 10 | public class ExportCommand : CommandBase 11 | { 12 | public override Task Execute(string[] commandLineArgs) 13 | { 14 | throw new CommandException($"The {AssemblyExtensions.GetExecutableName()} import/export commands have been deprecated. See https://g.octopushq.com/DataMigration for alternative options."); 15 | } 16 | 17 | public ExportCommand(ICommandOutputProvider commandOutputProvider) : base(commandOutputProvider) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Package/IPackageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NuGet.Packaging; 4 | 5 | namespace Octopus.Cli.Commands.Package 6 | { 7 | public enum PackageCompressionLevel 8 | { 9 | None, 10 | Fast, 11 | Optimal 12 | } 13 | 14 | public enum PackageFormat 15 | { 16 | Zip, 17 | NuPkg, 18 | 19 | [Obsolete("This is just here for backwards compat")] 20 | Nuget 21 | } 22 | 23 | public interface IPackageBuilder 24 | { 25 | string[] Files { get; } 26 | 27 | string PackageFormat { get; } 28 | 29 | void BuildPackage(string basePath, 30 | IList includes, 31 | ManifestMetadata metadata, 32 | string outFolder, 33 | bool overwrite, 34 | bool verboseInfo); 35 | 36 | void SetCompression(PackageCompressionLevel level); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/ListReleasesCommandFixture.ShouldGetListOfReleases.approved.txt: -------------------------------------------------------------------------------- 1 | Octopus Deploy Command Line Tool, version 2 | 3 | Releases: 4 4 | - Project: ProjectA 5 | Version: 1.0 6 | Assembled: 7 | Package Versions: Deploy a package 1.0 8 | Release Notes: Release Notes 1 9 | 10 | Version: 2.0 11 | Assembled: 12 | Package Versions: 13 | Release Notes: Release Notes 2 14 | 15 | Version: whateverdockerversion 16 | Assembled: 17 | Package Versions: 18 | Release Notes: Release Notes 3 19 | 20 | - Project: ProjectB 21 | - Project: Version controlled project 22 | Version: 1.2.3 23 | Assembled: 24 | Package Versions: 25 | Release Notes: Version controlled release notes 26 | Git Reference: main 27 | Git Commit: 87a072ad2b4a2e9bf2d7ff84d8636a032786394d 28 | 29 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Model/ProjectExport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Octopus.Client.Model; 4 | 5 | namespace Octopus.Cli.Model 6 | { 7 | public class ProjectExport 8 | { 9 | public ProjectResource Project { get; set; } 10 | public ReferenceDataItem ProjectGroup { get; set; } 11 | public VariableSetResource VariableSet { get; set; } 12 | public List NuGetFeeds { get; set; } 13 | public List ActionTemplates { get; set; } 14 | public DeploymentProcessResource DeploymentProcess { get; set; } 15 | public List LibraryVariableSets { get; set; } 16 | public ReferenceDataItem Lifecycle { get; set; } 17 | public List Channels { get; set; } 18 | public List ChannelLifecycles { get; set; } 19 | public List WorkerPools { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /source/Octo.Tests/Extensions/TimeSpanExtensionsFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using Octopus.Cli.Extensions; 4 | 5 | namespace Octo.Tests.Extensions 6 | { 7 | [TestFixture] 8 | public class TimeSpanExtensionsFixture 9 | { 10 | [Test] 11 | public void FormattingTests() 12 | { 13 | Assert.That(new TimeSpan(3, 2, 5, 7).Friendly(), Is.EqualTo("3 days, 2h:5m:7s")); 14 | Assert.That(new TimeSpan(1, 2, 5, 7).Friendly(), Is.EqualTo("1 day, 2h:5m:7s")); 15 | Assert.That(new TimeSpan(0, 2, 5, 7).Friendly(), Is.EqualTo("2h:5m:7s")); 16 | Assert.That(new TimeSpan(0, 0, 5, 7).Friendly(), Is.EqualTo("5m:7s")); 17 | Assert.That(new TimeSpan(0, 0, 0, 7).Friendly(), Is.EqualTo("7s")); 18 | Assert.That(new TimeSpan(0, 0, 0, 0).Friendly(), Is.EqualTo("0s")); 19 | Assert.That(TimeSpan.FromSeconds(84).Friendly(), Is.EqualTo("1m:24s")); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/VersionCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Octopus.Cli.Util; 4 | using Octopus.CommandLine; 5 | using Octopus.CommandLine.Commands; 6 | 7 | namespace Octopus.Cli.Commands 8 | { 9 | [Command("version", "v", "ver", Description = "Outputs Octopus CLI version.")] 10 | public class VersionCommand : CommandBase 11 | { 12 | public VersionCommand(ICommandOutputProvider commandOutputProvider) : base(commandOutputProvider) 13 | { 14 | } 15 | 16 | public override Task Execute(string[] commandLineArgs) 17 | { 18 | return Task.Run(() => 19 | { 20 | Options.Parse(commandLineArgs); 21 | 22 | if (printHelp) 23 | GetHelp(Console.Out, commandLineArgs); 24 | else 25 | Console.WriteLine($"{typeof(CliProgram).GetInformationalVersion()}"); 26 | }); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Extensions/TimeSpanExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Octopus.Cli.Extensions 4 | { 5 | public static class TimeSpanExtensions 6 | { 7 | public static string Friendly(this TimeSpan time) 8 | { 9 | if (time.TotalDays >= 1.0) 10 | return string.Format("{0:n0} day{1}, {2:n0}h:{3:n0}m:{4:n0}s", 11 | time.TotalDays, 12 | time.TotalDays >= 1.9 ? "s" : "", 13 | time.Hours, 14 | time.Minutes, 15 | time.Seconds); 16 | 17 | if (time.TotalHours >= 1.0) 18 | return string.Format("{0:n0}h:{1:n0}m:{2:n0}s", time.TotalHours, time.Minutes, time.Seconds); 19 | 20 | if (time.TotalMinutes >= 1.0) 21 | return string.Format("{0:n0}m:{1:n0}s", time.TotalMinutes, time.Seconds); 22 | 23 | return string.Format("{0:n0}s", time.TotalSeconds); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/NumericExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Octopus.Cli.Util 4 | { 5 | public static class NumericExtensions 6 | { 7 | const long Kilobyte = 1024; 8 | const long Megabyte = 1024 * Kilobyte; 9 | const long Gigabyte = 1024 * Megabyte; 10 | const long Terabyte = 1024 * Gigabyte; 11 | 12 | public static string ToFileSizeString(this long bytes) 13 | { 14 | return ToFileSizeString(bytes <= 0 ? 0 : (ulong)bytes); 15 | } 16 | 17 | public static string ToFileSizeString(this ulong bytes) 18 | { 19 | if (bytes > Terabyte) return (bytes / Terabyte).ToString("0 TB"); 20 | if (bytes > Gigabyte) return (bytes / Gigabyte).ToString("0 GB"); 21 | if (bytes > Megabyte) return (bytes / Megabyte).ToString("0 MB"); 22 | if (bytes > Kilobyte) return (bytes / Kilobyte).ToString("0 KB"); 23 | return bytes + " bytes"; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/ImportCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Octopus.Cli.Repositories; 5 | using Octopus.Cli.Util; 6 | using Octopus.Client; 7 | using Octopus.CommandLine; 8 | using Octopus.CommandLine.Commands; 9 | 10 | namespace Octopus.Cli.Commands 11 | { 12 | [Command("import", Description = "Imports an Octopus object from an export file. Deprecated. Please see https://g.octopushq.com/DataMigration for alternative options.")] 13 | public class ImportCommand : CommandBase 14 | { 15 | public override Task Execute(string[] commandLineArgs) 16 | { 17 | throw new CommandException($"The {AssemblyExtensions.GetExecutableName()} import/export commands have been deprecated. See https://g.octopushq.com/DataMigration for alternative options."); 18 | } 19 | 20 | public ImportCommand(ICommandOutputProvider commandOutputProvider) : base(commandOutputProvider) 21 | { 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Dockerfiles/alpine/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/runtime:6.0-alpine 2 | 3 | # Alpine base image does not have ICU libraries available 4 | # https://github.com/dotnet/corefx/blob/master/Documentation/architecture/globalization-invariant-mode.md 5 | RUN apk add --no-cache icu-libs 6 | ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false 7 | ENV LC_ALL en_US.UTF-8 8 | ENV LANG en_US.UTF-8 9 | 10 | # The dotnetcore bootstrapper doesnt work in alpine shell (built for bash) 11 | # This allows invoking octo if running interactive container 12 | RUN mkdir /octo &&\ 13 | echo "dotnet /octo/octo.dll \"\$@\"" > /octo/alpine &&\ 14 | ln /octo/alpine /usr/bin/octo &&\ 15 | chmod +x /usr/bin/octo 16 | 17 | ARG OCTO_TOOLS_VERSION=4.31.1 18 | 19 | LABEL maintainer="devops@octopus.com" 20 | LABEL octopus.dockerfile.version="1.0" 21 | LABEL octopus.tools.version=$OCTO_TOOLS_VERSION 22 | 23 | ADD OctopusTools.$OCTO_TOOLS_VERSION.portable.tar.gz /octo 24 | 25 | WORKDIR /src 26 | ENTRYPOINT ["dotnet", "/octo/octo.dll"] 27 | -------------------------------------------------------------------------------- /source/Octo.Tests/Helpers/ConsoleWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace Octopus.Cli.Tests.Helpers 6 | { 7 | //copied from https://stackoverflow.com/a/11911734/779192 8 | public class ConsoleWriter : TextWriter 9 | { 10 | public override Encoding Encoding => Encoding.UTF8; 11 | 12 | public override void Write(string value) 13 | { 14 | WriteEvent?.Invoke(this, new ConsoleWriterEventArgs(value)); 15 | } 16 | 17 | public override void WriteLine(string value) 18 | { 19 | WriteLineEvent?.Invoke(this, new ConsoleWriterEventArgs(value)); 20 | } 21 | 22 | public event EventHandler WriteEvent; 23 | public event EventHandler WriteLineEvent; 24 | 25 | public class ConsoleWriterEventArgs : EventArgs 26 | { 27 | public ConsoleWriterEventArgs(string value) 28 | { 29 | Value = value; 30 | } 31 | 32 | public string Value { get; } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Confused? https://help.github.com/articles/dealing-with-line-endings/ 2 | # Set the default behavior, in case people don't have core.autocrlf set. 3 | * text=auto 4 | 5 | *.doc diff=astextplain 6 | *.DOC diff=astextplain 7 | *.docx diff=astextplain 8 | *.DOCX diff=astextplain 9 | *.dot diff=astextplain 10 | *.DOT diff=astextplain 11 | *.pdf diff=astextplain 12 | *.PDF diff=astextplain 13 | *.rtf diff=astextplain 14 | *.RTF diff=astextplain 15 | 16 | *.bmp binary 17 | *.gif binary 18 | *.jpg binary 19 | *.png binary 20 | 21 | *.ascx text 22 | *.cmd text 23 | *.coffee text 24 | *.config text 25 | *.cs text diff=csharp 26 | *.css text 27 | *.less text 28 | *.cshtml text 29 | *.htm text 30 | *.html text 31 | *.htm text 32 | *.js text 33 | *.json text 34 | *.msbuild text 35 | *.resx text 36 | *.ruleset text 37 | *.Stylecop text 38 | *.targets text 39 | *.tt text 40 | *.txt text 41 | *.vb text 42 | *.vbhtml text 43 | *.xml text 44 | *.xunit text 45 | 46 | *.csproj text merge=union 47 | *.vbproj text merge=union 48 | 49 | *.sln text eol=crlf merge=union 50 | 51 | *.approved.* -text 52 | 53 | octo eol=lf 54 | *.sh eol=lf 55 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | Thank you for going to the effort of contributing some code. 3 | 4 | If you've found a bug or something isn't working, please quickly check the issues in 5 | our [main issue repository](https://github.com/OctopusDeploy/Issues). 6 | 7 | If your change is small or you have already made the change, please go ahead and submit a pull request. 8 | 9 | For other changes, please raise an issue first to check that the change aligns with our plan for the product. We ask this to avoid disappointment and wasted effort if it does not. 10 | 11 | If your pull request or proposed change is for a bug that is causing problems for you, please also [contact support](http://octopusdeploy.com/support) referencing your pull request to make sure it gets prioritized properly. They may also be able to find a work around in the meantime. 12 | 13 | If you have an idea or a feature request, please post it to [our UserVoice site](http://octopusdeploy.uservoice.com) so others can vote for it. 14 | 15 | You will need to have this [version of .NET Core SDK](https://download.microsoft.com/download/0/A/3/0A372822-205D-4A86-BFA7-084D2CBE9EDF/DotNetCore.1.0.1-SDK.1.0.0.Preview2-003133-x64.exe) installed to compile the solution. -------------------------------------------------------------------------------- /source/Octo.Tests/Helpers/ApprovalScrubberExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace Octopus.Cli.Tests.Helpers 5 | { 6 | public static class ApprovalScrubberExtensions 7 | { 8 | static readonly Regex CliVersionScrubber = new Regex("(?<=Octopus Deploy Command Line Tool, version\\s)[^\\s]+", RegexOptions.Compiled | RegexOptions.IgnoreCase); 9 | 10 | static readonly Regex AssembledTimestampScrubber = new Regex(@"Assembled: (?.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase); 11 | 12 | public static string ScrubApprovalString(this string approval) 13 | { 14 | return approval.ScrubCliVersion().ScrubAssembledTimestamps(); 15 | } 16 | 17 | static string ScrubCliVersion(this string subject) 18 | { 19 | return CliVersionScrubber.Replace(subject, ""); 20 | } 21 | 22 | static string ScrubAssembledTimestamps(this string subject) 23 | { 24 | foreach (Match m in AssembledTimestampScrubber.Matches(subject)) 25 | subject = subject.Replace(m.Groups["assembled"].Value, ""); 26 | return subject; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.*scc 2 | *.FileListAbsolute.txt 3 | *.aps 4 | *.bak 5 | *.[Cc]ache 6 | *.clw 7 | *.eto 8 | *.fb6lck 9 | *.fbl6 10 | *.fbpInf 11 | *.ilk 12 | *.lib 13 | *.log 14 | *.ncb 15 | *.nlb 16 | *.obj 17 | *.patch 18 | *.pch 19 | *.pdb 20 | *.plg 21 | *.[Pp]ublish.xml 22 | *.rdl.data 23 | *.sbr 24 | *.scc 25 | *.sig 26 | *.sqlsuo 27 | *.suo 28 | *.svclog 29 | *.tlb 30 | *.tlh 31 | *.tli 32 | *.tmp 33 | *.user 34 | *.vshost.* 35 | *DXCore.Solution 36 | *_i.c 37 | *_p.c 38 | Ankh.Load 39 | Backup*/ 40 | CVS/ 41 | PrecompiledWeb/ 42 | UpgradeLog*.* 43 | [Bb]in/ 44 | [Dd]ebug/ 45 | [Oo]bj/ 46 | [Rr]elease/ 47 | [Tt]humbs.db 48 | _UpgradeReport_Files 49 | _[Rr]e[Ss]harper.*/ 50 | hgignore[.-]* 51 | ignore[.-]* 52 | svnignore[.-]* 53 | lint.db 54 | *.ReSharper 55 | source/packages 56 | source/OctopusTools.v2.ncrunchsolution 57 | *.orig 58 | *.userprefs 59 | *.lock.json 60 | .vs 61 | .vscode 62 | /tools/Addins/ 63 | /tools/Cake/ 64 | /tools/GitVersion.CommandLine 65 | /tools/ILRepack 66 | /tools/* 67 | !/tools/packages.config 68 | /artifacts/ 69 | /publish/ 70 | TestResult.xml 71 | TestResults/ 72 | *.received.* 73 | .idea/ 74 | /source/Octo/Properties/launchSettings.json 75 | **/ExpectedSdkVersion.txt 76 | **/launchSettings.json 77 | 78 | .DS_Store 79 | 80 | octoversion.txt 81 | -------------------------------------------------------------------------------- /source/Octo.Tests/Diagnostics/LogUtilitiesFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using NUnit.Framework; 4 | using Octopus.Cli.Diagnostics; 5 | using Octopus.CommandLine.Commands; 6 | using Serilog.Events; 7 | 8 | namespace Octo.Tests.Diagnostics 9 | { 10 | [TestFixture] 11 | public sealed class LogUtilitiesFixture 12 | { 13 | [Test] 14 | public void ShouldThrowIfUnknownLogLevelIsProvided() 15 | { 16 | var result = Assert.Throws(() => LogUtilities.ParseLogLevel("z")); 17 | result.Message.ShouldBeEquivalentTo("Unrecognized loglevel 'z'. Valid options are verbose, debug, information, warning, error and fatal. " + 18 | "Defaults to 'debug'."); 19 | } 20 | 21 | [TestCase("fatal", LogEventLevel.Fatal)] 22 | [TestCase("error", LogEventLevel.Error)] 23 | [TestCase("warning", LogEventLevel.Warning)] 24 | [TestCase("information", LogEventLevel.Information)] 25 | [TestCase("debug", LogEventLevel.Debug)] 26 | [TestCase("verbose", LogEventLevel.Verbose)] 27 | public void ShouldParseLogLevelCorrectly(string value, LogEventLevel level) 28 | { 29 | var result = LogUtilities.ParseLogLevel(value); 30 | result.ShouldBeEquivalentTo(level); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/UriExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // ReSharper disable CheckNamespace 4 | namespace Octopus.Cli.Util 5 | { 6 | public static class UriExtensions 7 | // ReSharper restore CheckNamespace 8 | { 9 | public static Uri EnsureEndsWith(this Uri uri, string suffix) 10 | { 11 | var path = uri.AbsolutePath.ToLowerInvariant(); 12 | suffix = suffix.ToLowerInvariant(); 13 | var overlap = FindOverlapSection(path, suffix); 14 | if (!string.IsNullOrEmpty(overlap)) 15 | { 16 | path = path.Replace(overlap, string.Empty); 17 | suffix = suffix.Replace(overlap, string.Empty); 18 | } 19 | 20 | path = path + overlap + suffix; 21 | path = path.Replace("//", "/"); 22 | 23 | return new Uri(uri, path); 24 | } 25 | 26 | static string FindOverlapSection(string value1, string value2) 27 | { 28 | var longer = value1; 29 | var shorter = value2; 30 | if (shorter.Length > longer.Length) 31 | { 32 | var temp = longer; 33 | longer = shorter; 34 | shorter = temp; 35 | } 36 | 37 | return longer.Contains(shorter) ? shorter : string.Empty; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /BuildAssets/OctopusTools.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OctopusTools 5 | Octopus CLI 6 | $version$ 7 | Octopus Deploy 8 | Octopus Deploy 9 | Create, deploy and promote releases using Octopus Deploy. 10 | 11 | Octopus is a user-friendly DevOps tool for developers that supports release management, deployment automation, and operations runbooks. 12 | 13 | This package contains the Octopus CLI (octo), a tool to create and deploy releases, create and push packages, and manage environments with Octopus. 14 | 15 | 16 | en-US 17 | false 18 | Apache-2.0 19 | https://github.com/OctopusDeploy/OctopusCLI/ 20 | 21 | tools\icon.png 22 | automation deployment 23 | true 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /source/Octo/Octo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1;net6.0 5 | true 6 | portable 7 | octo 8 | Exe 9 | OctopusTools 10 | false 11 | false 12 | false 13 | false 14 | false 15 | Octo 16 | true 17 | win-x64;linux-x64;osx-x64 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Model/ChannelVersionRuleTestResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Octopus.Client.Model; 4 | 5 | namespace Octopus.Cli.Model 6 | { 7 | public class ChannelVersionRuleTestResult : Resource 8 | { 9 | const string Pass = "PASS"; 10 | const string Fail = "FAIL"; 11 | public IEnumerable Errors { get; set; } 12 | public bool SatisfiesVersionRange { get; set; } 13 | public bool SatisfiesPreReleaseTag { get; set; } 14 | public bool IsSatisfied => SatisfiesVersionRange && SatisfiesPreReleaseTag; 15 | public bool IsNull { get; private set; } 16 | 17 | public string ToSummaryString() 18 | { 19 | return IsNull ? "Allow any version" : $"Range: {(SatisfiesVersionRange ? Pass : Fail)} Tag: {(SatisfiesPreReleaseTag ? Pass : Fail)}"; 20 | } 21 | 22 | public static ChannelVersionRuleTestResult Failed() 23 | { 24 | return new ChannelVersionRuleTestResult 25 | { 26 | IsNull = false, 27 | SatisfiesVersionRange = false, 28 | SatisfiesPreReleaseTag = false 29 | }; 30 | } 31 | 32 | public static ChannelVersionRuleTestResult Null() 33 | { 34 | return new ChannelVersionRuleTestResult 35 | { 36 | IsNull = true, 37 | SatisfiesVersionRange = true, 38 | SatisfiesPreReleaseTag = true 39 | }; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text.RegularExpressions; 5 | using Octopus.CommandLine.Commands; 6 | 7 | namespace Octopus.Cli.Util 8 | { 9 | public static class StringExtensions 10 | { 11 | public static string CommaSeperate(this IEnumerable items) 12 | { 13 | return string.Join(", ", items); 14 | } 15 | 16 | public static string NewlineSeperate(this IEnumerable items) 17 | { 18 | return string.Join(Environment.NewLine, items); 19 | } 20 | 21 | public static string NormalizeNewLinesForWindows(this string originalString) 22 | { 23 | return Regex.Replace(originalString, @"\r\n|\n\r|\n|\r", "\r\n"); 24 | } 25 | 26 | public static string NormalizeNewLinesForNix(this string originalString) 27 | { 28 | return Regex.Replace(originalString, @"\r\n|\n\r|\n|\r", "\n"); 29 | } 30 | 31 | public static string NormalizeNewLines(this string originalString) 32 | { 33 | return Regex.Replace(originalString, @"\r\n|\n\r|\n|\r", Environment.NewLine); 34 | } 35 | 36 | public static void CheckForIllegalPathCharacters(this string path, string name) 37 | { 38 | if (path.IndexOfAny(Path.GetInvalidPathChars()) >= 0) 39 | throw new CommandException($"Argument {name} has a value of {path} which contains invalid path characters. If your path has a trailing backslash either remove it or escape it correctly by using \\\\"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Releases/ChannelVersionRuleTester.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Octopus.Cli.Model; 4 | using Octopus.Cli.Util; 5 | using Octopus.Client; 6 | using Octopus.Client.Model; 7 | 8 | namespace Octopus.Cli.Commands.Releases 9 | { 10 | public class ChannelVersionRuleTester : IChannelVersionRuleTester 11 | { 12 | public async Task Test(IOctopusAsyncRepository repository, ChannelVersionRuleResource rule, string packageVersion, string feedId) 13 | { 14 | if (rule == null) 15 | // Anything goes if there is no rule defined for this step 16 | return ChannelVersionRuleTestResult.Null(); 17 | 18 | if (string.IsNullOrEmpty(packageVersion)) 19 | // If we don't have a package version, this rule should be ignored 20 | return ChannelVersionRuleTestResult.Failed(); 21 | 22 | var link = await repository.Link("VersionRuleTest").ConfigureAwait(false); 23 | 24 | var resource = new 25 | { 26 | version = packageVersion, 27 | versionRange = rule.VersionRange, 28 | preReleaseTag = rule.Tag, 29 | feedId 30 | }; 31 | 32 | var response = (await repository.LoadRootDocument().ConfigureAwait(false)).UsePostForChannelVersionRuleTest() 33 | ? repository.Client.Post(link, resource) 34 | : repository.Client.Get(link, resource); 35 | 36 | return await response.ConfigureAwait(false); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/ListReleasesCommandFixture.JsonFormat_ShouldBeWellFormed.approved.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Project": { 4 | "Id": "projectaid", 5 | "Name": "ProjectA" 6 | }, 7 | "Releases": [ 8 | { 9 | "Version": "1.0", 10 | "Assembled": "0001-01-01T00:00:00.000+00:00", 11 | "PackageVersions": "", 12 | "ReleaseNotes": "Release Notes 1", 13 | "GitReference": null, 14 | "GitCommit": null 15 | }, 16 | { 17 | "Version": "2.0", 18 | "Assembled": "9999-12-31T23:59:59.999+00:00", 19 | "PackageVersions": "", 20 | "ReleaseNotes": "Release Notes 2", 21 | "GitReference": null, 22 | "GitCommit": null 23 | }, 24 | { 25 | "Version": "whateverdockerversion", 26 | "Assembled": "9999-12-31T23:59:59.999+00:00", 27 | "PackageVersions": "", 28 | "ReleaseNotes": "Release Notes 3", 29 | "GitReference": null, 30 | "GitCommit": null 31 | } 32 | ] 33 | }, 34 | { 35 | "Project": { 36 | "Id": "projectbid", 37 | "Name": "ProjectB" 38 | }, 39 | "Releases": [] 40 | }, 41 | { 42 | "Project": { 43 | "Id": "Projects-3", 44 | "Name": "Version controlled project" 45 | }, 46 | "Releases": [ 47 | { 48 | "Version": "1.2.3", 49 | "Assembled": "9999-12-31T23:59:59.999+00:00", 50 | "PackageVersions": "", 51 | "ReleaseNotes": "Version controlled release notes", 52 | "GitReference": "main", 53 | "GitCommit": "87a072ad2b4a2e9bf2d7ff84d8636a032786394d" 54 | } 55 | ] 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Infrastructure/CouldNotFindException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Octopus.CommandLine.Commands; 5 | 6 | namespace Octopus.Cli.Infrastructure 7 | { 8 | public class CouldNotFindException : CommandException 9 | { 10 | public CouldNotFindException(string what) 11 | : base("Could not find " + what + "; either it does not exist or you lack permissions to view it.") 12 | { 13 | } 14 | 15 | public CouldNotFindException(string what, string quotedName) 16 | : this(what + " '" + quotedName + "'") 17 | { 18 | } 19 | 20 | public CouldNotFindException(string resourceTypeDisplayName, 21 | string nameOrId, 22 | string enclosingContextDescription = "") : base( 23 | $"Cannot find the {resourceTypeDisplayName} with name or id '{nameOrId}'{enclosingContextDescription}. " 24 | + "Please check the spelling and that you have permissions to view it. Please use Configuration > Test Permissions to confirm.") 25 | { 26 | } 27 | 28 | public CouldNotFindException(string resourceTypeDisplayName, 29 | ICollection missingNamesOrIds, 30 | string enclosingContextDescription = "") : base( 31 | $"The {resourceTypeDisplayName}{(missingNamesOrIds.Count == 1 ? "" : "s")} {string.Join(", ", missingNamesOrIds.Select(m => "'" + m + "'"))} " 32 | + $"do{(missingNamesOrIds.Count == 1 ? "es" : "")} not exist{enclosingContextDescription} or you do not have permissions to view {(missingNamesOrIds.Count == 1 ? "it" : "them")}.") 33 | { 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/FeedCustomExpressionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Octopus.Client.Model; 3 | 4 | namespace Octopus.Cli.Util 5 | { 6 | /// 7 | /// Helps with situations where feeds use custom expressions Eg. #{MyCustomFeedURL} 8 | /// 9 | public static class FeedCustomExpressionHelper 10 | { 11 | public static string CustomExpressionFeedName = "Custom expression"; 12 | 13 | public static FeedResource CustomExpressionFeedWithId(string id) 14 | { 15 | var feed = new NuGetFeedResource 16 | { 17 | Id = id, 18 | Name = CustomExpressionFeedName 19 | }; 20 | return feed; 21 | } 22 | 23 | /// 24 | /// Helps to check for a valid repository-based feed Id. 25 | /// 26 | /// Feeds may have custom expressions as their Id, which may contain Octostache variable syntax #{MyCustomFeedURL}. 27 | /// If you pass a custom expression Id like this to the API, it will resolve to /api/feeds/, return all the feeds, then 28 | /// attempt to cast that response to a FeedResource object and you'll end up getting an empty FeedResource object instead 29 | /// of null. This method helps you detect valid repository feed objects before running into this confusing API scenario. 30 | /// 31 | /// 32 | /// 33 | public static bool IsRealFeedId(string id) 34 | { 35 | if (id.StartsWith("feeds-", StringComparison.OrdinalIgnoreCase)) 36 | return true; 37 | return false; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/WorkerPool/ListWorkerPoolsCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Octopus.Cli.Repositories; 6 | using Octopus.Cli.Util; 7 | using Octopus.Client; 8 | using Octopus.Client.Model; 9 | using Octopus.CommandLine; 10 | using Octopus.CommandLine.Commands; 11 | 12 | namespace Octopus.Cli.Commands.WorkerPool 13 | { 14 | [Command("list-workerpools", Description = "Lists worker pools.")] 15 | public class ListWorkerPoolsCommand : ApiCommand, ISupportFormattedOutput 16 | { 17 | List pools; 18 | 19 | public ListWorkerPoolsCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 20 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 21 | { 22 | } 23 | 24 | public async Task Request() 25 | { 26 | pools = await Repository.WorkerPools.FindAll().ConfigureAwait(false); 27 | } 28 | 29 | public void PrintDefaultOutput() 30 | { 31 | commandOutputProvider.Information("WorkerPools: {Count}", pools.Count); 32 | 33 | foreach (var pool in pools) 34 | commandOutputProvider.Information(" - {WorkerPools:l} (ID: {Id:l})", pool.Name, pool.Id); 35 | } 36 | 37 | public void PrintJsonOutput() 38 | { 39 | commandOutputProvider.Json( 40 | pools.Select(pool => new 41 | { 42 | pool.Id, 43 | pool.Name 44 | })); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BuildAssets/repos/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | # JFrog Artifactory Initial Setup 4 | 5 | *TODO: Automate this. It might be more appropriate as a runbook than part of package deployment, because (1) it belongs equally to Tentacle and Octopus CLI, (2) we'd usually want to bootstrap some initial packages, (3) it requires administrative permissions.* 6 | 7 | - Admin: General Security Configuration 8 | - First remove anonymous from permissions & groups 9 | - [x] Allow Anonymous Access 10 | - [x] Hide Existence of Unauthorized Resources 11 | - Allow Basic Read of Build Related Info 12 | [ ] Apply on Anonymous Access 13 | - Admin: Repositories: Local: New 14 | - Type: debian 15 | - Key: apt-prerelease 16 | - Admin: Repositories: Local: New 17 | - Type: debian 18 | - Key: apt 19 | - Admin: Repositories: Local: New 20 | - Type: rpm 21 | - Key: rpm-prerelease 22 | - [ ] Auto Calculate RPM Metadata 23 | - [x] Enable file list indexing 24 | - RPM Metadata Folder Depth: 1 25 | - Admin: Repositories: Local: New 26 | - Type: rpm 27 | - Key: rpm 28 | - [ ] Auto Calculate RPM Metadata 29 | - [x] Enable file list indexing 30 | - RPM Metadata Folder Depth: 1 31 | - Admin: Signing Keys 32 | - Added public/private/passphrase 33 | - Admin: Users: New 34 | - User Name: linux-package-uploader 35 | - Email Address: devops@octopus.com 36 | - Set password 37 | - Remove from groups 38 | - Admin: Permissions: Add 39 | - Name: linux-package-uploader 40 | - Repos: apt-prerelease, apt, rpm-prerelease, rpm 41 | - Users: linux-package-uploader 42 | - Actions: Repository up to Manage 43 | - Admin: Permissions: Add 44 | - Name: linux-package-downloader 45 | - Repos: apt-prerelease, apt, rpm-prerelease, rpm 46 | - Users: anonymous 47 | - Actions: Read 48 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Releases/ReleasePlanItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Octopus.Cli.Model; 3 | 4 | namespace Octopus.Cli.Commands.Releases 5 | { 6 | public class ReleasePlanItem 7 | { 8 | public ReleasePlanItem(string actionName, 9 | string packageReferenceName, 10 | string packageId, 11 | string packageFeedId, 12 | bool isResolveable, 13 | string userSpecifiedVersion) 14 | { 15 | ActionName = actionName; 16 | PackageReferenceName = packageReferenceName; 17 | PackageId = packageId; 18 | PackageFeedId = packageFeedId; 19 | IsResolveable = isResolveable; 20 | Version = userSpecifiedVersion; 21 | VersionSource = string.IsNullOrWhiteSpace(Version) ? "Cannot resolve" : "User specified"; 22 | } 23 | 24 | public string ActionName { get; } 25 | 26 | public string PackageReferenceName { get; set; } 27 | 28 | public string PackageId { get; } 29 | 30 | public string PackageFeedId { get; } 31 | 32 | public bool IsResolveable { get; } 33 | 34 | public string Version { get; private set; } 35 | 36 | public string VersionSource { get; private set; } 37 | 38 | public ChannelVersionRuleTestResult ChannelVersionRuleTestResult { get; private set; } 39 | 40 | public bool IsDisabled { get; set; } 41 | 42 | public void SetVersionFromLatest(string version) 43 | { 44 | Version = version; 45 | VersionSource = "Latest available"; 46 | } 47 | 48 | public void SetChannelVersionRuleTestResult(ChannelVersionRuleTestResult result) 49 | { 50 | ChannelVersionRuleTestResult = result; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Project/ListProjectsCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Octopus.Cli.Repositories; 6 | using Octopus.Cli.Util; 7 | using Octopus.Client; 8 | using Octopus.Client.Model; 9 | using Octopus.CommandLine; 10 | using Octopus.CommandLine.Commands; 11 | 12 | namespace Octopus.Cli.Commands.Project 13 | { 14 | [Command("list-projects", Description = "Lists all projects.")] 15 | public class ListProjectsCommand : ApiCommand, ISupportFormattedOutput 16 | { 17 | List _projectResources; 18 | 19 | public ListProjectsCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 20 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 21 | { 22 | } 23 | 24 | public async Task Request() 25 | { 26 | var projects = await Repository.Projects.FindAll().ConfigureAwait(false); 27 | _projectResources = projects; 28 | } 29 | 30 | public void PrintDefaultOutput() 31 | { 32 | commandOutputProvider.Information("Projects: {Count}", _projectResources.Count); 33 | foreach (var project in _projectResources) 34 | commandOutputProvider.Information(" - {Project:l} (ID: {Id:l})", project.Name, project.Id); 35 | } 36 | 37 | public void PrintJsonOutput() 38 | { 39 | commandOutputProvider.Json( 40 | _projectResources.Select(project => new 41 | { 42 | project.Id, 43 | project.Name 44 | })); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Environment/ListEnvironmentsCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Octopus.Cli.Repositories; 6 | using Octopus.Cli.Util; 7 | using Octopus.Client; 8 | using Octopus.Client.Model; 9 | using Octopus.CommandLine; 10 | using Octopus.CommandLine.Commands; 11 | 12 | namespace Octopus.Cli.Commands.Environment 13 | { 14 | [Command("list-environments", Description = "Lists environments.")] 15 | public class ListEnvironmentsCommand : ApiCommand, ISupportFormattedOutput 16 | { 17 | List environments; 18 | 19 | public ListEnvironmentsCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 20 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 21 | { 22 | } 23 | 24 | public async Task Request() 25 | { 26 | environments = await Repository.Environments.FindAll().ConfigureAwait(false); 27 | } 28 | 29 | public void PrintDefaultOutput() 30 | { 31 | commandOutputProvider.Information("Environments: {Count}", environments.Count); 32 | 33 | foreach (var environment in environments) 34 | commandOutputProvider.Information(" - {Environment:l} (ID: {Id:l})", environment.Name, environment.Id); 35 | } 36 | 37 | public void PrintJsonOutput() 38 | { 39 | commandOutputProvider.Json( 40 | environments.Select(environment => new 41 | { 42 | environment.Id, 43 | environment.Name 44 | })); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /source/Octopus.DotNet.Cli/Octopus.DotNet.Cli.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | 9 | netcoreapp3.1;net6.0 10 | True 11 | dotnet-octo 12 | Octopus.DotNet.Cli 13 | Octopus Deploy 14 | Octopus Deploy Pty Ltd 15 | icon.png 16 | https://octopus.com/ 17 | Apache-2.0 18 | git 19 | https://github.com/OctopusDeploy/OctopusCli 20 | Octopus Deploy is an automated release management tool for modern developers and DevOps teams. 21 | 22 | This package contains the dotnet tool version of the CLI library for interacting with the HTTP API in Octopus. 23 | 24 | DotnetCliTool 25 | true 26 | 27 | 28 | 29 | 30 | all 31 | runtime; build; native; contentfiles; analyzers 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /source/Octo.Tests/Extensions/StringExtensionsFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using NUnit.Framework; 5 | using Octopus.Cli.Util; 6 | 7 | namespace Octo.Tests.Extensions 8 | { 9 | [TestFixture] 10 | public class StringExtensionsFixture 11 | { 12 | [Test] 13 | [TestCaseSource(nameof(GetTestCases))] 14 | public void ShouldNormalizeLineEndings(string input, string windowsResult, string nixResult, string platformAgnosticResult) 15 | { 16 | input.NormalizeNewLinesForWindows().Should().Be(windowsResult, "windows requires carriage return, line feed."); 17 | input.NormalizeNewLinesForNix().Should().Be(nixResult, "*nix requires line feed."); 18 | input.NormalizeNewLines().Should().Be(platformAgnosticResult, "should use the environments preference."); 19 | } 20 | 21 | public static IEnumerable GetTestCases() 22 | { 23 | var newLine = Environment.NewLine; 24 | 25 | yield return new TestCaseData(string.Empty, string.Empty, string.Empty, string.Empty); 26 | yield return new TestCaseData("ABC 123 foo )&*)(*&\nbar\r\nABC 123 foo )&*)(*&\n\r", "ABC 123 foo )&*)(*&\r\nbar\r\nABC 123 foo )&*)(*&\r\n", "ABC 123 foo )&*)(*&\nbar\nABC 123 foo )&*)(*&\n", $"ABC 123 foo )&*)(*&{newLine}bar{newLine}ABC 123 foo )&*)(*&{newLine}"); 27 | yield return new TestCaseData("\n\r\n\n\r", "\r\n\r\n\r\n", "\n\n\n", $"{newLine}{newLine}{newLine}"); 28 | yield return new TestCaseData("\n foo \n\n", "\r\n foo \r\n\r\n", "\n foo \n\n", $"{newLine} foo {newLine}{newLine}"); 29 | yield return new TestCaseData("\t\n\0", "\t\r\n\0", "\t\n\0", $"\t{newLine}\0"); 30 | yield return new TestCaseData($"{newLine}{newLine}{newLine}", "\r\n\r\n\r\n", "\n\n\n", $"{newLine}{newLine}{newLine}"); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/VersionTestFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using FluentAssertions; 5 | using NUnit.Framework; 6 | using Octopus.Cli.Commands; 7 | using Octopus.Cli.Tests.Helpers; 8 | using Octopus.CommandLine; 9 | using Serilog; 10 | 11 | namespace Octo.Tests.Commands 12 | { 13 | [TestFixture] 14 | public class VersionTestFixture 15 | { 16 | VersionCommand versionCommand; 17 | StringWriter output; 18 | TextWriter originalOutput; 19 | ICommandOutputProvider commandOutputProvider; 20 | ILogger logger; 21 | 22 | [SetUp] 23 | public void SetUp() 24 | { 25 | originalOutput = Console.Out; 26 | output = new StringWriter(); 27 | Console.SetOut(output); 28 | 29 | commandOutputProvider = new CommandOutputProvider("Octo", "1.0.0", logger); 30 | versionCommand = new VersionCommand(commandOutputProvider); 31 | logger = new LoggerConfiguration().WriteTo.TextWriter(output).CreateLogger(); 32 | } 33 | 34 | [Test] 35 | public void ShouldPrintCorrectVersionNumber() 36 | { 37 | var filename = Path.Combine(Path.GetDirectoryName(AssemblyPath()), "ExpectedSdkVersion.txt"); 38 | var version = GetVersionFromFile(filename); 39 | 40 | versionCommand.Execute(); 41 | 42 | output.ToString() 43 | .Should() 44 | .Contain(version); 45 | } 46 | 47 | static string AssemblyPath() 48 | { 49 | return Assembly.GetExecutingAssembly().Location; 50 | } 51 | 52 | string GetVersionFromFile(string versionFilePath) 53 | { 54 | using (var reader = new StreamReader(File.OpenRead(versionFilePath))) 55 | { 56 | return reader.ReadLine(); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Tenant/ListTenantsCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Octopus.Cli.Repositories; 6 | using Octopus.Cli.Util; 7 | using Octopus.Client; 8 | using Octopus.Client.Model; 9 | using Octopus.CommandLine; 10 | using Octopus.CommandLine.Commands; 11 | 12 | namespace Octopus.Cli.Commands.Tenant 13 | { 14 | [Command("list-tenants", Description = "Lists tenants.")] 15 | public class ListTenantsCommand : ApiCommand, ISupportFormattedOutput 16 | { 17 | List tenants; 18 | 19 | public ListTenantsCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 20 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 21 | { 22 | } 23 | 24 | public async Task Request() 25 | { 26 | var multiTenancyStatus = await Repository.Tenants.Status().ConfigureAwait(false); 27 | if (multiTenancyStatus.Enabled) 28 | tenants = await Repository.Tenants.FindAll().ConfigureAwait(false); 29 | else 30 | throw new CommandException("Multi-Tenancy is not enabled"); 31 | } 32 | 33 | public void PrintDefaultOutput() 34 | { 35 | commandOutputProvider.Information("Tenants: {Count}", tenants.Count); 36 | 37 | foreach (var tenant in tenants.OrderBy(m => m.Name)) 38 | commandOutputProvider.Information(" - {Tenant:l} (ID: {Count})", tenant.Name, tenant.Id); 39 | } 40 | 41 | public void PrintJsonOutput() 42 | { 43 | commandOutputProvider.Json(tenants.OrderBy(x => x.Name) 44 | .Select(t => new 45 | { 46 | t.Id, 47 | t.Name 48 | })); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /source/Octo.Tests/Util/CommandSuggesterFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using NUnit.Framework; 5 | using Octopus.Cli.Util; 6 | 7 | namespace Octo.Tests.Util 8 | { 9 | [TestFixture] 10 | public class CompleteCommandFixture 11 | { 12 | readonly Dictionary testCompletionItems = new Dictionary 13 | { 14 | { "list-deployments", new[] { "--apiKey" } }, 15 | { "list-environments", new[] { "--url", "--space" } }, 16 | { "list-projects", new string[] { } }, 17 | { "help", new[] { "--helpOutputFormat", "--help" } } 18 | }; 19 | 20 | [TestCaseSource(nameof(GetTestCases))] 21 | public void ShouldGetCorrectCompletions(string[] words, string[] expectedItems) 22 | { 23 | CommandSuggester.SuggestCommandsFor(words, testCompletionItems).ShouldBeEquivalentTo(expectedItems); 24 | } 25 | 26 | static IEnumerable GetTestCases() 27 | { 28 | yield return new TestCaseData(new[] { "list" }, new[] { "list-deployments", "list-environments", "list-projects" }); 29 | yield return new TestCaseData(new[] { "list-e" }, new[] { "list-environments" }); 30 | yield return new TestCaseData(new[] { "" }, new[] { "list-deployments", "list-environments", "list-projects", "help" }); 31 | yield return new TestCaseData(new string[] { null }, new[] { "list-deployments", "list-environments", "list-projects", "help" }); 32 | yield return new TestCaseData(new[] { "junk" }, new string[] { }); 33 | yield return new TestCaseData(new[] { "list-environments", "--" }, new[] { "--url", "--space" }); 34 | yield return new TestCaseData(new[] { "list-environments", "--sp" }, new[] { "--space" }); 35 | yield return new TestCaseData(new[] { "--" }, new[] { "--help", "--helpOutputFormat" }); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Diagnostics/LogUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Octopus.CommandLine.Commands; 5 | using Octopus.CommandLine.OptionParsing; 6 | using Serilog.Core; 7 | using Serilog.Events; 8 | 9 | namespace Octopus.Cli.Diagnostics 10 | { 11 | static class LogUtilities 12 | { 13 | static readonly Dictionary Lookup; 14 | 15 | static LogUtilities() 16 | { 17 | LevelSwitch = new LoggingLevelSwitch(DefaultLogLevel); 18 | Lookup = ((LogEventLevel[])Enum.GetValues(typeof(LogEventLevel))) 19 | .ToDictionary(key => key.ToString().ToLowerInvariant(), value => value); 20 | } 21 | 22 | public static LoggingLevelSwitch LevelSwitch { get; } 23 | public static LogEventLevel DefaultLogLevel => LogEventLevel.Debug; 24 | 25 | public static LogEventLevel ParseLogLevel(string value) 26 | { 27 | if (Lookup.TryGetValue(value, out var level)) 28 | return level; 29 | 30 | throw new CommandException($"Unrecognized loglevel '{value}'. Valid options are {GetValidOptions()}. " + 31 | $"Defaults to '{DefaultLogLevel.ToString().ToLowerInvariant()}'."); 32 | } 33 | 34 | public static void AddLogLevelOptions(this OptionSet options) 35 | { 36 | var description = $"[Optional] The log level. Valid options are {GetValidOptions()}. " + 37 | $"Defaults to '{DefaultLogLevel.ToString().ToLowerInvariant()}'."; 38 | options.Add("logLevel=", description, s => LevelSwitch.MinimumLevel = s); 39 | } 40 | 41 | static string GetValidOptions() 42 | { 43 | var values = Lookup.Where(kv => kv.Key.Length > 1).OrderBy(x => x.Value).Select(x => x.Key).ToArray(); 44 | return $"{string.Join(", ", values.Take(values.Length - 1))} and {values.Last()}"; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/ReplaceStatus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Octopus.Cli.Util 4 | { 5 | public struct ReplaceStatus : IEquatable 6 | { 7 | public static readonly ReplaceStatus Created = new ReplaceStatus("Created"); 8 | public static readonly ReplaceStatus Deleted = new ReplaceStatus("Deleted"); 9 | public static readonly ReplaceStatus Updated = new ReplaceStatus("Updated"); 10 | public static readonly ReplaceStatus SkippedIncomplete = new ReplaceStatus("Skipped (incomplete)"); 11 | public static readonly ReplaceStatus SkippedTooOld = new ReplaceStatus("Skipped (maxage)"); 12 | public static readonly ReplaceStatus SkippedNoOverwrite = new ReplaceStatus("Skipped (no overwrite)"); 13 | public static readonly ReplaceStatus SkippedNull = new ReplaceStatus("Skipped (null)"); 14 | public static readonly ReplaceStatus Unchanged = new ReplaceStatus("Unchanged"); 15 | public static readonly ReplaceStatus MissingDependencies = new ReplaceStatus("Missing dependencies"); 16 | public static readonly ReplaceStatus PropertyAdded = new ReplaceStatus("Property added"); 17 | public static readonly ReplaceStatus PropertyDeleted = new ReplaceStatus("Property deleted"); 18 | public static readonly ReplaceStatus PropertyUnchanged = new ReplaceStatus("Property unchanged"); 19 | public static readonly ReplaceStatus PropertyUpdated = new ReplaceStatus("Property updated"); 20 | public static readonly ReplaceStatus Error = new ReplaceStatus("Error"); 21 | public readonly string Description; 22 | 23 | public ReplaceStatus(string description) 24 | { 25 | Description = description; 26 | } 27 | 28 | public bool Equals(ReplaceStatus other) 29 | { 30 | return Description == other.Description; 31 | } 32 | 33 | public override string ToString() 34 | { 35 | return Description; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/ListExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | // ReSharper disable CheckNamespace 6 | 7 | namespace Octopus.Cli.Util 8 | { 9 | public static class ListExtensions 10 | // ReSharper restore CheckNamespace 11 | { 12 | public static void RemoveWhere(this IList source, Func remove) 13 | { 14 | if (source == null) 15 | return; 16 | 17 | for (var i = 0; i < source.Count; i++) 18 | { 19 | var item = source[i]; 20 | if (!remove(item)) 21 | continue; 22 | 23 | source.RemoveAt(i); 24 | i--; 25 | } 26 | } 27 | 28 | public static void AddRange(this ICollection source, IEnumerable itemsToAdd) 29 | { 30 | if (itemsToAdd == null || source == null) 31 | return; 32 | 33 | foreach (var item in itemsToAdd) 34 | source.Add(item); 35 | } 36 | 37 | public static void AddRangeUnique(this ICollection source, IEnumerable itemsToAdd) 38 | { 39 | if (itemsToAdd == null || source == null) 40 | return; 41 | 42 | foreach (var item in itemsToAdd.Where(item => !source.Contains(item))) 43 | source.Add(item); 44 | } 45 | 46 | public static IEnumerable Apply(this IEnumerable source, Action apply) 47 | { 48 | foreach (var item in source) 49 | { 50 | apply(item); 51 | yield return item; 52 | } 53 | } 54 | 55 | #if NET45 56 | public static HashSet ToHashSet(this IEnumerable items) => new HashSet(items); 57 | #endif 58 | public static bool None(this IEnumerable items) 59 | { 60 | return !items.Any(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/ListWorkerPoolsCommandFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using Newtonsoft.Json; 6 | using NSubstitute; 7 | using NUnit.Framework; 8 | using Octopus.Cli.Commands.WorkerPool; 9 | using Octopus.Client.Model; 10 | 11 | namespace Octo.Tests.Commands 12 | { 13 | [TestFixture] 14 | public class ListWorkerPoolsCommandFixture : ApiCommandFixtureBase 15 | { 16 | ListWorkerPoolsCommand listWorkerPoolsCommand; 17 | 18 | [SetUp] 19 | public void SetUp() 20 | { 21 | listWorkerPoolsCommand = new ListWorkerPoolsCommand(RepositoryFactory, FileSystem, ClientFactory, CommandOutputProvider); 22 | } 23 | 24 | [Test] 25 | public async Task ShouldGetListOfWorkerPools() 26 | { 27 | SetupPools(); 28 | 29 | await listWorkerPoolsCommand.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 30 | 31 | LogLines.Should().Contain("WorkerPools: 2"); 32 | LogLines.Should().Contain(" - default (ID: defaultid)"); 33 | LogLines.Should().Contain(" - windows (ID: windowsid)"); 34 | } 35 | 36 | [Test] 37 | public async Task JsonFormat_ShouldBeWellFormed() 38 | { 39 | SetupPools(); 40 | 41 | CommandLineArgs.Add("--outputFormat=json"); 42 | await listWorkerPoolsCommand.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 43 | 44 | var logoutput = LogOutput.ToString(); 45 | JsonConvert.DeserializeObject(logoutput); 46 | logoutput.Should().Contain("defaultid"); 47 | logoutput.Should().Contain("windowsid"); 48 | } 49 | 50 | void SetupPools() 51 | { 52 | Repository.WorkerPools.FindAll() 53 | .Returns(new List 54 | { 55 | new WorkerPoolResource { Name = "default", Id = "defaultid" }, 56 | new WorkerPoolResource { Name = "windows", Id = "windowsid" } 57 | }); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /BuildAssets/repos/test-linux-package-from-feed-in-dists.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Smoke test our apt and rpm feeds in various dockerized distros. 3 | 4 | if [[ ! -e "$LPF_PATH" ]]; then 5 | echo "This script requires the environment variable LPF_PATH - the location of 'linux-package-feeds' scripts to use." >&2 6 | echo "They come from https://github.com/OctopusDeploy/linux-package-feeds, distributed in TeamCity" >&2 7 | echo " via 'Infrastructure / Linux Package Feeds'." >&2 8 | exit 1 9 | fi 10 | if [[ -z "$PUBLISH_LINUX_EXTERNAL" ]]; then 11 | echo 'This script requires the environment variable PUBLISH_LINUX_EXTERNAL - specify "true" to test the external public feed.' >&2 12 | exit 1 13 | fi 14 | if [[ -z "$OCTOPUS_CLI_SERVER" || -z "$OCTOPUS_CLI_API_KEY" || -z "$OCTOPUS_SPACE" || -z "$OCTOPUS_EXPECT_ENV" ]]; then 15 | echo -e 'This script requires the environment variables OCTOPUS_CLI_SERVER, OCTOPUS_CLI_API_KEY, OCTOPUS_SPACE, and'\ 16 | '\nOCTOPUS_EXPECT_ENV - specifying an Octopus server for testing "list-environments", an API key to access it, the'\ 17 | '\nSpace to search, and an environment name expected to be found there.' >&2 18 | exit 1 19 | fi 20 | 21 | SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" 22 | 23 | 24 | for DOCKER_IMAGE in $(cat "$LPF_PATH/test-env-docker-images.conf" | grep -o '^[^#]*' | tr -d '\r') 25 | do 26 | if [[ "$DOCKER_IMAGE" == *rhel* ]]; then 27 | RHEL_OPTS='--env REDHAT_SUBSCRIPTION_USERNAME --env REDHAT_SUBSCRIPTION_PASSWORD' 28 | else 29 | RHEL_OPTS='' 30 | fi 31 | 32 | echo "== Testing in '$DOCKER_IMAGE' ==" 33 | docker pull "$DOCKER_IMAGE" >/dev/null || exit 34 | docker run --rm \ 35 | --hostname "octotestfeedpkg$RANDOM" \ 36 | --volume "$(pwd):/working" --volume "$SCRIPT_DIR/test-linux-package-from-feed.sh:/test-linux-package-from-feed.sh" \ 37 | --volume "$(realpath "$LPF_PATH"):/opt/linux-package-feeds" \ 38 | --env PUBLISH_LINUX_EXTERNAL \ 39 | --env OCTOPUS_CLI_SERVER --env OCTOPUS_CLI_API_KEY --env OCTOPUS_SPACE --env OCTOPUS_EXPECT_ENV \ 40 | $RHEL_OPTS \ 41 | "$DOCKER_IMAGE" bash -c 'cd /working && bash /test-linux-package-from-feed.sh' || exit 42 | done 43 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/ListEnvironmentsCommandFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using Newtonsoft.Json; 6 | using NSubstitute; 7 | using NUnit.Framework; 8 | using Octopus.Cli.Commands.Environment; 9 | using Octopus.Client.Model; 10 | 11 | namespace Octo.Tests.Commands 12 | { 13 | [TestFixture] 14 | public class ListEnvironmentsCommandFixture : ApiCommandFixtureBase 15 | { 16 | ListEnvironmentsCommand listEnvironmentsCommand; 17 | 18 | [SetUp] 19 | public void SetUp() 20 | { 21 | listEnvironmentsCommand = new ListEnvironmentsCommand(RepositoryFactory, FileSystem, ClientFactory, CommandOutputProvider); 22 | } 23 | 24 | [Test] 25 | public async Task ShouldGetListOfEnvironments() 26 | { 27 | SetupEnvironments(); 28 | 29 | await listEnvironmentsCommand.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 30 | 31 | LogLines.Should().Contain("Environments: 2"); 32 | LogLines.Should().Contain(" - Dev (ID: devenvid)"); 33 | LogLines.Should().Contain(" - Prod (ID: prodenvid)"); 34 | } 35 | 36 | [Test] 37 | public async Task JsonFormat_ShouldBeWellFormed() 38 | { 39 | SetupEnvironments(); 40 | 41 | CommandLineArgs.Add("--outputFormat=json"); 42 | await listEnvironmentsCommand.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 43 | 44 | var logoutput = LogOutput.ToString(); 45 | JsonConvert.DeserializeObject(logoutput); 46 | logoutput.Should().Contain("devenvid"); 47 | logoutput.Should().Contain("prodenvid"); 48 | } 49 | 50 | void SetupEnvironments() 51 | { 52 | Repository.Environments.FindAll() 53 | .Returns(new List 54 | { 55 | new EnvironmentResource { Name = "Dev", Id = "devenvid" }, 56 | new EnvironmentResource { Name = "Prod", Id = "prodenvid" } 57 | }); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/LineSplitter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Octopus.Cli.Util 6 | { 7 | public static class LineSplitter 8 | { 9 | const int MaxLineLength = 80; 10 | 11 | public static string Split(string indentation, string text) 12 | { 13 | var lines = new List(); 14 | 15 | var currentLine = new StringBuilder(); 16 | var lastWasWhitespace = false; 17 | var additional = indentation.Length; 18 | 19 | var pop = new Action(delegate(bool isForceWrap) 20 | { 21 | var current = currentLine.ToString(); 22 | if (string.IsNullOrWhiteSpace(current)) return; 23 | 24 | var next = indentation; 25 | 26 | if (isForceWrap) 27 | { 28 | var lastSpace = current.LastIndexOf(' '); 29 | if (lastSpace > MaxLineLength - additional - 10) 30 | { 31 | next += current.Substring(lastSpace).Trim(); 32 | current = current.Substring(0, lastSpace).TrimEnd(); 33 | } 34 | } 35 | 36 | lines.Add(current); 37 | currentLine.Clear(); 38 | currentLine.Append(next); 39 | additional = 0; 40 | }); 41 | 42 | foreach (var c in text) 43 | { 44 | if (c == '\r' || c == '\n') 45 | { 46 | pop(false); 47 | lastWasWhitespace = true; 48 | continue; 49 | } 50 | 51 | if (currentLine.Length >= MaxLineLength - additional) 52 | pop(true); 53 | 54 | if (char.IsWhiteSpace(c) && lastWasWhitespace) 55 | continue; 56 | 57 | currentLine.Append(c); 58 | 59 | lastWasWhitespace = char.IsWhiteSpace(c); 60 | } 61 | 62 | pop(false); 63 | 64 | return string.Join(Environment.NewLine, lines); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/DeletionOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Octopus.Cli.Util 4 | { 5 | public class DeletionOptions : IEquatable 6 | { 7 | DeletionOptions() 8 | { 9 | SleepBetweenAttemptsMilliseconds = 100; 10 | } 11 | 12 | public static DeletionOptions TryThreeTimes => new DeletionOptions { RetryAttempts = 3, ThrowOnFailure = true }; 13 | 14 | public static DeletionOptions TryThreeTimesIgnoreFailure => new DeletionOptions { RetryAttempts = 3, ThrowOnFailure = false }; 15 | 16 | public int RetryAttempts { get; private set; } 17 | public int SleepBetweenAttemptsMilliseconds { get; } 18 | public bool ThrowOnFailure { get; private set; } 19 | 20 | public bool Equals(DeletionOptions other) 21 | { 22 | if (ReferenceEquals(null, other)) return false; 23 | if (ReferenceEquals(this, other)) return true; 24 | return RetryAttempts == other.RetryAttempts && SleepBetweenAttemptsMilliseconds == other.SleepBetweenAttemptsMilliseconds && ThrowOnFailure.Equals(other.ThrowOnFailure); 25 | } 26 | 27 | public override bool Equals(object obj) 28 | { 29 | if (ReferenceEquals(null, obj)) return false; 30 | if (ReferenceEquals(this, obj)) return true; 31 | if (obj.GetType() != GetType()) return false; 32 | return Equals((DeletionOptions)obj); 33 | } 34 | 35 | public override int GetHashCode() 36 | { 37 | unchecked 38 | { 39 | var hashCode = RetryAttempts; 40 | hashCode = (hashCode * 397) ^ SleepBetweenAttemptsMilliseconds; 41 | hashCode = (hashCode * 397) ^ ThrowOnFailure.GetHashCode(); 42 | return hashCode; 43 | } 44 | } 45 | 46 | public static bool operator ==(DeletionOptions left, DeletionOptions right) 47 | { 48 | return Equals(left, right); 49 | } 50 | 51 | public static bool operator !=(DeletionOptions left, DeletionOptions right) 52 | { 53 | return !Equals(left, right); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/DummyApiCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Octopus.Cli.Commands; 4 | using Octopus.Cli.Repositories; 5 | using Octopus.Cli.Util; 6 | using Octopus.Client; 7 | using Octopus.CommandLine; 8 | using Octopus.CommandLine.Commands; 9 | 10 | namespace Octo.Tests.Commands 11 | { 12 | public class DummyApiCommand : ApiCommand 13 | { 14 | string pill; 15 | 16 | public DummyApiCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 17 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 18 | { 19 | var options = Options.For("Dummy"); 20 | options.Add("pill=", "Red or Blue. Blue, the story ends. Red, stay in Wonderland and see how deep the rabbit hole goes.", v => pill = v); 21 | commandOutputProvider.Debug("Pill: " + pill); 22 | } 23 | 24 | protected override Task Execute() 25 | { 26 | return Task.WhenAll(); 27 | } 28 | } 29 | 30 | [Command("dummy-command", Description = "this is the command's description")] 31 | public class DummyApiCommandWithFormattedOutputSupport : ApiCommand, ISupportFormattedOutput 32 | { 33 | public DummyApiCommandWithFormattedOutputSupport(IOctopusClientFactory clientFactory, IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, ICommandOutputProvider commandOutputProvider) 34 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 35 | { 36 | } 37 | 38 | public bool QueryCalled { get; set; } 39 | public bool PrintDefaultOutputCalled { get; set; } 40 | public bool PrintJsonOutputCalled { get; set; } 41 | 42 | public Task Request() 43 | { 44 | QueryCalled = true; 45 | return Task.WhenAll(); 46 | } 47 | 48 | public void PrintDefaultOutput() 49 | { 50 | PrintDefaultOutputCalled = true; 51 | } 52 | 53 | public void PrintJsonOutput() 54 | { 55 | PrintJsonOutputCalled = true; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Notice 2 | 3 | Octopus CLI is no longer under feature development. All feature development of the Octopus CLI is being done in the updated [cli](https://github.com/OctopusDeploy/cli). 4 | This tool will continue to be supported for security patches until July 2023. 5 | 6 | --- 7 | 8 | This repository contains the Octopus CLI (`octo`) for [Octopus][1], a user-friendly DevOps tool for developers that supports release management, deployment automation, and operations runbooks. You can use it to create and deploy releases, create and push packages, and manage environments. 9 | 10 | `octo` can be [downloaded from the Octopus downloads page][2]. 11 | 12 | ## Documentation 13 | 14 | - [octo][3] 15 | 16 | ## Issues 17 | 18 | Please see [Contributing](CONTRIBUTING.md) 19 | 20 | ## Development 21 | 22 | ### Pre-requisites 23 | 24 | You need the following items installed on your system: 25 | 26 | - Rider, VSCode or Visual Studio 15.3 27 | - .NET Core SDK 6.x 28 | 29 | ### Build and Test 30 | 31 | Run the build script to build, test and package the project. 32 | 33 | **Do this before pushing as it will run the surface area tests as well, which require approval on almost every change.** 34 | 35 | #### Unix-like systems 36 | 37 | ``` 38 | # on Unix-like systems we don't generate the OctopusTools NuGet package as it calls `nuget.exe` to create the package. 39 | $ ./build.sh 40 | ``` 41 | 42 | #### Windows 43 | 44 | ``` 45 | > build.cmd 46 | ``` 47 | 48 | ### Publish a new version 49 | 50 | To release a new version, tag `main` with the next `..` version, [GitHub Actions][5] will build, test and produce the required packages and [Octopus Deploy][6] will do publish the packages to the appropriate locations. 51 | 52 | Every successful GitHub Actions build for all branches will be pushed to Feedz.io. 53 | 54 | ## Compatibility 55 | 56 | See the [Compatibility][4] page in our docs 57 | 58 | [1]: https://octopus.com 59 | [2]: https://octopus.com/downloads 60 | [3]: https://octopus.com/docs/api-and-integration/octo.exe-command-line 61 | [4]: https://octopus.com/docs/api-and-integration/compatibility 62 | [5]: https://github.com/OctopusDeploy/OctopusCLI/actions/workflows/build.yml 63 | [6]: https://deploy.octopus.app/app#/Spaces-62/projects/octopus-cli/deployments 64 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/ListProjectsCommandFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using Newtonsoft.Json; 6 | using NSubstitute; 7 | using NUnit.Framework; 8 | using Octopus.Cli.Commands.Project; 9 | using Octopus.Client.Model; 10 | 11 | namespace Octo.Tests.Commands 12 | { 13 | [TestFixture] 14 | public class ListProjectsCommandFixture : ApiCommandFixtureBase 15 | { 16 | ListProjectsCommand listProjectsCommand; 17 | 18 | [SetUp] 19 | public void SetUp() 20 | { 21 | listProjectsCommand = new ListProjectsCommand(RepositoryFactory, FileSystem, ClientFactory, CommandOutputProvider); 22 | } 23 | 24 | [Test] 25 | public async Task ShouldGetListOfProjects() 26 | { 27 | Repository.Projects.FindAll() 28 | .Returns(new List 29 | { 30 | new ProjectResource { Name = "ProjectA", Id = "projectaid" }, 31 | new ProjectResource { Name = "ProjectB", Id = "projectbid" } 32 | }); 33 | 34 | await listProjectsCommand.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 35 | 36 | LogLines.Should().Contain("Projects: 2"); 37 | LogLines.Should().Contain(" - ProjectA (ID: projectaid)"); 38 | LogLines.Should().Contain(" - ProjectB (ID: projectbid)"); 39 | } 40 | 41 | [Test] 42 | public async Task JsonFormat_ShouldBeWellFormed() 43 | { 44 | CommandLineArgs.Add("--outputFormat=json"); 45 | Repository.Projects.FindAll() 46 | .Returns(new List 47 | { 48 | new ProjectResource { Name = "ProjectA", Id = "projectaid" }, 49 | new ProjectResource { Name = "ProjectB", Id = "projectbid" } 50 | }); 51 | 52 | await listProjectsCommand.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 53 | 54 | var logoutput = LogOutput.ToString(); 55 | JsonConvert.DeserializeObject(logoutput); 56 | logoutput.Should().Contain("projectaid"); 57 | logoutput.Should().Contain("projectbid"); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Repositories/ActionTemplateRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Octopus.Client; 5 | using Octopus.Client.Model; 6 | 7 | namespace Octopus.Cli.Repositories 8 | { 9 | public class ActionTemplateRepository : IActionTemplateRepository 10 | { 11 | readonly IOctopusAsyncClient client; 12 | 13 | public ActionTemplateRepository(IOctopusAsyncClient client) 14 | { 15 | this.client = client; 16 | } 17 | 18 | public async Task Get(string idOrHref) 19 | { 20 | if (string.IsNullOrWhiteSpace(idOrHref)) return null; 21 | var templatesPath = await client.Repository.Link("ActionTemplates").ConfigureAwait(false); 22 | return await client.Get(templatesPath, new { id = idOrHref }).ConfigureAwait(false); 23 | } 24 | 25 | public async Task Create(ActionTemplateResource resource) 26 | { 27 | var templatesPath = await client.Repository.Link("ActionTemplates").ConfigureAwait(false); 28 | return await client.Create(templatesPath, resource).ConfigureAwait(false); 29 | } 30 | 31 | public Task Modify(ActionTemplateResource resource) 32 | { 33 | return client.Update(resource.Links["Self"], resource); 34 | } 35 | 36 | public async Task FindByName(string name) 37 | { 38 | ActionTemplateResource template = null; 39 | 40 | name = (name ?? string.Empty).Trim(); 41 | var templatesPath = await client.Repository.Link("ActionTemplates").ConfigureAwait(false); 42 | await client.Paginate(templatesPath, 43 | page => 44 | { 45 | template = page.Items.FirstOrDefault(t => string.Equals(t.Name ?? string.Empty, name, StringComparison.OrdinalIgnoreCase)); 46 | // If no matching template was found, then we need to try the next page. 47 | return template == null; 48 | }) 49 | .ConfigureAwait(false); 50 | 51 | return template; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/CreateWorkerPoolCommandFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using FluentAssertions; 4 | using Newtonsoft.Json; 5 | using NSubstitute; 6 | using NUnit.Framework; 7 | using Octopus.Cli.Commands.WorkerPool; 8 | using Octopus.Client.Model; 9 | 10 | namespace Octo.Tests.Commands 11 | { 12 | public class CreateWorkerPoolCommandFixture : ApiCommandFixtureBase 13 | { 14 | CreateWorkerPoolCommand createWorkerPoolCommand; 15 | 16 | [SetUp] 17 | public void Setup() 18 | { 19 | createWorkerPoolCommand = new CreateWorkerPoolCommand(RepositoryFactory, FileSystem, ClientFactory, CommandOutputProvider); 20 | } 21 | 22 | [Test] 23 | public async Task DefaultOutput_CreateNewWorkerPool() 24 | { 25 | var newPool = Guid.NewGuid().ToString(); 26 | CommandLineArgs.Add($"--name={newPool}"); 27 | 28 | Repository.WorkerPools.FindByName(Arg.Any()).Returns((WorkerPoolResource)null); 29 | Repository.WorkerPools.Create(Arg.Any()) 30 | .Returns(new WorkerPoolResource { Id = Guid.NewGuid().ToString(), Name = newPool }); 31 | 32 | await createWorkerPoolCommand.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 33 | 34 | LogLines.Should().Contain($"Creating worker pool: {newPool}"); 35 | } 36 | 37 | [Test] 38 | public async Task JsonOutput_CreateNewWorkerPool() 39 | { 40 | var newPool = Guid.NewGuid().ToString(); 41 | CommandLineArgs.Add($"--name={newPool}"); 42 | CommandLineArgs.Add("--outputFormat=json"); 43 | 44 | Repository.WorkerPools.FindByName(Arg.Any()).Returns((WorkerPoolResource)null); 45 | Repository.WorkerPools.Create(Arg.Any()) 46 | .Returns(new WorkerPoolResource { Id = Guid.NewGuid().ToString(), Name = newPool }); 47 | 48 | await createWorkerPoolCommand.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 49 | 50 | var logoutput = LogOutput.ToString(); 51 | Console.WriteLine(logoutput); 52 | JsonConvert.DeserializeObject(logoutput); 53 | logoutput.Should().Contain(newPool); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/ListTenantsFixtures.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using Newtonsoft.Json; 6 | using NSubstitute; 7 | using NUnit.Framework; 8 | using Octopus.Cli.Commands.Tenant; 9 | using Octopus.Client.Model; 10 | using Octopus.CommandLine.Commands; 11 | 12 | namespace Octo.Tests.Commands 13 | { 14 | public class ListTenantsFixtures : ApiCommandFixtureBase 15 | { 16 | ListTenantsCommand listTenantsCommand; 17 | 18 | [SetUp] 19 | public void Setup() 20 | { 21 | listTenantsCommand = new ListTenantsCommand(RepositoryFactory, FileSystem, ClientFactory, CommandOutputProvider); 22 | Repository.Tenants.FindAll() 23 | .Returns(Task.FromResult(new List 24 | { 25 | new TenantResource { Name = "Tenant1", Id = "Tenant-1" }, 26 | new TenantResource { Name = "Tenant2", Id = "Tenant-2" } 27 | })); 28 | 29 | Repository.Tenants.Status() 30 | .ReturnsForAnyArgs(new MultiTenancyStatusResource { Enabled = true }); 31 | } 32 | 33 | [Test] 34 | public async Task MultiTenacyFeatureDisabled_ShouldThrowException() 35 | { 36 | Repository.Tenants.Status() 37 | .ReturnsForAnyArgs(new MultiTenancyStatusResource { Enabled = false }); 38 | 39 | try 40 | { 41 | await listTenantsCommand.Execute(CommandLineArgs.ToArray()); 42 | Assert.Fail("Should have thrown CommandException"); 43 | } 44 | catch (CommandException commandException) 45 | { 46 | commandException.Message.Should().Contain("Multi-Tenancy"); 47 | } 48 | } 49 | 50 | [Test] 51 | public async Task JsonFormat_ShouldBeWellFormed() 52 | { 53 | CommandLineArgs.Add("--outputFormat=json"); 54 | await listTenantsCommand.Execute(CommandLineArgs.ToArray()); 55 | var logoutput = LogOutput.ToString(); 56 | JsonConvert.DeserializeObject(logoutput); 57 | logoutput.Should().Contain("Tenant-1"); 58 | logoutput.Should().Contain("Tenant-2"); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Releases/IPackageVersionResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Octopus.Cli.Commands.Releases 4 | { 5 | public interface IPackageVersionResolver 6 | { 7 | void AddFolder(string folderPath); 8 | void Add(string stepNameOrPackageIdAndVersion); 9 | 10 | /// 11 | /// Adds a package version to be used with the release 12 | /// 13 | /// The name of the step or package 14 | /// The named package, or null if this is the default (unnamed) package 15 | /// The version of the package 16 | void Add(string stepNameOrPackageId, string packageReferenceName, string packageVersion); 17 | 18 | /// 19 | /// Adds a package version for the unnamed package to be used with the release 20 | /// 21 | /// The name of the step or package 22 | /// The version of the package 23 | void Add(string stepNameOrPackageId, string packageVersion); 24 | 25 | void Default(string packageVersion); 26 | 27 | /// 28 | /// Get the version of a previously defined package 29 | /// 30 | /// The name of the step or package 31 | /// The named package, or null if this is the default (unnamed) package 32 | /// The package ID, used as a secondary check to stepName 33 | /// The version assigned to the package 34 | string ResolveVersion(string stepName, string packageId, string packageReferenceName); 35 | 36 | /// 37 | /// Get the version of a previously defined unnamed package (i.e. package reference null is null) 38 | /// 39 | /// The name of the step or package 40 | /// The package ID, used as a secondary check to stepName 41 | /// The version assigned to the package 42 | string ResolveVersion(string stepName, string packageId); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /BuildAssets/test-linux-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Test that the octopuscli*.deb or octopuscli*.rpm package in the working directory installs an octo command that can list-environments. 3 | 4 | if [[ -z "$OCTOPUS_CLI_SERVER" || -z "$OCTOPUS_CLI_API_KEY" || -z "$OCTOPUS_SPACE" || -z "$OCTOPUS_EXPECT_ENV" ]]; then 5 | echo -e 'This script requires the environment variables OCTOPUS_CLI_SERVER, OCTOPUS_CLI_API_KEY, OCTOPUS_SPACE, and'\ 6 | '\nOCTOPUS_EXPECT_ENV - specifying an Octopus server for testing "list-environments", an API key to access it, the'\ 7 | '\nSpace to search, and an environment name expected to be found there.' >&2 8 | exit 1 9 | fi 10 | 11 | OSRELID="$(. /etc/os-release && echo $ID)" 12 | if [[ "$OSRELID" == "rhel" && ( -z "$REDHAT_SUBSCRIPTION_USERNAME" || -z "$REDHAT_SUBSCRIPTION_PASSWORD" ) ]]; then 13 | echo -e 'This script requires the environment variables REDHAT_SUBSCRIPTION_USERNAME and REDHAT_SUBSCRIPTION_PASSWORD to register'\ 14 | '\nthe test system to install packages.' >&2 15 | exit 1 16 | fi 17 | 18 | 19 | # Install the package (with any needed docker config, system registration, dependencies) using a script from 'linux-package-feeds'. 20 | 21 | bash ./install-linux-package.sh || exit 22 | 23 | if command -v dpkg > /dev/null; then 24 | echo Detected dpkg. Installing ca-certificates to support octo HTTPS communication. 25 | export DEBIAN_FRONTEND=noninteractive 26 | apt-get --no-install-recommends --yes install ca-certificates >/dev/null || exit 27 | fi 28 | 29 | if [[ "$OSRELID" == "fedora" ]]; then 30 | echo "Fedora detected. Setting DOTNET_BUNDLE_EXTRACT_BASE_DIR to $(pwd)/dotnet-extraction-dir" 31 | # to workaround error 32 | # realpath(): Operation not permitted 33 | # Failure processing application bundle. 34 | # Failed to determine location for extracting embedded files 35 | # DOTNET_BUNDLE_EXTRACT_BASE_DIR is not set, and a read-write temp-directory couldn't be created. 36 | # A fatal error was encountered. Could not extract contents of the bundle 37 | 38 | mkdir dotnet-extraction-dir 39 | export DOTNET_BUNDLE_EXTRACT_BASE_DIR=$(pwd)/dotnet-extraction-dir 40 | fi 41 | 42 | echo Testing octo. 43 | octo version || exit 44 | OCTO_RESULT="$(octo list-environments --space="$OCTOPUS_SPACE")" || { echo "$OCTO_RESULT"; exit 1; } 45 | echo "$OCTO_RESULT" | grep "$OCTOPUS_EXPECT_ENV" || { echo "Expected environment not found: $OCTOPUS_EXPECT_ENV." >&2; exit 1; } 46 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/CreateEnvironmentCommandFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using FluentAssertions; 4 | using Newtonsoft.Json; 5 | using NSubstitute; 6 | using NUnit.Framework; 7 | using Octopus.Cli.Commands.Environment; 8 | using Octopus.Client.Model; 9 | 10 | namespace Octo.Tests.Commands 11 | { 12 | public class CreateEnvironmentCommandFixture : ApiCommandFixtureBase 13 | { 14 | CreateEnvironmentCommand createEnvironmentCommand; 15 | 16 | [SetUp] 17 | public void Setup() 18 | { 19 | createEnvironmentCommand = new CreateEnvironmentCommand(RepositoryFactory, FileSystem, ClientFactory, CommandOutputProvider); 20 | //Repository.Environments.Create(Arg.Any()).Returns() 21 | } 22 | 23 | [Test] 24 | public async Task DefaultOutput_CreateNewEnvironment() 25 | { 26 | var newEnv = Guid.NewGuid().ToString(); 27 | CommandLineArgs.Add($"--name={newEnv}"); 28 | 29 | Repository.Environments.FindByName(Arg.Any()).Returns((EnvironmentResource)null); 30 | Repository.Environments.Create(Arg.Any()) 31 | .Returns(new EnvironmentResource { Id = Guid.NewGuid().ToString(), Name = newEnv }); 32 | 33 | await createEnvironmentCommand.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 34 | 35 | LogLines.Should().Contain($"Creating environment: {newEnv}"); 36 | } 37 | 38 | [Test] 39 | public async Task JsonOutput_CreateNewEnvironment() 40 | { 41 | var newEnv = Guid.NewGuid().ToString(); 42 | CommandLineArgs.Add($"--name={newEnv}"); 43 | CommandLineArgs.Add("--outputFormat=json"); 44 | 45 | Repository.Environments.FindByName(Arg.Any()).Returns((EnvironmentResource)null); 46 | Repository.Environments.Create(Arg.Any()) 47 | .Returns(new EnvironmentResource { Id = Guid.NewGuid().ToString(), Name = newEnv }); 48 | 49 | await createEnvironmentCommand.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 50 | 51 | var logoutput = LogOutput.ToString(); 52 | Console.WriteLine(logoutput); 53 | JsonConvert.DeserializeObject(logoutput); 54 | logoutput.Should().Contain(newEnv); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ '**' ] 17 | schedule: 18 | - cron: '18 4 * * 6' 19 | 20 | jobs: 21 | analyze: 22 | name: Analyze 23 | runs-on: ubuntu-latest 24 | permissions: 25 | actions: read 26 | contents: read 27 | security-events: write 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | language: [ 'csharp' ] 33 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 34 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v2 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v2 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v2 68 | -------------------------------------------------------------------------------- /BuildAssets/create-octopuscli-linux-packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Package octopuscli from BINARIES_PATH, with executable permission and a /usr/bin symlink, into .deb and .rpm packages in PACKAGES_PATH. 3 | 4 | if [[ -z "$VERSION" ]]; then 5 | echo 'This script requires the environment variable VERSION - the version being packaged.' >&2 6 | exit 1 7 | fi 8 | if [[ -z "$BINARIES_PATH" ]]; then 9 | echo 'This script requires the environment variable BINARIES_PATH - the path containing binaries and related files to package.' >&2 10 | exit 1 11 | fi 12 | if [[ -z "$PACKAGES_PATH" ]]; then 13 | echo 'This script requires the environment variable PACKAGES_PATH - the path where packages should be written.' >&2 14 | exit 1 15 | fi 16 | 17 | which fpm >/dev/null || { 18 | echo 'This script requires fpm and related tools, and is intended to be run in the container "octopusdeploy/package-linux-docker".' >&2 19 | exit 1 20 | } 21 | if [[ ! -e /opt/linux-package-feeds ]]; then 22 | echo "This script requires 'linux-package-feeds' scripts, installed in '/opt/linux-package-feeds'." >&2 23 | echo "They come from https://github.com/OctopusDeploy/linux-package-feeds, distributed in TeamCity" >&2 24 | echo " via 'Infrastructure / Linux Package Feeds'. If running inside a Docker container, supply them using a volume mount." >&2 25 | exit 1 26 | fi 27 | 28 | 29 | # Create .deb and .rpm packages, with executable permission and a /usr/bin symlink, using a script from 'linux-package-feeds'. 30 | COMMAND_FILE=octo 31 | INSTALL_PATH=/opt/octopus/octopuscli 32 | PACKAGE_NAME=octopuscli 33 | PACKAGE_DESC='Command line tool for Octopus Deploy' 34 | FPM_OPTS=( 35 | --exclude 'opt/octopus/octopuscli/Octo' # Octo wrapper not needed here (it provides backwards compat in the .tar.gz direct download) 36 | ) 37 | FPM_DEB_OPTS=( 38 | --depends 'liblttng-ust0 | liblttng-ust1' 39 | --depends 'libcurl3 | libcurl4' 40 | --depends 'libssl1.0.0 | libssl1.0.2 | libssl1.1 | libssl3' 41 | --depends 'libkrb5-3' 42 | --depends 'zlib1g' 43 | --depends 'libicu52 | libicu55 | libicu57 | libicu60 | libicu63 | libicu66 | libicu67 | libicu70' 44 | ) 45 | # Note: Microsoft recommends dep 'lttng-ust' but it seems to be unavailable in CentOS 7, so we're omitting it for now. 46 | # As it's related to tracing, hopefully it will not be required for normal usage. 47 | FPM_RPM_OPTS=( 48 | --depends 'libcurl' 49 | --depends 'openssl-libs' 50 | --depends 'krb5-libs' 51 | --depends 'zlib' 52 | --depends 'libicu' 53 | ) 54 | source /opt/linux-package-feeds/create-linux-packages.sh || exit 55 | -------------------------------------------------------------------------------- /BuildAssets/repos/test-linux-package-from-feed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Test that octopuscli and tentacle can be installed from our APT and RPM feeds, and octo can list-environments. 3 | 4 | if [[ -z "$PUBLISH_LINUX_EXTERNAL" ]]; then 5 | echo 'This script requires the environment variable PUBLISH_LINUX_EXTERNAL - specify "true" to test the external public feed.' >&2 6 | exit 1 7 | fi 8 | OSRELID="$(. /etc/os-release && echo $ID)" 9 | if [[ "$OSRELID" == "rhel" && ( -z "$REDHAT_SUBSCRIPTION_USERNAME" || -z "$REDHAT_SUBSCRIPTION_PASSWORD" ) ]]; then 10 | echo -e 'This script requires the environment variables REDHAT_SUBSCRIPTION_USERNAME and REDHAT_SUBSCRIPTION_PASSWORD to register'\ 11 | '\nthe test system to install packages.' >&2 12 | exit 1 13 | fi 14 | if [[ -z "$OCTOPUS_CLI_SERVER" || -z "$OCTOPUS_CLI_API_KEY" || -z "$OCTOPUS_SPACE" || -z "$OCTOPUS_EXPECT_ENV" ]]; then 15 | echo -e 'This script requires the environment variables OCTOPUS_CLI_SERVER, OCTOPUS_CLI_API_KEY, OCTOPUS_SPACE, and'\ 16 | '\nOCTOPUS_EXPECT_ENV - specifying an Octopus server for testing "list-environments", an API key to access it, the'\ 17 | '\nSpace to search, and an environment name expected to be found there.' >&2 18 | exit 1 19 | fi 20 | 21 | if [[ ! -e /opt/linux-package-feeds ]]; then 22 | echo "This script requires 'linux-package-feeds' scripts, installed in '/opt/linux-package-feeds'." >&2 23 | echo "They come from https://github.com/OctopusDeploy/linux-package-feeds, distributed in TeamCity" >&2 24 | echo " via 'Infrastructure / Linux Package Feeds'. If running inside a Docker container, supply them using a volume mount." >&2 25 | exit 1 26 | fi 27 | 28 | 29 | # Install the packages from our package feed (with any needed docker config, system registration) using a script from 'linux-package-feeds'. 30 | export PKG_NAMES="octopuscli tentacle" 31 | bash /opt/linux-package-feeds/install-linux-feed-package.sh || exit 32 | 33 | if command -v dpkg > /dev/null; then 34 | echo Detected dpkg. Installing ca-certificates to support octo HTTPS communication. 35 | export DEBIAN_FRONTEND=noninteractive 36 | apt-get --no-install-recommends --yes install ca-certificates >/dev/null || exit 37 | fi 38 | 39 | echo Testing octo. 40 | octo version || exit 41 | OCTO_RESULT="$(octo list-environments --space="$OCTOPUS_SPACE")" || { echo "$OCTO_RESULT"; exit 1; } 42 | echo "$OCTO_RESULT" | grep "$OCTOPUS_EXPECT_ENV" || { echo "Expected environment not found: $OCTOPUS_EXPECT_ENV." >&2; exit 1; } 43 | 44 | echo Testing tentacle. 45 | /opt/octopus/tentacle/Tentacle version || exit 46 | echo 47 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/IOctopusFileSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading; 5 | 6 | namespace Octopus.Cli.Util 7 | { 8 | public interface IOctopusFileSystem 9 | { 10 | bool FileExists(string path); 11 | bool DirectoryExists(string path); 12 | bool DirectoryIsEmpty(string path); 13 | void DeleteFile(string path); 14 | void DeleteFile(string path, DeletionOptions options); 15 | void DeleteDirectory(string path); 16 | void DeleteDirectory(string path, DeletionOptions options); 17 | IEnumerable EnumerateDirectories(string parentDirectoryPath); 18 | IEnumerable EnumerateDirectoriesRecursively(string parentDirectoryPath); 19 | IEnumerable EnumerateFiles(string parentDirectoryPath, params string[] searchPatterns); 20 | IEnumerable EnumerateFilesRecursively(string parentDirectoryPath, params string[] searchPatterns); 21 | long GetFileSize(string path); 22 | string ReadFile(string path); 23 | void AppendToFile(string path, string contents); 24 | void OverwriteFile(string path, string contents); 25 | Stream OpenFile(string path, FileAccess access = FileAccess.ReadWrite, FileShare share = FileShare.Read); 26 | Stream OpenFile(string path, FileMode mode = FileMode.OpenOrCreate, FileAccess access = FileAccess.ReadWrite, FileShare share = FileShare.Read); 27 | Stream CreateTemporaryFile(string extension, out string path); 28 | string CreateTemporaryDirectory(); 29 | void CopyDirectory(string sourceDirectory, string targetDirectory, int overwriteFileRetryAttempts = 3); 30 | void CopyDirectory(string sourceDirectory, string targetDirectory, CancellationToken cancel, int overwriteFileRetryAttempts = 3); 31 | ReplaceStatus CopyFile(string sourceFile, string destinationFile, int overwriteFileRetryAttempts = 3); 32 | void EnsureDirectoryExists(string directoryPath); 33 | string GetFullPath(string relativeOrAbsoluteFilePath); 34 | void OverwriteAndDelete(string originalFile, string temporaryReplacement); 35 | void WriteAllBytes(string filePath, byte[] data); 36 | string RemoveInvalidFileNameChars(string path); 37 | void MoveFile(string sourceFile, string destinationFile); 38 | ReplaceStatus Replace(string path, Stream stream, int overwriteRetryAttempts = 3); 39 | bool EqualHash(Stream first, Stream second); 40 | string ReadAllText(string scriptFile); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Octopus.Cli.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1;net6.0 5 | Octopus Deploy 6 | Octopus Deploy Pty Ltd 7 | Apache-2.0 8 | icon.png 9 | git 10 | https://github.com/OctopusDeploy/OctopusCli 11 | Octopus Deploy is an automated release management tool for modern developers and DevOps teams. 12 | 13 | This package contains the .NET CLI library for interacting with the HTTP API in Octopus. 14 | true 15 | 16 | 17 | 18 | NU5104 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Package/NuGetPackageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using NuGet.Packaging; 6 | using Octopus.Cli.Util; 7 | using Octopus.CommandLine; 8 | using Octopus.CommandLine.Commands; 9 | 10 | namespace Octopus.Cli.Commands.Package 11 | { 12 | public class NuGetPackageBuilder : IPackageBuilder 13 | { 14 | readonly IOctopusFileSystem fileSystem; 15 | readonly ICommandOutputProvider commandOutputProvider; 16 | readonly List files; 17 | 18 | public NuGetPackageBuilder(IOctopusFileSystem fileSystem, ICommandOutputProvider commandOutputProvider) 19 | { 20 | this.fileSystem = fileSystem; 21 | this.commandOutputProvider = commandOutputProvider; 22 | files = new List(); 23 | } 24 | 25 | public string[] Files => files.ToArray(); 26 | public string PackageFormat => "nupkg"; 27 | 28 | public void BuildPackage(string basePath, 29 | IList includes, 30 | ManifestMetadata metadata, 31 | string outFolder, 32 | bool overwrite, 33 | bool verboseInfo) 34 | { 35 | var nugetPkgBuilder = new PackageBuilder(); 36 | 37 | var manifestFiles = includes.Select(i => new ManifestFile { Source = i }).ToList(); 38 | nugetPkgBuilder.PopulateFiles(basePath, manifestFiles); 39 | nugetPkgBuilder.Populate(metadata); 40 | 41 | if (verboseInfo) 42 | foreach (var file in nugetPkgBuilder.Files) 43 | commandOutputProvider.Information($"Added file: {file.Path}"); 44 | files.AddRange(nugetPkgBuilder.Files.Select(x => x.Path).ToArray()); 45 | 46 | var filename = $"{metadata.Id}.{metadata.Version}.nupkg"; 47 | var output = Path.Combine(outFolder, filename); 48 | 49 | if (fileSystem.FileExists(output) && !overwrite) 50 | throw new CommandException("The package file already exists and --overwrite was not specified"); 51 | 52 | commandOutputProvider.Information("Saving {Filename} to {OutFolder}...", filename, outFolder); 53 | 54 | fileSystem.EnsureDirectoryExists(outFolder); 55 | 56 | using (var outStream = fileSystem.OpenFile(output, FileMode.Create)) 57 | { 58 | nugetPkgBuilder.Save(outStream); 59 | } 60 | } 61 | 62 | public void SetCompression(PackageCompressionLevel level) 63 | { 64 | // does nothing 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Package/DeletePackageCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Octopus.Cli.Repositories; 4 | using Octopus.Cli.Util; 5 | using Octopus.Client; 6 | using Octopus.CommandLine; 7 | using Octopus.CommandLine.Commands; 8 | 9 | namespace Octopus.Cli.Commands.Package 10 | { 11 | [Command("delete-package", Description = "Deletes a package from the built-in NuGet repository in an Octopus Server.")] 12 | public class DeletePackageCommand : ApiCommand, ISupportFormattedOutput 13 | { 14 | object result; 15 | 16 | public DeletePackageCommand(IOctopusAsyncRepositoryFactory repositoryFactory,IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 17 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 18 | { 19 | var options = Options.For("Deletion"); 20 | options.Add("packageId=", "Id of the package.", v => PackageId = v); 21 | options.Add("version=", "Version number of the package.", v => PackageVersion = v); 22 | } 23 | 24 | public string PackageId { get; set; } 25 | public string PackageVersion { get; set; } 26 | 27 | public async Task Request() 28 | { 29 | if (string.IsNullOrWhiteSpace(PackageId)) throw new CommandException("Please specify a package id using the parameter: --packageId=XYZ"); 30 | if (string.IsNullOrWhiteSpace(PackageVersion)) throw new CommandException("Please specify a package version using the parameter: --version=1.0.0"); 31 | 32 | commandOutputProvider.Debug("Finding package: {PackageId:l} with version {Version:l}", PackageId, PackageVersion); 33 | 34 | // If package == null client throws 404 caught in CliProgram 35 | var package = await Repository.BuiltInPackageRepository.GetPackage(PackageId, PackageVersion).ConfigureAwait(false); 36 | 37 | commandOutputProvider.Debug("Found package with id: {PackageId:l}... Deleting package", package.PackageId); 38 | await Repository.BuiltInPackageRepository.DeletePackage(package).ConfigureAwait(false); 39 | result = new 40 | { 41 | Status = "Success", 42 | Package = new {PackageId, Version = PackageVersion} 43 | }; 44 | } 45 | 46 | public void PrintDefaultOutput() 47 | { 48 | commandOutputProvider.Information("Package deleted"); 49 | } 50 | 51 | public void PrintJsonOutput() 52 | { 53 | commandOutputProvider.Json(result); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/FeatureDetectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Octopus.Client; 4 | using Octopus.Client.Model; 5 | 6 | namespace Octopus.Cli.Util 7 | { 8 | public static class FeatureDetectionExtensions 9 | { 10 | public static async Task SupportsChannels(this IOctopusAsyncRepository repository) 11 | { 12 | var hasChannelLink = await repository.HasLink("Channels").ConfigureAwait(false); 13 | if (!hasChannelLink) 14 | // When default space is off and SpaceId is not provided, we check if it is in post space world, as channels are always available in spaces 15 | return await repository.HasLink("SpaceHome").ConfigureAwait(false); 16 | 17 | return true; 18 | } 19 | 20 | public static async Task SupportsTenants(this IOctopusAsyncRepository repository) 21 | { 22 | var hasTenantLink = await repository.HasLink("Tenants").ConfigureAwait(false); 23 | if (!hasTenantLink) 24 | // When default space is off and SpaceId is not provided, we check if it is in post space world, as tenants are always available in spaces 25 | return await repository.HasLink("SpaceHome").ConfigureAwait(false); 26 | 27 | return true; 28 | } 29 | 30 | public static bool UsePostForChannelVersionRuleTest(this RootResource source) 31 | { 32 | // Assume octo 3.4 should use the OctopusServer 3.4 POST, otherwise if we're certain this is an older Octopus Server use the GET method 33 | return source == null || 34 | !SemanticVersion.TryParse(source.Version, out var octopusServerVersion) || 35 | octopusServerVersion >= new SemanticVersion("3.4"); 36 | } 37 | 38 | public static bool HasProjectDeploymentSettingsSeparation(this RootResource source) 39 | { 40 | // The separation of Projects from DeploymentSettings was exposed from 2021.2 onwards 41 | return source == null || 42 | !SemanticVersion.TryParse(source.Version, out var octopusServerVersion) || 43 | octopusServerVersion >= new SemanticVersion("2021.2"); 44 | } 45 | 46 | public static bool UseIdsForConfigAsCode(this RootResource source) 47 | { 48 | // The separation of Projects from DeploymentSettings was exposed from 2021.2 onwards 49 | return source == null || 50 | !SemanticVersion.TryParse(source.Version, out var octopusServerVersion) || 51 | octopusServerVersion >= new SemanticVersion("2022.3.4517"); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Project/DeleteProjectCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Octopus.Cli.Repositories; 4 | using Octopus.Cli.Util; 5 | using Octopus.Client; 6 | using Octopus.Client.Model; 7 | using Octopus.CommandLine; 8 | using Octopus.CommandLine.Commands; 9 | using Octopus.Cli.Infrastructure; 10 | 11 | namespace Octopus.Cli.Commands.Project 12 | { 13 | [Command("delete-project", Description = "Deletes a project.")] 14 | public class DeleteProjectCommand : ApiCommand, ISupportFormattedOutput 15 | { 16 | ProjectResource project; 17 | bool ProjectDeleted; 18 | 19 | public DeleteProjectCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 20 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 21 | { 22 | var options = Options.For("Project deletion"); 23 | options.Add("name=", "The name of the project.", v => ProjectName = v); 24 | } 25 | 26 | public string ProjectName { get; set; } 27 | 28 | public async Task Request() 29 | { 30 | if (string.IsNullOrWhiteSpace(ProjectName)) throw new CommandException("Please specify a project name using the parameter: --name=XYZ"); 31 | 32 | commandOutputProvider.Information("Finding project: {Project:l}", ProjectName); 33 | 34 | project = await Repository.Projects.FindByName(ProjectName).ConfigureAwait(false); 35 | if (project == null) 36 | { 37 | throw new CouldNotFindException("project"); 38 | } 39 | 40 | commandOutputProvider.Information("Deleting project: {Project:l}", ProjectName); 41 | 42 | try 43 | { 44 | await Repository.Projects.Delete(project).ConfigureAwait(false); 45 | } 46 | catch (Exception ex) 47 | { 48 | commandOutputProvider.Error("Error deleting project {Project:l}: {Exception:l}", project.Name, ex.Message); 49 | throw; 50 | } 51 | ProjectDeleted = true; 52 | 53 | } 54 | 55 | public void PrintDefaultOutput() 56 | { 57 | commandOutputProvider.Information("Project deleted. ID: {Id:l}", project.Id); 58 | } 59 | 60 | public void PrintJsonOutput() 61 | { 62 | commandOutputProvider.Json(new 63 | { 64 | Project = new 65 | { 66 | project.Id, 67 | project.Name, 68 | ProjectDeleted 69 | } 70 | }); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/WorkerPool/CreateWorkerPoolCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Octopus.Cli.Repositories; 4 | using Octopus.Cli.Util; 5 | using Octopus.Client; 6 | using Octopus.Client.Model; 7 | using Octopus.CommandLine; 8 | using Octopus.CommandLine.Commands; 9 | 10 | namespace Octopus.Cli.Commands.WorkerPool 11 | { 12 | [Command("create-workerpool", Description = "Creates a pool for workers.")] 13 | public class CreateWorkerPoolCommand : ApiCommand, ISupportFormattedOutput 14 | { 15 | WorkerPoolResource pool; 16 | 17 | public CreateWorkerPoolCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 18 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 19 | { 20 | var options = Options.For("WorkerPool creation"); 21 | options.Add("name=", "The name of the worker pool.", v => WorkerPoolName = v); 22 | options.Add("ignoreIfExists", "If the pool already exists, an error will be returned. Set this flag to ignore the error.", v => IgnoreIfExists = true); 23 | } 24 | 25 | public string WorkerPoolName { get; set; } 26 | public bool IgnoreIfExists { get; set; } 27 | 28 | public async Task Request() 29 | { 30 | if (string.IsNullOrWhiteSpace(WorkerPoolName)) throw new CommandException("Please specify a worker pool name using the parameter: --name=XYZ"); 31 | 32 | pool = await Repository.WorkerPools.FindByName(WorkerPoolName).ConfigureAwait(false); 33 | if (pool != null) 34 | { 35 | if (IgnoreIfExists) 36 | { 37 | commandOutputProvider.Information("The worker pool {WorkerPool:l} (ID {Id:l}) already exists", pool.Name, pool.Id); 38 | return; 39 | } 40 | 41 | throw new CommandException("The worker pool " + pool.Name + " (ID " + pool.Id + ") already exists"); 42 | } 43 | 44 | commandOutputProvider.Information("Creating worker pool: {WorkerPool:l}", WorkerPoolName); 45 | pool = await Repository.WorkerPools.Create(new WorkerPoolResource { Name = WorkerPoolName }).ConfigureAwait(false); 46 | } 47 | 48 | public void PrintDefaultOutput() 49 | { 50 | commandOutputProvider.Information("WorkerPool created. ID: {Id:l}", pool.Id); 51 | } 52 | 53 | public void PrintJsonOutput() 54 | { 55 | commandOutputProvider.Json(new 56 | { 57 | pool.Id, 58 | pool.Name 59 | }); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Environment/CreateEnvironmentCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Octopus.Cli.Repositories; 4 | using Octopus.Cli.Util; 5 | using Octopus.Client; 6 | using Octopus.Client.Model; 7 | using Octopus.CommandLine; 8 | using Octopus.CommandLine.Commands; 9 | 10 | namespace Octopus.Cli.Commands.Environment 11 | { 12 | [Command("create-environment", Description = "Creates a deployment environment.")] 13 | public class CreateEnvironmentCommand : ApiCommand, ISupportFormattedOutput 14 | { 15 | EnvironmentResource env; 16 | 17 | public CreateEnvironmentCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 18 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 19 | { 20 | var options = Options.For("Environment creation"); 21 | options.Add("name=", "The name of the environment.", v => EnvironmentName = v); 22 | options.Add("ignoreIfExists", "If the environment already exists, an error will be returned. Set this flag to ignore the error.", v => IgnoreIfExists = true); 23 | } 24 | 25 | public string EnvironmentName { get; set; } 26 | public bool IgnoreIfExists { get; set; } 27 | 28 | public async Task Request() 29 | { 30 | if (string.IsNullOrWhiteSpace(EnvironmentName)) throw new CommandException("Please specify an environment name using the parameter: --name=XYZ"); 31 | 32 | env = await Repository.Environments.FindByName(EnvironmentName).ConfigureAwait(false); 33 | if (env != null) 34 | { 35 | if (IgnoreIfExists) 36 | { 37 | commandOutputProvider.Information("The environment {Environment:l} (ID {Id:l}) already exists", env.Name, env.Id); 38 | return; 39 | } 40 | 41 | throw new CommandException("The environment " + env.Name + " (ID " + env.Id + ") already exists"); 42 | } 43 | 44 | commandOutputProvider.Information("Creating environment: {Environment:l}", EnvironmentName); 45 | env = await Repository.Environments.Create(new EnvironmentResource { Name = EnvironmentName }).ConfigureAwait(false); 46 | } 47 | 48 | public void PrintDefaultOutput() 49 | { 50 | commandOutputProvider.Information("Environment created. ID: {Id:l}", env.Id); 51 | } 52 | 53 | public void PrintJsonOutput() 54 | { 55 | commandOutputProvider.Json(new 56 | { 57 | env.Id, 58 | env.Name 59 | }); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/DeletePackageCommandFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using FluentAssertions; 4 | using NSubstitute; 5 | using NUnit.Framework; 6 | using Octopus.Cli.Commands.Package; 7 | using Octopus.Client.Model; 8 | using Octopus.CommandLine.Commands; 9 | 10 | namespace Octo.Tests.Commands 11 | { 12 | public class DeletePackageCommandFixture : ApiCommandFixtureBase 13 | { 14 | DeletePackageCommand deletePackageCommand; 15 | string packageId; 16 | string packageVersion; 17 | 18 | [SetUp] 19 | public void Setup() 20 | { 21 | deletePackageCommand = new DeletePackageCommand(RepositoryFactory, 22 | FileSystem, 23 | ClientFactory, 24 | CommandOutputProvider); 25 | 26 | packageId = "TestPackage"; 27 | packageVersion = "1.0.0"; 28 | } 29 | 30 | [Test] 31 | public async Task DefaultOutput_ShouldDeleteTheGivenPackage() 32 | { 33 | deletePackageCommand.PackageId = packageId; 34 | deletePackageCommand.PackageVersion = packageVersion; 35 | Repository.BuiltInPackageRepository.GetPackage(packageId, packageVersion) 36 | .Returns(new PackageFromBuiltInFeedResource { PackageId = packageId, Version = packageVersion}); 37 | 38 | await deletePackageCommand.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 39 | 40 | LogLines.Should().Contain("Package deleted"); 41 | 42 | deletePackageCommand.PrintJsonOutput(); 43 | 44 | var logOutput = LogOutput.ToString(); 45 | Assert.True(logOutput.Contains("\"Status\": \"Success\"")); 46 | Assert.True(logOutput.Contains($"\"PackageId\": \"{packageId}\"")); 47 | Assert.True(logOutput.Contains($"\"Version\": \"{packageVersion}\"")); 48 | } 49 | 50 | [Test] 51 | public void CommandException_ShouldNotSearchForPackageWhenThereIsNoPackageId() 52 | { 53 | Func exec = () => deletePackageCommand.Execute(CommandLineArgs.ToArray()); 54 | exec.ShouldThrow() 55 | .WithMessage("Please specify a package id using the parameter: --packageId=XYZ"); 56 | } 57 | 58 | [Test] 59 | public void CommandException_ShouldNotSearchForPackageWhenThereIsNoPackageVersion() 60 | { 61 | deletePackageCommand.PackageId = "TestPackage"; 62 | Func exec = () => deletePackageCommand.Execute(CommandLineArgs.ToArray()); 63 | exec.ShouldThrow() 64 | .WithMessage("Please specify a package version using the parameter: --version=1.0.0"); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/Humanize.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Octopus.Cli.Util 6 | { 7 | public static class Humanize 8 | { 9 | public static string FriendlyDuration(this TimeSpan time) 10 | { 11 | if (time.TotalSeconds < 1) return "less than a second"; 12 | if (time.TotalMinutes < 1) return Format(time.TotalSeconds, "second"); 13 | if (time.TotalHours < 1) return Format(time.TotalMinutes, "minute"); 14 | if (time.TotalDays < 1) return Format(time.TotalHours, "hour"); 15 | return Format(time.TotalDays, "day"); 16 | } 17 | 18 | static string Format(double totalUnits, string unit) 19 | { 20 | var floor = (int)Math.Round(totalUnits); 21 | return string.Format("{0:n0} {1}", floor, unit.Plural(floor)); 22 | } 23 | 24 | public static string Plural(this string simpleNoun, int count = 2) 25 | { 26 | if (simpleNoun == null) throw new ArgumentNullException(nameof(simpleNoun)); 27 | if (count == 1) return simpleNoun; 28 | 29 | if (simpleNoun.EndsWith("ay") || 30 | simpleNoun.EndsWith("ey") || 31 | simpleNoun.EndsWith("iy") || 32 | simpleNoun.EndsWith("oy") || 33 | simpleNoun.EndsWith("uy")) 34 | return simpleNoun + "s"; 35 | 36 | if (simpleNoun.EndsWith("y")) 37 | return simpleNoun.Substring(0, simpleNoun.Length - 1) + "ies"; 38 | 39 | if (simpleNoun.EndsWith("ss")) 40 | return simpleNoun + "es"; 41 | 42 | if (simpleNoun.EndsWith("s")) 43 | return simpleNoun; 44 | 45 | return simpleNoun + "s"; 46 | } 47 | 48 | public static string ReadableJoin(this IEnumerable list, string junction = "and") 49 | { 50 | if (list == null) throw new ArgumentNullException(nameof(list)); 51 | 52 | var result = new StringBuilder(); 53 | object prev = null; 54 | 55 | string separator = "", final = ""; 56 | var enumerator = list.GetEnumerator(); 57 | while (enumerator.MoveNext()) 58 | { 59 | if (prev != null) 60 | { 61 | result.Append(separator); 62 | result.Append(prev); 63 | separator = ", "; 64 | final = " " + junction + " "; 65 | } 66 | 67 | prev = enumerator.Current; 68 | } 69 | 70 | if (prev != null) 71 | { 72 | result.Append(final); 73 | result.Append(prev); 74 | } 75 | 76 | return result.ToString(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/ResourceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Octopus.Client; 6 | using Octopus.Client.Model; 7 | 8 | namespace Octopus.Cli.Util 9 | { 10 | public static class ResourceCollectionExtensions 11 | { 12 | const string PageNext = "Page.Next"; 13 | 14 | public static bool HasNextPage(this ResourceCollection source) 15 | { 16 | return source.HasLink(PageNext); 17 | } 18 | 19 | public static string NextPageLink(this ResourceCollection source) 20 | { 21 | return source.Link(PageNext); 22 | } 23 | 24 | public static async Task> GetAllPages(this ResourceCollection source, IOctopusAsyncRepository repository) 25 | { 26 | var items = source.Items.ToList(); 27 | 28 | while (source.HasNextPage()) 29 | { 30 | source = await repository.Client.List(source.NextPageLink()).ConfigureAwait(false); 31 | items.AddRange(source.Items); 32 | } 33 | 34 | return items; 35 | } 36 | 37 | public static async Task Paginate(this ResourceCollection source, IOctopusAsyncRepository repository, Func, bool> getNextPage) 38 | { 39 | while (getNextPage(source) && source.Items.Count > 0 && source.HasNextPage()) 40 | source = await repository.Client.List(source.NextPageLink()).ConfigureAwait(false); 41 | } 42 | 43 | public static async Task FindOne(this ResourceCollection source, IOctopusAsyncRepository repository, Func search) 44 | { 45 | var resource = default(TResource); 46 | await source.Paginate(repository, 47 | page => 48 | { 49 | resource = page.Items.FirstOrDefault(search); 50 | return resource == null; 51 | }) 52 | .ConfigureAwait(false); 53 | return resource; 54 | } 55 | 56 | public static async Task> FindMany(this ResourceCollection source, IOctopusAsyncRepository repository, Func search) 57 | { 58 | var resources = new List(); 59 | await source.Paginate(repository, 60 | page => 61 | { 62 | resources.AddRange(page.Items.Where(search)); 63 | return true; 64 | }) 65 | .ConfigureAwait(false); 66 | return resources; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [9.1.7](https://github.com/OctopusDeploy/OctopusCLI/compare/v9.1.6...v9.1.7) (2022-08-24) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * Corrected null reference exception when release version wasn't specified ([#239](https://github.com/OctopusDeploy/OctopusCLI/issues/239)) ([e8ca274](https://github.com/OctopusDeploy/OctopusCLI/commit/e8ca2749104c74c7750a98f0cd65c3cc915147d2)) 9 | 10 | ## [9.1.6](https://github.com/OctopusDeploy/OctopusCLI/compare/v9.1.6...v9.1.6) (2022-08-24) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * Validate that the release version passed in does not contain whitespaces ([#227](https://github.com/OctopusDeploy/OctopusCLI/issues/227)) ([f92d2fe](https://github.com/OctopusDeploy/OctopusCLI/commit/f92d2fe6d3a75072ccb263fa19c6941d48d83e47)) 16 | 17 | ## [9.1.5](https://github.com/OctopusDeploy/OctopusCLI/compare/v9.1.4...v9.1.5) (2022-08-19) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * update dependencies ([9452189](https://github.com/OctopusDeploy/OctopusCLI/commit/94521896a522dfcbc3b1a1cfc4e5537a7745d8f6)) 23 | 24 | ## [9.1.4](https://github.com/OctopusDeploy/OctopusCLI/compare/v9.1.3...v9.1.4) (2022-08-18) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * Correct the build process to push release notes through for the public website ([8fe03ba](https://github.com/OctopusDeploy/OctopusCLI/commit/8fe03bada346cfcedd9fd2f618fb41ac5f9ecddd)) 30 | 31 | ## [9.1.3](https://github.com/OctopusDeploy/OctopusCLI/compare/v9.1.2...v9.1.3) (2022-07-14) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * Corrected the release-please setup for managing releases ([3707e96](https://github.com/OctopusDeploy/OctopusCLI/commit/3707e96fab360565c082ec8f13f0eda474f6a077)) 37 | * Update deployment process to use different user for homebrew publish ([#212](https://github.com/OctopusDeploy/OctopusCLI/issues/212)) ([3707e96](https://github.com/OctopusDeploy/OctopusCLI/commit/3707e96fab360565c082ec8f13f0eda474f6a077)) 38 | 39 | ## [9.1.2](https://github.com/OctopusDeploy/OctopusCLI/compare/v9.1.1...v9.1.2) (2022-07-13) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * [7660](https://github.com/OctopusDeploy/Issues/issues/7660) Update release plan builder to use IDs in new versions of Octopus ([#208](https://github.com/OctopusDeploy/OctopusCLI/pull/208)) ([28dbe04](https://github.com/OctopusDeploy/OctopusCLI/commit/28dbe04c6c2718d95089314d49dc1b2d2bcdacbc)) 45 | 46 | ## [9.1.1](https://github.com/OctopusDeploy/OctopusCLI/compare/v9.1.0...v9.1.1) (2022-06-17) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * corrected a build trigger for the release please workflow ([#199](https://github.com/OctopusDeploy/OctopusCLI/issues/199)) ([e98e3c4](https://github.com/OctopusDeploy/OctopusCLI/commit/e98e3c49d3ee39617238a30ff7f09915250bcf4e)) 52 | 53 | ## [9.1.0](https://github.com/OctopusDeploy/OctopusCLI/compare/9.0.0...v9.1.0) (2022-06-17) 54 | 55 | 56 | ### Features 57 | 58 | * added commands to delete a project and to disable a project ([#177](https://github.com/OctopusDeploy/OctopusCLI/issues/177)) ([2bdb94c](https://github.com/OctopusDeploy/OctopusCLI/commit/2bdb94c62ff89f2b220990069100a163da13249d)) 59 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/ChannelVersionRuleTesterFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using FluentAssertions; 4 | using NSubstitute; 5 | using NUnit.Framework; 6 | using Octopus.Cli.Commands.Releases; 7 | using Octopus.Cli.Model; 8 | using Octopus.Client; 9 | using Octopus.Client.Model; 10 | 11 | namespace Octo.Tests.Commands 12 | { 13 | [TestFixture] 14 | public class ChannelVersionRuleTesterFixture 15 | { 16 | const string FeedId = "feeds-builtin"; 17 | 18 | [Test] 19 | public async Task NoRuleShouldReturnNullResult() 20 | { 21 | var repo = Substitute.For(); 22 | 23 | var result = await new ChannelVersionRuleTester().Test(repo, null, "1.0.0", FeedId); 24 | result.IsNull.Should().BeTrue(); 25 | 26 | result.SatisfiesVersionRange.Should().BeTrue(); 27 | result.SatisfiesPreReleaseTag.Should().BeTrue(); 28 | 29 | await repo.Client.DidNotReceive().Post(Arg.Any(), Arg.Any()); 30 | } 31 | 32 | [Test] 33 | public async Task NoPackageVersionShouldReturnFailedResult() 34 | { 35 | var repo = Substitute.For(); 36 | 37 | var rule = new ChannelVersionRuleResource 38 | { 39 | VersionRange = "[1.0,)", 40 | Tag = "$^" 41 | }; 42 | var result = await new ChannelVersionRuleTester().Test(repo, rule, "", FeedId); 43 | result.IsNull.Should().BeFalse(); 44 | 45 | result.SatisfiesVersionRange.Should().BeFalse(); 46 | result.SatisfiesPreReleaseTag.Should().BeFalse(); 47 | 48 | await repo.Client.DidNotReceive().Post(Arg.Any(), Arg.Any()); 49 | } 50 | 51 | [Test] 52 | public async Task PackageVersionShouldReturnSuccessfulResult() 53 | { 54 | var expectedTestResult = new ChannelVersionRuleTestResult 55 | { 56 | SatisfiesVersionRange = true, 57 | SatisfiesPreReleaseTag = true 58 | }; 59 | var repo = Substitute.For(); 60 | repo.Client.Post(Arg.Any(), Arg.Any()) 61 | .Returns(Task.FromResult(expectedTestResult)); 62 | 63 | var rule = new ChannelVersionRuleResource 64 | { 65 | VersionRange = "[1.0,)", 66 | Tag = "$^" 67 | }; 68 | var result = await new ChannelVersionRuleTester().Test(repo, rule, "1.0.0", FeedId); 69 | result.IsNull.Should().BeFalse(); 70 | 71 | result.SatisfiesVersionRange.Should().BeTrue(); 72 | result.SatisfiesPreReleaseTag.Should().BeTrue(); 73 | 74 | await repo.Client.Received(1).Post(Arg.Any(), Arg.Any()); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/CommandConventionFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Autofac; 6 | using FluentAssertions; 7 | using NUnit.Framework; 8 | using Octopus.Cli.Commands; 9 | using Octopus.CommandLine; 10 | using Octopus.CommandLine.Commands; 11 | 12 | namespace Octo.Tests.Commands 13 | { 14 | [TestFixture] 15 | public class CommandConventionFixture 16 | { 17 | [TestCaseSource(nameof(Commands))] 18 | public void AllCommandsShouldBeDecoratedWithTheCommandAttribute(Type commaType) 19 | { 20 | commaType.GetCustomAttribute() 21 | .Should() 22 | .NotBeNull($"The following type '{commaType.Name}' implements {nameof(ICommand)} " + 23 | $"but is not decorated with a {nameof(CommandAttribute)}, which is required for the program to detect it as an available command."); 24 | } 25 | 26 | [TestCaseSource(nameof(SubclassesOfApiCommand))] 27 | public void AllSubclassesOfApiCommandShouldEitherOverrideExecuteMethodOrImplementISupportFormattedOutputInterface(Type commandType) 28 | { 29 | var isImplementedWithCorrectInterface = typeof(ISupportFormattedOutput).IsAssignableFrom(commandType); 30 | 31 | const string methodName = "Execute"; 32 | var isMethodOverridenCorrectly = commandType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic) 33 | .Any(m => m.Name == methodName && 34 | m.GetBaseDefinition()?.IsVirtual == true && 35 | m.GetBaseDefinition()?.IsFamily == true && 36 | m.IsFamily && 37 | m.GetBaseDefinition()?.DeclaringType != m.DeclaringType); 38 | 39 | var isImplementedWrongly = !isImplementedWithCorrectInterface && !isMethodOverridenCorrectly || 40 | isImplementedWithCorrectInterface && isMethodOverridenCorrectly; 41 | 42 | isImplementedWrongly.Should().BeFalse($"The following type '{commandType.Name}' is a subclass of '{nameof(ApiCommand)}', it must only either overrides virtual '{methodName}' method Or implements {nameof(ISupportFormattedOutput)} interface."); 43 | } 44 | 45 | static IEnumerable Commands() 46 | { 47 | return CommandTypes() 48 | .Select(t => new TestCaseData(t).SetName(t.Name)); 49 | } 50 | 51 | static IEnumerable SubclassesOfApiCommand() 52 | { 53 | return CommandTypes() 54 | .Where(t => t.IsSubclassOf(typeof(ApiCommand))) 55 | .Select(t => new TestCaseData(t).SetName(t.Name)); 56 | } 57 | 58 | static IEnumerable CommandTypes() 59 | { 60 | return Assembly.GetAssembly(typeof(ICommand)) 61 | .GetTypes() 62 | .Where(t => t.IsAssignableTo() && 63 | !t.IsAbstract && 64 | !t.IsInterface); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Project/DisableProjectCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Octopus.Cli.Repositories; 4 | using Octopus.Cli.Util; 5 | using Octopus.Client; 6 | using Octopus.Client.Model; 7 | using Octopus.CommandLine; 8 | using Octopus.CommandLine.Commands; 9 | using Octopus.Cli.Infrastructure; 10 | 11 | namespace Octopus.Cli.Commands.Project 12 | { 13 | [Command("disable-project", Description = "Disables a project.")] 14 | public class DisableProjectCommand : ApiCommand, ISupportFormattedOutput 15 | { 16 | ProjectResource project; 17 | 18 | public DisableProjectCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 19 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 20 | { 21 | var options = Options.For("Project disablement"); 22 | options.Add("name=", "The name of the project.", v => ProjectName = v); 23 | } 24 | 25 | public string ProjectName { get; set; } 26 | 27 | public async Task Request() 28 | { 29 | if (string.IsNullOrWhiteSpace(ProjectName)) throw new CommandException("Please specify a project name using the parameter: --name=XYZ"); 30 | 31 | commandOutputProvider.Information("Finding project: {Project:l}", ProjectName); 32 | 33 | // Get project 34 | project = await Repository.Projects.FindByName(ProjectName).ConfigureAwait(false); 35 | 36 | // Check that is project exists and isn't already disabled 37 | if (project == null) 38 | { 39 | throw new CouldNotFindException("project"); 40 | } 41 | else if (project.IsDisabled == true) 42 | { 43 | commandOutputProvider.Information("The project {Project:l} is already disabled.", project.Name); 44 | return; 45 | } 46 | 47 | // Disable project 48 | commandOutputProvider.Information("Disabling project: {Project:l}", ProjectName); 49 | try 50 | { 51 | project.IsDisabled = true; 52 | await Repository.Projects.Modify(project).ConfigureAwait(false); 53 | } 54 | catch (Exception ex) 55 | { 56 | commandOutputProvider.Error("Error disabling project {Project:l}: {Exception:l}", project.Name, ex.Message); 57 | } 58 | 59 | } 60 | 61 | public void PrintDefaultOutput() 62 | { 63 | commandOutputProvider.Information("Project {Project:l} disabled. ID: {Id:l}", project.Name, project.Id); 64 | } 65 | 66 | public void PrintJsonOutput() 67 | { 68 | commandOutputProvider.Json(new 69 | { 70 | Project = new 71 | { 72 | project.Id, 73 | project.Name, 74 | project.IsDisabled 75 | } 76 | }); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /Dockerfiles/Readme.md: -------------------------------------------------------------------------------- 1 | # octopusdeploy/octo 2 | A docker wrapped version of the popular Octopus CLI, [octo](https://octopus.com/docs/api-and-integration/octo.exe-command-line) 3 | 4 | ## Platforms 5 | Images are currently available for 6 | - [linux/amd64](alpine/Dockerfile) - based on alpine 7 | 8 | 9 | ## Command Options 10 | Arguments passed to the container will be passed directly to the `octo` tool internally. For the full list of commands and parameters [read our docs](https://octopus.com/docs/api-and-integration/octo.exe-command-line). 11 | 12 | ### Example Usage 13 | #### Help 14 | ``` 15 | >$ docker run --rm octopusdeploy/octo help 16 | 17 | Usage: octo [] 18 | 19 | Where is one of: 20 | 21 | clean-environment 22 | Cleans all Offline Machines from an Environment 23 | ... 24 | ... 25 | ``` 26 | #### Print Version 27 | ``` 28 | >$ docker run --rm octopusdeploy/octo version 29 | 30 | 4.31.1 31 | ``` 32 | 33 | ### Pack 34 | Packing and pushing requires providing a volume mount so that the container process is able to access the required files. Internally, the working directory is set to `/src` so the simplest approach is to mount this point to the current working directory, and then pass the command arguments relative to this location. 35 | 36 | ``` 37 | >$ docker run --rm -v $(pwd):/src octopusdeploy/octo pack --id=AcmeWeb --version=3.1.4 --basePath=WebApp --overwrite 38 | 39 | Packing AcmeWeb version "3.1.4"... 40 | Saving "AcmeWeb.3.1.4.nupkg" to "/src"... 41 | Done. 42 | ``` 43 | _(When running on windows, replace the file mount `$(pwd):/src` with `"$(Convert-Path .):C:\src"`)_ 44 | 45 | 46 | ### Push 47 | ``` 48 | >$ docker run --rm -v $(pwd):/src octopusdeploy/octo push --package AcmeWeb.3.1.4.nupkg --replace-existing --server https://myoctopus.acme.com --apiKey $env:apikey 49 | 50 | Octopus Deploy Command Line Tool, version 4.31.1 51 | Handshaking with Octopus server: https://myoctopus.acme.com 52 | Handshake successful. Octopus version: 2018.4.0; API version: 3.0.0 53 | Authenticated as: steve 54 | Pushing package: /src/AcmeWeb.3.1.4.nupkg... 55 | Push successful 56 | ``` 57 | 58 | ### Simplified command for Linux 59 | if using Linux you can add the following function to `~/.bashrc` to interact with the tool like a normal command. All parameters that refer to the filesystem (`pack`/`push`) can then just be referenced by relative path from the cwd. 60 | ``` 61 | echo 'function octo(){ sudo docker run --rm -v $(pwd):/src octopusdeploy/octo "$@" ;}' >> ~/.bashrc 62 | source ~/.bashrc 63 | octo pack --id=Acme --version=3.1.4 --basePath=OctopusDocker 64 | ``` 65 | 66 | ## Licence 67 | Copyright (c) Octopus Deploy and contributors. All rights reserved. 68 | 69 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 70 | these files except in compliance with the License. You may obtain a copy of the 71 | License at 72 | 73 | http://www.apache.org/licenses/LICENSE-2.0 74 | 75 | Unless required by applicable law or agreed to in writing, software distributed 76 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 77 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 78 | specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/SerilogLogProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Octopus.Client.Logging; 3 | using Serilog; 4 | using Serilog.Context; 5 | using Serilog.Events; 6 | 7 | namespace Octopus.Cli.Util 8 | { 9 | public class CliSerilogLogProvider : ILogProvider 10 | { 11 | public CliSerilogLogProvider(ILogger logger) 12 | { 13 | Logger = logger ?? throw new ArgumentNullException(nameof(logger)); 14 | } 15 | 16 | public ILogger Logger { get; set; } 17 | 18 | public static bool PrintMessages { get; set; } 19 | 20 | public Logger GetLogger(string name) 21 | { 22 | return new SerilogLogger(Logger.ForContext("SourceContext", name)).Log; 23 | } 24 | 25 | public IDisposable OpenNestedContext(string message) 26 | { 27 | return LogContext.PushProperty("Context", message); 28 | } 29 | 30 | public IDisposable OpenMappedContext(string key, string value) 31 | { 32 | return LogContext.PushProperty(key, value); 33 | } 34 | 35 | internal class SerilogLogger 36 | { 37 | readonly ILogger logger; 38 | 39 | public SerilogLogger(ILogger logger) 40 | { 41 | this.logger = logger; 42 | } 43 | 44 | public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) 45 | { 46 | if (!PrintMessages) 47 | return false; 48 | 49 | var translatedLevel = TranslateLevel(logLevel); 50 | if (messageFunc == null) 51 | return logger.IsEnabled(translatedLevel); 52 | 53 | if (!logger.IsEnabled(translatedLevel)) 54 | return false; 55 | 56 | if (exception != null) 57 | LogException(translatedLevel, messageFunc, exception, formatParameters); 58 | else 59 | LogMessage(translatedLevel, messageFunc, formatParameters); 60 | 61 | return true; 62 | } 63 | 64 | void LogMessage(LogEventLevel logLevel, Func messageFunc, object[] formatParameters) 65 | { 66 | logger.Write(logLevel, messageFunc(), formatParameters); 67 | } 68 | 69 | void LogException(LogEventLevel logLevel, Func messageFunc, Exception exception, object[] formatParams) 70 | { 71 | logger.Write(logLevel, exception, messageFunc(), formatParams); 72 | } 73 | 74 | static LogEventLevel TranslateLevel(LogLevel logLevel) 75 | { 76 | switch (logLevel) 77 | { 78 | case LogLevel.Fatal: 79 | return LogEventLevel.Fatal; 80 | case LogLevel.Error: 81 | return LogEventLevel.Error; 82 | case LogLevel.Warn: 83 | return LogEventLevel.Warning; 84 | case LogLevel.Info: 85 | return LogEventLevel.Information; 86 | case LogLevel.Trace: 87 | return LogEventLevel.Verbose; 88 | default: 89 | return LogEventLevel.Debug; 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /source/Octo.Tests/Octo.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | Octo.Tests 6 | true 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | PreserveNewest 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | $(OutDir)ExpectedSdkVersion.txt 51 | 52 | $(Version) 53 | 54 | $([System.IO.File]::ReadAllText($(ExpectedVersionFileInIntermediateFolder))) 55 | 56 | 57 | false 58 | true 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/SupportFormattedOutputFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using Newtonsoft.Json; 6 | using NUnit.Framework; 7 | using Octopus.CommandLine.Commands; 8 | 9 | namespace Octo.Tests.Commands 10 | { 11 | [TestFixture] 12 | public class SupportFormattedOutputFixture : ApiCommandFixtureBase 13 | { 14 | [Test] 15 | public void FormattedOutput_ShouldAddOutputOption() 16 | { 17 | var sw = new StringWriter(); 18 | var command = new DummyApiCommandWithFormattedOutputSupport(ClientFactory, RepositoryFactory, FileSystem, CommandOutputProvider); 19 | 20 | command.GetHelp(sw, new[] { "command" }); 21 | 22 | sw.ToString().Should().ContainEquivalentOf("--output"); 23 | } 24 | 25 | [Test] 26 | public async Task FormattedOutput_FormatSetToJson() 27 | { 28 | var command = 29 | new DummyApiCommandWithFormattedOutputSupport(ClientFactory, RepositoryFactory, FileSystem, CommandOutputProvider); 30 | 31 | CommandLineArgs.Add("--outputFormat=json"); 32 | 33 | await command.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 34 | 35 | command.PrintJsonOutputCalled.ShouldBeEquivalentTo(true); 36 | } 37 | 38 | [Test] 39 | public void FormattedOutput_FormatInvalid() 40 | { 41 | var command = new DummyApiCommandWithFormattedOutputSupport(ClientFactory, RepositoryFactory, FileSystem, CommandOutputProvider); 42 | CommandLineArgs.Add("--helpOutputFormat=blah"); 43 | 44 | var exception = Assert.ThrowsAsync(async () => await command.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false)); 45 | 46 | command.PrintJsonOutputCalled.Should().BeFalse(); 47 | command.PrintDefaultOutputCalled.Should().BeFalse(); 48 | exception.Message.Should().Be("Could not convert string `blah' to type OutputFormat for option `--helpOutputFormat'. Valid values are Default and Json."); 49 | } 50 | 51 | [Test] 52 | public async Task JsonFormattedOutputHelp_ShouldBeWellFormed() 53 | { 54 | var command = new DummyApiCommandWithFormattedOutputSupport(ClientFactory, RepositoryFactory, FileSystem, CommandOutputProvider); 55 | 56 | CommandLineArgs.Add("--helpOutputFormat=json"); 57 | CommandLineArgs.Add("--help"); 58 | 59 | await command.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 60 | 61 | var logOutput = LogOutput.ToString(); 62 | Console.WriteLine(logOutput); 63 | JsonConvert.DeserializeObject(logOutput); 64 | logOutput.Should().Contain("--helpOutputFormat=VALUE"); 65 | logOutput.Should().Contain("--help"); 66 | logOutput.Should().Contain("dummy-command"); 67 | logOutput.Should().Contain("this is the command's description"); 68 | } 69 | 70 | [Test] 71 | public async Task PlainTextFormattedOutputHelp_ShouldBeWellFormed() 72 | { 73 | var command = new DummyApiCommandWithFormattedOutputSupport(ClientFactory, RepositoryFactory, FileSystem, CommandOutputProvider); 74 | 75 | CommandLineArgs.Add("--help"); 76 | 77 | await command.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 78 | 79 | var logOutput = LogOutput.ToString(); 80 | Console.WriteLine(logOutput); 81 | logOutput.Should().Contain("--helpOutputFormat=VALUE"); 82 | logOutput.Should().Contain("--help"); 83 | logOutput.Should().Contain("dummy-command"); 84 | logOutput.Should().Contain("this is the command's description"); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Releases/AllowReleaseProgressionCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Octopus.Cli.Repositories; 5 | using Octopus.Cli.Util; 6 | using Octopus.Client; 7 | using Octopus.Client.Exceptions; 8 | using Octopus.Client.Model; 9 | using Octopus.CommandLine; 10 | using Octopus.CommandLine.Commands; 11 | using Octopus.Versioning.Octopus; 12 | 13 | namespace Octopus.Cli.Commands.Releases 14 | { 15 | [Command("allow-releaseprogression", Description = "Allows a release to progress to the next phase.")] 16 | public class AllowReleaseProgressionCommand : ApiCommand, ISupportFormattedOutput 17 | { 18 | static readonly OctopusVersionParser OctopusVersionParser = new OctopusVersionParser(); 19 | ProjectResource project; 20 | ReleaseResource release; 21 | 22 | public AllowReleaseProgressionCommand(IOctopusClientFactory clientFactory, IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, ICommandOutputProvider commandOutputProvider) 23 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 24 | { 25 | var options = Options.For("Allowing release progression"); 26 | options.Add("project=", "Name or ID of the project.", v => ProjectNameOrId = v); 27 | options.Add("version=|releaseNumber=", "Release version/number.", v => ReleaseVersionNumber = v); 28 | } 29 | 30 | public string ProjectNameOrId { get; set; } 31 | 32 | public string ReleaseVersionNumber { get; set; } 33 | 34 | protected override async Task ValidateParameters() 35 | { 36 | if (string.IsNullOrWhiteSpace(ProjectNameOrId)) throw new CommandException("Please specify a project name or ID using the parameter: --project=XYZ"); 37 | if (string.IsNullOrWhiteSpace(ReleaseVersionNumber)) throw new CommandException("Please specify a release version number using the version parameter: --version=1.0.5"); 38 | if (!OctopusVersionParser.TryParse(ReleaseVersionNumber, out _)) throw new CommandException("Please provide a valid release version format: --version=1.0.5"); 39 | await base.ValidateParameters().ConfigureAwait(false); 40 | } 41 | 42 | public async Task Request() 43 | { 44 | project = await Repository.Projects.FindByNameOrIdOrFail(ProjectNameOrId).ConfigureAwait(false); 45 | 46 | release = await Repository.Projects.GetReleaseByVersion(project, ReleaseVersionNumber).ConfigureAwait(false); 47 | if (release == null) throw new OctopusResourceNotFoundException($"Unable to locate a release with version/release number '{ReleaseVersionNumber}'."); 48 | 49 | var isReleaseAllowedFromProgressionAlready = (await Repository.Defects.GetDefects(release).ConfigureAwait(false)).Items.All(i => i.Status == DefectStatus.Resolved); 50 | if (isReleaseAllowedFromProgressionAlready) 51 | { 52 | commandOutputProvider.Debug($"Release with version/release number '{ReleaseVersionNumber}' is already allowed to progress to next phase."); 53 | 54 | return; 55 | } 56 | 57 | await Repository.Defects.ResolveDefect(release).ConfigureAwait(false); 58 | } 59 | 60 | public void PrintDefaultOutput() 61 | { 62 | commandOutputProvider.Information("Allowed successfully."); 63 | } 64 | 65 | public void PrintJsonOutput() 66 | { 67 | commandOutputProvider.Json(new 68 | { 69 | project.SpaceId, 70 | Project = new { project.Id, project.Name }, 71 | Release = new { release.Id, release.Version, IsPreventedFromProgressing = false } 72 | }); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Releases/ListReleasesCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Octopus.Cli.Repositories; 6 | using Octopus.Cli.Util; 7 | using Octopus.Client; 8 | using Octopus.Client.Model; 9 | using Octopus.CommandLine; 10 | using Octopus.CommandLine.Commands; 11 | 12 | namespace Octopus.Cli.Commands.Releases 13 | { 14 | [Command("list-releases", Description = "Lists releases by project.")] 15 | public class ListReleasesCommand : ApiCommand, ISupportFormattedOutput 16 | { 17 | readonly HashSet projects = new HashSet(StringComparer.OrdinalIgnoreCase); 18 | List projectResources; 19 | string[] projectsFilter; 20 | List releases; 21 | 22 | public ListReleasesCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 23 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 24 | { 25 | var options = Options.For("Listing"); 26 | options.Add("project=", "Name of a project to filter by. Can be specified many times.", v => projects.Add(v), allowsMultiple: true); 27 | } 28 | 29 | public async Task Request() 30 | { 31 | projectResources = new List(); 32 | projectsFilter = new string[0]; 33 | 34 | if (projects.Count > 0) 35 | { 36 | commandOutputProvider.Debug("Loading projects..."); 37 | //var test = Repository.Projects.FindByNames(projects.ToArray()); 38 | projectResources = await Repository.Projects.FindByNames(projects.ToArray()).ConfigureAwait(false); 39 | projectsFilter = projectResources.Select(p => p.Id).ToArray(); 40 | } 41 | 42 | commandOutputProvider.Debug("Loading releases..."); 43 | 44 | releases = await Repository.Releases 45 | .FindMany(x => projectsFilter.Contains(x.ProjectId)) 46 | .ConfigureAwait(false); 47 | } 48 | 49 | public void PrintDefaultOutput() 50 | { 51 | commandOutputProvider.Information("Releases: {Count}", releases.Count); 52 | foreach (var project in projectResources) 53 | { 54 | commandOutputProvider.Information(" - Project: {Project:l}", project.Name); 55 | 56 | foreach (var release in releases.Where(x => x.ProjectId == project.Id)) 57 | { 58 | var propertiesToLog = new List(); 59 | propertiesToLog.AddRange(FormatReleasePropertiesAsStrings(release)); 60 | foreach (var property in propertiesToLog) 61 | commandOutputProvider.Information(" {Property:l}", property); 62 | commandOutputProvider.Information(""); 63 | } 64 | } 65 | } 66 | 67 | public void PrintJsonOutput() 68 | { 69 | commandOutputProvider.Json(projectResources.Select(pr => new 70 | { 71 | Project = new { pr.Id, pr.Name }, 72 | Releases = releases.Where(r => r.ProjectId == pr.Id) 73 | .Select(r => new 74 | { 75 | r.Version, 76 | r.Assembled, 77 | PackageVersions = GetPackageVersionsAsString(r.SelectedPackages), 78 | ReleaseNotes = GetReleaseNotes(r), 79 | GitReference = r.VersionControlReference?.GitRef, 80 | r.VersionControlReference?.GitCommit 81 | }) 82 | })); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Util/CommandSuggester.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Octopus.Cli.Util 6 | { 7 | public static class CommandSuggester 8 | { 9 | public static IEnumerable SuggestCommandsFor( 10 | string[] words, 11 | IReadOnlyDictionary completionItems) 12 | { 13 | // some shells will pass the command name as invoked on the command line. 14 | // If so, strip them from the beginning of the array 15 | words = words 16 | .Take(2) 17 | .Except(new[] { "octo", "complete" }, StringComparer.OrdinalIgnoreCase) 18 | .Union(words.Skip(2)) 19 | .Where(word => string.IsNullOrWhiteSpace(word) == false) 20 | .ToArray(); 21 | 22 | var numberOfArgs = words.Length; 23 | var hasSubCommand = numberOfArgs > 1; 24 | var searchTerm = 25 | numberOfArgs > 0 26 | ? words.Last() 27 | : ""; 28 | var suggestions = new List(); 29 | var isOptionSearch = searchTerm.StartsWith("--"); 30 | var allSubCommands = completionItems.Keys.ToList(); 31 | 32 | if (isOptionSearch) 33 | { 34 | if (hasSubCommand) 35 | { 36 | // e.g. `octo subcommand --searchTerm` 37 | var subCommandName = words.Where(w => IsSubCommand(w, allSubCommands)).Last(); 38 | suggestions.AddRange(GetSubCommandOptionSuggestions(completionItems, subCommandName, searchTerm)); 39 | } 40 | else 41 | { 42 | // e.g. `octo --searchTerm` 43 | return GetBaseOptionSuggestions(completionItems, searchTerm).OrderBy(name => name); 44 | } 45 | } 46 | else if (ZeroOrOneSubCommands(words, allSubCommands)) 47 | { 48 | // e.g. `octo searchterm` or just `octo` 49 | suggestions.AddRange(GetSubCommandSuggestions(completionItems, searchTerm)); 50 | } 51 | 52 | return suggestions.OrderBy(name => name); 53 | } 54 | 55 | static IEnumerable GetSubCommandSuggestions(IReadOnlyDictionary completionItems, string searchTerm) 56 | { 57 | return completionItems.Keys.Where(s => 58 | s.StartsWith(searchTerm, StringComparison.OrdinalIgnoreCase)); 59 | } 60 | 61 | static bool ZeroOrOneSubCommands(string[] words, List allSubCommands) 62 | { 63 | return words.Where(w => IsSubCommand(w, allSubCommands)).Count() <= 1; 64 | } 65 | 66 | static IEnumerable GetSubCommandOptionSuggestions(IReadOnlyDictionary completionItems, string subCommandName, string searchTerm) 67 | { 68 | if (completionItems.TryGetValue(subCommandName, out var options)) 69 | return options 70 | .Where(name => name.StartsWith(searchTerm, StringComparison.OrdinalIgnoreCase)) 71 | .ToList(); 72 | 73 | return new string[] { }; 74 | } 75 | 76 | static IEnumerable GetBaseOptionSuggestions(IReadOnlyDictionary completionItems, string searchTerm) 77 | { 78 | // If you type 'octo', you'll be redirected to the 'help' command, so show these options 79 | return GetSubCommandOptionSuggestions(completionItems, "help", searchTerm); 80 | } 81 | 82 | static bool IsSubCommand(string arg, List subCommandList) 83 | { 84 | if (arg == null) return false; 85 | if (arg.StartsWith("--")) return false; 86 | return subCommandList.Contains(arg); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Package/BuildInformationCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Newtonsoft.Json; 5 | using Octopus.Cli.Repositories; 6 | using Octopus.Cli.Util; 7 | using Octopus.Client; 8 | using Octopus.Client.Model; 9 | using Octopus.Client.Model.BuildInformation; 10 | using Octopus.CommandLine; 11 | using Octopus.CommandLine.Commands; 12 | 13 | namespace Octopus.Cli.Commands.Package 14 | { 15 | [Command("build-information", Description = "Pushes build information to Octopus Server.")] 16 | public class BuildInformationCommand : ApiCommand, ISupportFormattedOutput 17 | { 18 | static readonly OverwriteMode DefaultOverwriteMode = OverwriteMode.FailIfExists; 19 | readonly List pushedBuildInformation; 20 | OctopusPackageVersionBuildInformationMappedResource resultResource; 21 | 22 | public BuildInformationCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 23 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 24 | { 25 | var options = Options.For("Build information pushing"); 26 | options.Add("package-id=", "The ID of the package. Specify multiple packages by specifying this argument multiple times: \n--package-id 'MyCompany.MyApp' --package-id 'MyCompany.MyApp2'.", packageId => PackageIds.Add(packageId), allowsMultiple: true); 27 | options.Add("version=", "The version of the package; defaults to a timestamp-based version.", v => Version = v); 28 | options.Add("file=", "Octopus Build Information Json file.", file => File = file); 29 | options.Add("overwrite-mode=", $"Determines behavior if the package already exists in the repository. Valid values are {Enum.GetNames(typeof(OverwriteMode)).ReadableJoin()}. Default is {DefaultOverwriteMode}.", mode => OverwriteMode = mode); 30 | 31 | pushedBuildInformation = new List(); 32 | } 33 | 34 | public HashSet PackageIds { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); 35 | public string Version { get; set; } 36 | public string File { get; set; } 37 | 38 | public OverwriteMode OverwriteMode { get; set; } = DefaultOverwriteMode; 39 | 40 | public async Task Request() 41 | { 42 | if (string.IsNullOrEmpty(File)) 43 | throw new CommandException("Please specify the build information file."); 44 | if (PackageIds.None()) 45 | throw new CommandException("Please specify at least one package id."); 46 | if (string.IsNullOrEmpty(Version)) 47 | throw new CommandException("Please specify the package version."); 48 | 49 | if (!FileSystem.FileExists(File)) 50 | throw new CommandException($"Build information file '{File}' does not exist"); 51 | 52 | var fileContent = FileSystem.ReadAllText(File); 53 | 54 | var buildInformation = JsonConvert.DeserializeObject(fileContent); 55 | 56 | foreach (var packageId in PackageIds) 57 | { 58 | commandOutputProvider.Debug("Pushing build information for package {PackageId} version {Version}...", packageId, Version); 59 | resultResource = await Repository.BuildInformationRepository.Push(packageId, Version, buildInformation, OverwriteMode); 60 | pushedBuildInformation.Add(resultResource); 61 | } 62 | } 63 | 64 | public void PrintDefaultOutput() 65 | { 66 | commandOutputProvider.Debug("Push successful"); 67 | } 68 | 69 | public void PrintJsonOutput() 70 | { 71 | commandOutputProvider.Json(pushedBuildInformation); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/ListLatestDeploymentsCommandFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using Newtonsoft.Json; 6 | using NSubstitute; 7 | using NUnit.Framework; 8 | using Octopus.Cli.Commands.Deployment; 9 | using Octopus.Client.Model; 10 | 11 | namespace Octo.Tests.Commands 12 | { 13 | [TestFixture] 14 | public class ListLatestDeploymentsCommandFixture : ApiCommandFixtureBase 15 | { 16 | ListLatestDeploymentsCommand listLatestDeploymentsCommands; 17 | 18 | [SetUp] 19 | public void SetUp() 20 | { 21 | listLatestDeploymentsCommands = new ListLatestDeploymentsCommand(RepositoryFactory, FileSystem, ClientFactory, CommandOutputProvider); 22 | 23 | var dashboardResources = new DashboardResource 24 | { 25 | Items = new List 26 | { 27 | new DashboardItemResource 28 | { 29 | EnvironmentId = "environmentid1", 30 | ProjectId = "projectaid", 31 | TenantId = "tenantid1", 32 | ReleaseId = "Release1" 33 | }, 34 | new DashboardItemResource 35 | { 36 | EnvironmentId = "environmentid1", 37 | ProjectId = "projectaid", 38 | TenantId = "tenantid2", 39 | ReleaseId = "Release2" 40 | } 41 | }, 42 | Tenants = new List 43 | { 44 | new DashboardTenantResource 45 | { 46 | Id = "tenantid1", 47 | Name = "tenant1" 48 | } 49 | } 50 | }; 51 | 52 | Repository.Projects.FindByNames(Arg.Any>()) 53 | .Returns(Task.FromResult( 54 | new List 55 | { 56 | new ProjectResource { Name = "ProjectA", Id = "projectaid" } 57 | })); 58 | 59 | Repository.Environments.FindAll() 60 | .Returns(Task.FromResult( 61 | new List 62 | { 63 | new EnvironmentResource { Name = "EnvA", Id = "environmentid1" } 64 | })); 65 | 66 | Repository.Releases.Get(Arg.Is("Release1")).Returns(new ReleaseResource { Version = "0.0.1" }); 67 | Repository.Releases.Get(Arg.Is("Release2")).Returns(new ReleaseResource { Version = "V1.0.0" }); 68 | 69 | Repository.Dashboards.GetDynamicDashboard(Arg.Any(), Arg.Any()).ReturnsForAnyArgs(dashboardResources); 70 | } 71 | 72 | [Test] 73 | public async Task ShouldNotFailWhenTenantIsRemoved() 74 | { 75 | CommandLineArgs.Add("--project=ProjectA"); 76 | 77 | await listLatestDeploymentsCommands.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 78 | 79 | LogLines.Should().Contain(" - Tenant: tenant1"); 80 | LogLines.Should().Contain(" - Tenant: "); 81 | LogLines.Should().Contain(" Version: V1.0.0"); 82 | } 83 | 84 | [Test] 85 | public async Task JsonOutput_ShouldNotFailOnRemovedTenant() 86 | { 87 | CommandLineArgs.Add("--project=ProjectA"); 88 | CommandLineArgs.Add("--outputFormat=json"); 89 | 90 | await listLatestDeploymentsCommands.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 91 | 92 | var logoutput = LogOutput.ToString(); 93 | JsonConvert.DeserializeObject(logoutput); 94 | logoutput.Should().Contain("tenant1"); 95 | logoutput.Should().Contain(""); 96 | logoutput.Should().Contain("V1.0.0"); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Package/PushMetadataCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Newtonsoft.Json; 4 | using Octopus.Cli.Repositories; 5 | using Octopus.Cli.Util; 6 | using Octopus.Client; 7 | using Octopus.Client.Model; 8 | using Octopus.Client.Model.PackageMetadata; 9 | using Octopus.CommandLine; 10 | using Octopus.CommandLine.Commands; 11 | 12 | namespace Octopus.Cli.Commands.Package 13 | { 14 | [Command("push-metadata", Description = "Pushes package metadata to Octopus Server. Deprecated. Please use the build-information command for Octopus Server 2019.10.0 and above.")] 15 | public class PushMetadataCommand : ApiCommand, ISupportFormattedOutput 16 | { 17 | static readonly OverwriteMode DefaultOverwriteMode = OverwriteMode.FailIfExists; 18 | 19 | OctopusPackageMetadataMappedResource resultResource; 20 | 21 | public PushMetadataCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 22 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 23 | { 24 | var options = Options.For("Package metadata pushing"); 25 | options.Add("package-id=", "The ID of the package, e.g., 'MyCompany.MyApp'.", v => PackageId = v); 26 | options.Add("version=", "The version of the package; defaults to a timestamp-based version.", v => Version = v); 27 | options.Add("metadata-file=", "Octopus Package metadata Json file.", file => MetadataFile = file); 28 | options.Add("overwrite-mode=", $"Determines behavior if the package already exists in the repository. Valid values are {Enum.GetNames(typeof(OverwriteMode)).ReadableJoin()}. Default is {DefaultOverwriteMode}.", mode => OverwriteMode = mode); 29 | options.Add("replace-existing", "If the package metadata already exists in the repository, the default behavior is to reject the new package metadata being pushed. You can pass this flag to overwrite the existing package metadata. This flag may be deprecated in a future version; passing it is the same as using the OverwriteExisting overwrite-mode.", replace => OverwriteMode = OverwriteMode.OverwriteExisting); 30 | } 31 | 32 | public string PackageId { get; set; } 33 | public string Version { get; set; } 34 | public string MetadataFile { get; set; } 35 | public OverwriteMode OverwriteMode { get; set; } = DefaultOverwriteMode; 36 | 37 | public async Task Request() 38 | { 39 | if (string.IsNullOrEmpty(MetadataFile)) 40 | throw new CommandException("Please specify the metadata file."); 41 | if (string.IsNullOrEmpty(PackageId)) 42 | throw new CommandException("Please specify the package id."); 43 | if (string.IsNullOrEmpty(Version)) 44 | throw new CommandException("Please specify the package version."); 45 | 46 | if (!FileSystem.FileExists(MetadataFile)) 47 | throw new CommandException($"Metadata file '{MetadataFile}' does not exist"); 48 | 49 | var rootDocument = await Repository.LoadRootDocument(); 50 | if (rootDocument.HasLink("BuildInformation")) 51 | commandOutputProvider.Warning("This Octopus server supports the BuildInformation API, we recommend using the `build-information` command as `package-metadata` has been deprecated."); 52 | 53 | commandOutputProvider.Debug("Pushing package metadata: {PackageId}...", PackageId); 54 | 55 | var fileContent = FileSystem.ReadAllText(MetadataFile); 56 | var octopusPackageMetadata = JsonConvert.DeserializeObject(fileContent); 57 | 58 | resultResource = await Repository.PackageMetadataRepository.Push(PackageId, Version, octopusPackageMetadata, OverwriteMode); 59 | } 60 | 61 | public void PrintDefaultOutput() 62 | { 63 | commandOutputProvider.Debug("Push successful"); 64 | } 65 | 66 | public void PrintJsonOutput() 67 | { 68 | commandOutputProvider.Json(resultResource); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Releases/PreventReleaseProgressionCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Octopus.Cli.Repositories; 5 | using Octopus.Cli.Util; 6 | using Octopus.Client; 7 | using Octopus.Client.Exceptions; 8 | using Octopus.Client.Model; 9 | using Octopus.CommandLine; 10 | using Octopus.CommandLine.Commands; 11 | using Octopus.Versioning.Octopus; 12 | 13 | namespace Octopus.Cli.Commands.Releases 14 | { 15 | [Command("prevent-releaseprogression", Description = "Prevents a release from progressing to the next phase.")] 16 | public class PreventReleaseProgressionCommand : ApiCommand, ISupportFormattedOutput 17 | { 18 | static readonly OctopusVersionParser OctopusVersionParser = new OctopusVersionParser(); 19 | ProjectResource project; 20 | ReleaseResource release; 21 | 22 | public PreventReleaseProgressionCommand(IOctopusClientFactory clientFactory, IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, ICommandOutputProvider commandOutputProvider) 23 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 24 | { 25 | var options = Options.For("Preventing release progression"); 26 | options.Add("project=", "Name or ID of the project.", v => ProjectNameOrId = v); 27 | options.Add("version=|releaseNumber=", "Release version/number.", v => ReleaseVersionNumber = v); 28 | options.Add("reason=", "Reason to prevent this release from progressing to next phase.", v => ReasonToPrevent = v); 29 | } 30 | 31 | public string ProjectNameOrId { get; set; } 32 | 33 | public string ReleaseVersionNumber { get; set; } 34 | 35 | public string ReasonToPrevent { get; set; } 36 | 37 | protected override async Task ValidateParameters() 38 | { 39 | if (string.IsNullOrWhiteSpace(ProjectNameOrId)) throw new CommandException("Please specify a project name or ID using the parameter: --project=XYZ"); 40 | if (string.IsNullOrWhiteSpace(ReleaseVersionNumber)) throw new CommandException("Please specify a release version number using the version parameter: --version=1.0.5"); 41 | if (!OctopusVersionParser.TryParse(ReleaseVersionNumber, out _)) throw new CommandException("Please provide a valid release version format: --version=1.0.5"); 42 | if (string.IsNullOrWhiteSpace(ReasonToPrevent)) throw new CommandException("Please specify a reason why you would like to prevent this release from progressing to next phase using the reason parameter: --reason=Contract Tests Failed"); 43 | 44 | await base.ValidateParameters().ConfigureAwait(false); 45 | } 46 | 47 | public async Task Request() 48 | { 49 | project = await Repository.Projects.FindByNameOrIdOrFail(ProjectNameOrId).ConfigureAwait(false); 50 | 51 | release = await Repository.Projects.GetReleaseByVersion(project, ReleaseVersionNumber).ConfigureAwait(false); 52 | if (release == null) throw new OctopusResourceNotFoundException($"Unable to locate a release with version/release number '{ReleaseVersionNumber}'."); 53 | 54 | var isReleasePreventedFromProgressionAlready = (await Repository.Defects.GetDefects(release).ConfigureAwait(false)).Items.Any(i => i.Status == DefectStatus.Unresolved); 55 | if (isReleasePreventedFromProgressionAlready) 56 | { 57 | commandOutputProvider.Debug($"Release with version/release number '{ReleaseVersionNumber}' is already prevented from progressing to next phase."); 58 | 59 | return; 60 | } 61 | 62 | await Repository.Defects.RaiseDefect(release, ReasonToPrevent).ConfigureAwait(false); 63 | } 64 | 65 | public void PrintDefaultOutput() 66 | { 67 | commandOutputProvider.Information("Prevented successfully."); 68 | } 69 | 70 | public void PrintJsonOutput() 71 | { 72 | commandOutputProvider.Json(new 73 | { 74 | project.SpaceId, 75 | Project = new { project.Id, project.Name }, 76 | Release = new { release.Id, release.Version, IsPreventedFromProgressing = true }, 77 | ReasonToPrevent 78 | }); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/HealthStatusProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Octopus.Client; 5 | using Octopus.Client.Model; 6 | using Octopus.CommandLine; 7 | using Octopus.CommandLine.Commands; 8 | 9 | #pragma warning disable 618 10 | namespace Octopus.Cli.Commands 11 | { 12 | /// 13 | /// This class exists to provide backwards compataility to the pre 3.4.0 changes to machine state. 14 | /// As of 3.4.0 the enum has been marked as obselete to be replaced with 15 | /// 16 | public class HealthStatusProvider 17 | { 18 | public static readonly string[] StatusNames = Enum.GetNames(typeof(MachineModelStatus)); 19 | public static readonly string[] HealthStatusNames = Enum.GetNames(typeof(MachineModelHealthStatus)); 20 | readonly HashSet statuses; 21 | readonly HashSet healthStatuses; 22 | readonly ICommandOutputProvider commandOutputProvider; 23 | 24 | public HealthStatusProvider(IOctopusAsyncRepository repository, 25 | HashSet statuses, 26 | HashSet healthStatuses, 27 | ICommandOutputProvider commandOutputProvider, 28 | RootResource rootDocument) 29 | { 30 | this.statuses = statuses; 31 | this.healthStatuses = healthStatuses; 32 | this.commandOutputProvider = commandOutputProvider; 33 | IsHealthStatusPendingDeprication = new SemanticVersion(rootDocument.Version).Version >= new SemanticVersion("3.4.0").Version; 34 | ValidateOptions(); 35 | } 36 | 37 | bool IsHealthStatusPendingDeprication { get; } 38 | 39 | void ValidateOptions() 40 | { 41 | if (IsHealthStatusPendingDeprication) 42 | { 43 | if (statuses.Any()) 44 | commandOutputProvider.Warning("The `--status` parameter will be deprecated in Octopus Deploy 4.0. You may want to execute this command with the `--health-status=` parameter instead."); 45 | } 46 | else 47 | { 48 | if (healthStatuses.Any()) 49 | throw new CommandException("The `--health-status` parameter is only available on Octopus Server instances from 3.4.0 onwards."); 50 | } 51 | } 52 | 53 | public string GetStatus(MachineBasedResource machineResource) 54 | { 55 | if (IsHealthStatusPendingDeprication) 56 | { 57 | var status = machineResource.HealthStatus.ToString(); 58 | if (machineResource.IsDisabled) 59 | status = status + " - Disabled"; 60 | return status; 61 | } 62 | 63 | return machineResource.Status.ToString(); 64 | } 65 | 66 | public IEnumerable Filter(IEnumerable machines) where TMachineResource : MachineBasedResource 67 | { 68 | machines = FilterByProvidedStatus(machines); 69 | machines = FilterByProvidedHealthStatus(machines); 70 | return machines; 71 | } 72 | 73 | IEnumerable FilterByProvidedStatus(IEnumerable machines) where TMachineResource : MachineBasedResource 74 | { 75 | var statusFilter = new List(); 76 | if (statuses.Count > 0) 77 | { 78 | commandOutputProvider.Debug("Loading statuses..."); 79 | statusFilter.AddRange(statuses); 80 | } 81 | 82 | return statusFilter.Any() 83 | ? machines.Where(p => statusFilter.Contains(p.Status)) 84 | : machines; 85 | } 86 | 87 | IEnumerable FilterByProvidedHealthStatus(IEnumerable machines) where TMachineResource : MachineBasedResource 88 | { 89 | var statusFilter = new List(); 90 | if (healthStatuses.Count > 0) 91 | { 92 | commandOutputProvider.Debug("Loading health statuses..."); 93 | statusFilter.AddRange(healthStatuses); 94 | } 95 | 96 | return statusFilter.Any() 97 | ? machines.Where(p => statusFilter.Contains(p.HealthStatus)) 98 | : machines; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /source/Octo.Tests/Commands/ListDeploymentsCommandFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using Newtonsoft.Json; 6 | using NSubstitute; 7 | using NUnit.Framework; 8 | using Octopus.Cli.Commands.Deployment; 9 | using Octopus.Client.Extensibility; 10 | using Octopus.Client.Model; 11 | 12 | namespace Octo.Tests.Commands 13 | { 14 | [TestFixture] 15 | public class ListDeploymentsCommandFixture : ApiCommandFixtureBase 16 | { 17 | ListDeploymentsCommand listDeploymentsCommands; 18 | 19 | [SetUp] 20 | public void SetUp() 21 | { 22 | listDeploymentsCommands = new ListDeploymentsCommand(RepositoryFactory, FileSystem, ClientFactory, CommandOutputProvider); 23 | 24 | var deploymentResources = new ResourceCollection( 25 | new List 26 | { 27 | new DeploymentResource 28 | { 29 | Name = "", 30 | Id = "deploymentid1", 31 | ProjectId = "projectaid", 32 | EnvironmentId = "environmentid1", 33 | ReleaseId = "Release1" 34 | }, 35 | new DeploymentResource 36 | { 37 | Name = "", 38 | Id = "deploymentid2", 39 | ProjectId = "projectbid", 40 | EnvironmentId = "environmentid2", 41 | ReleaseId = "Release2" 42 | } 43 | }, 44 | new LinkCollection()); 45 | 46 | Repository.Tenants.Status() 47 | .ReturnsForAnyArgs(new MultiTenancyStatusResource()); 48 | 49 | Repository.Deployments 50 | .When( 51 | x => 52 | x.Paginate(Arg.Any(), 53 | Arg.Any(), 54 | Arg.Any(), 55 | Arg.Any, bool>>())) 56 | .Do(r => r.Arg, bool>>()(deploymentResources)); 57 | 58 | Repository.Projects.FindAll() 59 | .Returns(Task.FromResult( 60 | new List 61 | { 62 | new ProjectResource { Name = "ProjectA", Id = "projectaid" }, 63 | new ProjectResource { Name = "ProjectB", Id = "projectbid" } 64 | })); 65 | 66 | Repository.Environments.FindAll() 67 | .Returns(Task.FromResult( 68 | new List 69 | { 70 | new EnvironmentResource { Name = "EnvA", Id = "environmentid1" }, 71 | new EnvironmentResource { Name = "EnvB", Id = "environmentid2" } 72 | })); 73 | 74 | Repository.Tenants.FindAll() 75 | .Returns(Task.FromResult(new List())); 76 | 77 | Repository.Releases.Get(Arg.Is("Release1")).Returns(new ReleaseResource { Version = "0.0.1" }); 78 | Repository.Releases.Get(Arg.Is("Release2")).Returns(new ReleaseResource { Version = "somedockertag" }); 79 | } 80 | 81 | [Test] 82 | public async Task ShouldGetListOfDeployments() 83 | { 84 | var argsWithNumber = new List(CommandLineArgs) 85 | { 86 | "--number=1" 87 | }; 88 | 89 | await listDeploymentsCommands.Execute(argsWithNumber.ToArray()).ConfigureAwait(false); 90 | 91 | LogLines.Should().Contain(" - Project: ProjectA"); 92 | LogLines.Should().NotContain(" - Project: ProjectB"); 93 | } 94 | 95 | [Test] 96 | public async Task JsonOutput_ShouldBeWellFormedJson() 97 | { 98 | CommandLineArgs.Add("--outputFormat=json"); 99 | 100 | await listDeploymentsCommands.Execute(CommandLineArgs.ToArray()).ConfigureAwait(false); 101 | 102 | var logoutput = LogOutput.ToString(); 103 | JsonConvert.DeserializeObject(logoutput); 104 | logoutput.Should().Contain("ProjectA"); 105 | logoutput.Should().Contain("ProjectB"); 106 | logoutput.Should().Contain("0.0.1"); 107 | logoutput.Should().Contain("somedockertag"); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Package/PushCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Octopus.Cli.Repositories; 7 | using Octopus.Cli.Util; 8 | using Octopus.Client; 9 | using Octopus.Client.Model; 10 | using Octopus.CommandLine; 11 | using Octopus.CommandLine.Commands; 12 | 13 | namespace Octopus.Cli.Commands.Package 14 | { 15 | [Command("push", Description = "Pushes a package (.nupkg, .zip, .tar.gz, .jar, .war, etc.) package to the built-in NuGet repository in an Octopus Server.")] 16 | public class PushCommand : ApiCommand, ISupportFormattedOutput 17 | { 18 | static readonly OverwriteMode DefaultOverwriteMode = OverwriteMode.FailIfExists; 19 | readonly List pushedPackages; 20 | readonly List> failedPackages; 21 | 22 | public PushCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 23 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 24 | { 25 | var options = Options.For("Package pushing"); 26 | options.Add("package=", 27 | "Package file to push. Specify multiple packages by specifying this argument multiple times: \n--package package1 --package package2.", 28 | package => Packages.Add(EnsurePackageExists(fileSystem, package)), 29 | allowsMultiple: true); 30 | options.Add("overwrite-mode=", $"Determines behavior if the package already exists in the repository. Valid values are {Enum.GetNames(typeof(OverwriteMode)).ReadableJoin()}. Default is {DefaultOverwriteMode}.", mode => OverwriteMode = mode); 31 | options.Add("replace-existing", 32 | "If the package already exists in the repository, the default behavior is to reject the new package being pushed. You can pass this flag to overwrite the existing package. This flag may be deprecated in a future version; passing it is the same as using the OverwriteExisting overwrite-mode.", 33 | replace => OverwriteMode = OverwriteMode.OverwriteExisting); 34 | options.Add("use-delta-compression=", 35 | "Allows disabling of delta compression when uploading packages to the Octopus Server. (True or False. Defaults to true.)", 36 | v => UseDeltaCompression = v); 37 | pushedPackages = new List(); 38 | failedPackages = new List>(); 39 | } 40 | 41 | public HashSet Packages { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); 42 | public OverwriteMode OverwriteMode { get; set; } = DefaultOverwriteMode; 43 | public bool UseDeltaCompression { get; set; } = true; 44 | 45 | public int KeepAlive { get; set; } = 0; 46 | 47 | public async Task Request() 48 | { 49 | if (Packages.Count == 0) throw new CommandException("Please specify a package to push"); 50 | 51 | foreach (var package in Packages) 52 | { 53 | commandOutputProvider.Debug("Pushing package: {Package:l}...", package); 54 | 55 | try 56 | { 57 | using (var fileStream = FileSystem.OpenFile(package, FileAccess.Read)) 58 | { 59 | await Repository.BuiltInPackageRepository 60 | .PushPackage(Path.GetFileName(package), fileStream, OverwriteMode, UseDeltaCompression) 61 | .ConfigureAwait(false); 62 | } 63 | 64 | pushedPackages.Add(package); 65 | } 66 | catch (Exception ex) 67 | { 68 | if (OutputFormat == OutputFormat.Default) 69 | throw; 70 | failedPackages.Add(new Tuple(package, ex)); 71 | } 72 | } 73 | } 74 | 75 | public void PrintDefaultOutput() 76 | { 77 | commandOutputProvider.Debug("Push successful"); 78 | } 79 | 80 | public void PrintJsonOutput() 81 | { 82 | commandOutputProvider.Json(new 83 | { 84 | SuccessfulPackages = pushedPackages, 85 | FailedPackages = failedPackages.Select(x => new { Package = x.Item1, Reason = x.Item2.Message.Replace("\r\n", string.Empty) }) 86 | }); 87 | } 88 | 89 | static string EnsurePackageExists(IOctopusFileSystem fileSystem, string package) 90 | { 91 | var path = fileSystem.GetFullPath(package); 92 | if (!File.Exists(path)) throw new CommandException("Package file not found: " + path); 93 | return path; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /source/Octopus.Cli/Commands/Project/CreateProjectCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Octopus.Cli.Repositories; 4 | using Octopus.Cli.Util; 5 | using Octopus.Client; 6 | using Octopus.Client.Model; 7 | using Octopus.CommandLine; 8 | using Octopus.CommandLine.Commands; 9 | 10 | namespace Octopus.Cli.Commands.Project 11 | { 12 | [Command("create-project", Description = "Creates a project.")] 13 | public class CreateProjectCommand : ApiCommand, ISupportFormattedOutput 14 | { 15 | ProjectResource project; 16 | ProjectGroupResource projectGroup; 17 | bool projectGroupCreated; 18 | 19 | public CreateProjectCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) 20 | : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) 21 | { 22 | var options = Options.For("Project creation"); 23 | options.Add("name=", "The name of the project.", v => ProjectName = v); 24 | options.Add("projectGroup=", "The name of the project group to add this project to. If the group doesn't exist, it will be created.", v => ProjectGroupName = v); 25 | options.Add("lifecycle=", "The name of the lifecycle that the project will use.", v => LifecycleName = v); 26 | options.Add("ignoreIfExists", "If the project already exists, an error will be returned. Set this flag to ignore the error.", v => IgnoreIfExists = true); 27 | } 28 | 29 | public string ProjectName { get; set; } 30 | public string ProjectGroupName { get; set; } 31 | public bool IgnoreIfExists { get; set; } 32 | public string LifecycleName { get; set; } 33 | 34 | public async Task Request() 35 | { 36 | if (string.IsNullOrWhiteSpace(ProjectGroupName)) throw new CommandException("Please specify a project group name using the parameter: --projectGroup=XYZ"); 37 | if (string.IsNullOrWhiteSpace(ProjectName)) throw new CommandException("Please specify a project name using the parameter: --name=XYZ"); 38 | if (string.IsNullOrWhiteSpace(LifecycleName)) throw new CommandException("Please specify a lifecycle name using the parameter: --lifecycle=XYZ"); 39 | 40 | commandOutputProvider.Information("Finding project group: {Group:l}", ProjectGroupName); 41 | 42 | projectGroup = await Repository.ProjectGroups.FindByName(ProjectGroupName).ConfigureAwait(false); 43 | if (projectGroup == null) 44 | { 45 | commandOutputProvider.Information("Project group does not exist, it will be created"); 46 | projectGroup = await Repository.ProjectGroups.Create(new ProjectGroupResource { Name = ProjectGroupName }).ConfigureAwait(false); 47 | projectGroupCreated = true; 48 | } 49 | 50 | commandOutputProvider.Information("Finding lifecycle: {Lifecycle:l}", LifecycleName); 51 | var lifecycle = await Repository.Lifecycles.FindOne(l => l.Name.Equals(LifecycleName, StringComparison.OrdinalIgnoreCase)).ConfigureAwait(false); 52 | if (lifecycle == null) 53 | throw new CommandException($"The lifecycle {LifecycleName} does not exist."); 54 | 55 | project = await Repository.Projects.FindByName(ProjectName).ConfigureAwait(false); 56 | if (project != null) 57 | { 58 | if (IgnoreIfExists) 59 | { 60 | commandOutputProvider.Information("The project {Project:l} (ID {Id:l}) already exists", project.Name, project.Id); 61 | return; 62 | } 63 | 64 | throw new CommandException($"The project {project.Name} (ID {project.Id}) already exists in this project group."); 65 | } 66 | 67 | commandOutputProvider.Information("Creating project: {Project:l}", ProjectName); 68 | project = await Repository.Projects.Create(new ProjectResource { Name = ProjectName, ProjectGroupId = projectGroup.Id, IsDisabled = false, LifecycleId = lifecycle.Id }).ConfigureAwait(false); 69 | } 70 | 71 | public void PrintDefaultOutput() 72 | { 73 | commandOutputProvider.Information("Project created. ID: {Id:l}", project.Id); 74 | } 75 | 76 | public void PrintJsonOutput() 77 | { 78 | commandOutputProvider.Json(new 79 | { 80 | Project = new 81 | { 82 | project.Id, 83 | project.Name 84 | }, 85 | Group = new 86 | { 87 | projectGroup.Id, 88 | projectGroup.Name, 89 | NewGroupCreated = projectGroupCreated 90 | } 91 | }); 92 | } 93 | } 94 | } 95 | --------------------------------------------------------------------------------