├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── README.md └── src ├── Auth ├── AuthConstants.cs └── HMACAuthentication.cs ├── Extensions ├── DateTimeExtensions.cs ├── StringExtensions.cs └── UriExtensions.cs ├── PackageHelper.cs ├── Parse.cs ├── Program.cs ├── Properties ├── Defaults.Designer.cs ├── Defaults.resx ├── HelpTextResource.Designer.cs ├── HelpTextResource.resx ├── Resources.Designer.cs ├── Resources.resx └── launchSettings.json ├── UmbPack.csproj ├── UmbPack.sln └── Verbs ├── InitCommand.cs ├── PackCommand.cs └── PushCommand.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 2 | root = true 3 | 4 | # C# files 5 | [*.cs] 6 | 7 | #### Core EditorConfig Options #### 8 | 9 | # Indentation and spacing 10 | indent_size = 4 11 | indent_style = space 12 | tab_width = 4 13 | 14 | # New line preferences 15 | end_of_line = crlf 16 | insert_final_newline = false 17 | 18 | #### .NET Coding Conventions #### 19 | 20 | # Organize usings 21 | dotnet_separate_import_directive_groups = false 22 | dotnet_sort_system_directives_first = true 23 | 24 | # this. and Me. preferences 25 | dotnet_style_qualification_for_event = false:silent 26 | dotnet_style_qualification_for_field = false:silent 27 | dotnet_style_qualification_for_method = false:silent 28 | dotnet_style_qualification_for_property = false:silent 29 | 30 | # Language keywords vs BCL types preferences 31 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 32 | dotnet_style_predefined_type_for_member_access = true:silent 33 | 34 | # Parentheses preferences 35 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 36 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 37 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 38 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 39 | 40 | # Modifier preferences 41 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 42 | 43 | # Expression-level preferences 44 | dotnet_style_coalesce_expression = true:suggestion 45 | dotnet_style_collection_initializer = true:suggestion 46 | dotnet_style_explicit_tuple_names = true:suggestion 47 | dotnet_style_null_propagation = true:suggestion 48 | dotnet_style_object_initializer = true:suggestion 49 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 50 | dotnet_style_prefer_auto_properties = true:silent 51 | dotnet_style_prefer_compound_assignment = true:suggestion 52 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 53 | dotnet_style_prefer_conditional_expression_over_return = true:silent 54 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 55 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 56 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 57 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 58 | dotnet_style_prefer_simplified_interpolation = true:suggestion 59 | 60 | # Field preferences 61 | dotnet_style_readonly_field = true:suggestion 62 | 63 | # Parameter preferences 64 | dotnet_code_quality_unused_parameters = all:suggestion 65 | 66 | # Suppression preferences 67 | dotnet_remove_unnecessary_suppression_exclusions = none 68 | 69 | #### C# Coding Conventions #### 70 | 71 | # var preferences 72 | csharp_style_var_elsewhere = false:silent 73 | csharp_style_var_for_built_in_types = false:silent 74 | csharp_style_var_when_type_is_apparent = false:silent 75 | 76 | # Expression-bodied members 77 | csharp_style_expression_bodied_accessors = true:silent 78 | csharp_style_expression_bodied_constructors = false:silent 79 | csharp_style_expression_bodied_indexers = true:silent 80 | csharp_style_expression_bodied_lambdas = true:silent 81 | csharp_style_expression_bodied_local_functions = false:silent 82 | csharp_style_expression_bodied_methods = false:silent 83 | csharp_style_expression_bodied_operators = false:silent 84 | csharp_style_expression_bodied_properties = true:silent 85 | 86 | # Pattern matching preferences 87 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 88 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 89 | csharp_style_prefer_not_pattern = true:suggestion 90 | csharp_style_prefer_pattern_matching = true:silent 91 | csharp_style_prefer_switch_expression = true:suggestion 92 | 93 | # Null-checking preferences 94 | csharp_style_conditional_delegate_call = true:suggestion 95 | 96 | # Modifier preferences 97 | csharp_prefer_static_local_function = true:suggestion 98 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent 99 | 100 | # Code-block preferences 101 | csharp_prefer_braces = true:silent 102 | csharp_prefer_simple_using_statement = true:suggestion 103 | 104 | # Expression-level preferences 105 | csharp_prefer_simple_default_expression = true:suggestion 106 | csharp_style_deconstructed_variable_declaration = true:suggestion 107 | csharp_style_inlined_variable_declaration = true:suggestion 108 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 109 | csharp_style_prefer_index_operator = true:suggestion 110 | csharp_style_prefer_range_operator = true:suggestion 111 | csharp_style_throw_expression = true:suggestion 112 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 113 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 114 | 115 | # 'using' directive preferences 116 | csharp_using_directive_placement = inside_namespace:silent 117 | 118 | #### C# Formatting Rules #### 119 | 120 | # New line preferences 121 | csharp_new_line_before_catch = true 122 | csharp_new_line_before_else = true 123 | csharp_new_line_before_finally = true 124 | csharp_new_line_before_members_in_anonymous_types = true 125 | csharp_new_line_before_members_in_object_initializers = true 126 | csharp_new_line_before_open_brace = all 127 | csharp_new_line_between_query_expression_clauses = true 128 | 129 | # Indentation preferences 130 | csharp_indent_block_contents = true 131 | csharp_indent_braces = false 132 | csharp_indent_case_contents = true 133 | csharp_indent_case_contents_when_block = true 134 | csharp_indent_labels = one_less_than_current 135 | csharp_indent_switch_labels = true 136 | 137 | # Space preferences 138 | csharp_space_after_cast = false 139 | csharp_space_after_colon_in_inheritance_clause = true 140 | csharp_space_after_comma = true 141 | csharp_space_after_dot = false 142 | csharp_space_after_keywords_in_control_flow_statements = true 143 | csharp_space_after_semicolon_in_for_statement = true 144 | csharp_space_around_binary_operators = before_and_after 145 | csharp_space_around_declaration_statements = false 146 | csharp_space_before_colon_in_inheritance_clause = true 147 | csharp_space_before_comma = false 148 | csharp_space_before_dot = false 149 | csharp_space_before_open_square_brackets = false 150 | csharp_space_before_semicolon_in_for_statement = false 151 | csharp_space_between_empty_square_brackets = false 152 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 153 | csharp_space_between_method_call_name_and_opening_parenthesis = false 154 | csharp_space_between_method_call_parameter_list_parentheses = false 155 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 156 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 157 | csharp_space_between_method_declaration_parameter_list_parentheses = false 158 | csharp_space_between_parentheses = false 159 | csharp_space_between_square_brackets = false 160 | 161 | # Wrapping preferences 162 | csharp_preserve_single_line_blocks = true 163 | csharp_preserve_single_line_statements = true 164 | 165 | #### Naming styles #### 166 | 167 | # Naming rules 168 | 169 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 170 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 171 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 172 | 173 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 174 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 175 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 176 | 177 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 178 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 179 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 180 | 181 | # Symbol specifications 182 | 183 | dotnet_naming_symbols.interface.applicable_kinds = interface 184 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 185 | dotnet_naming_symbols.interface.required_modifiers = 186 | 187 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 188 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 189 | dotnet_naming_symbols.types.required_modifiers = 190 | 191 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 192 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 193 | dotnet_naming_symbols.non_field_members.required_modifiers = 194 | 195 | # Naming styles 196 | 197 | dotnet_naming_style.pascal_case.required_prefix = 198 | dotnet_naming_style.pascal_case.required_suffix = 199 | dotnet_naming_style.pascal_case.word_separator = 200 | dotnet_naming_style.pascal_case.capitalization = pascal_case 201 | 202 | dotnet_naming_style.begins_with_i.required_prefix = I 203 | dotnet_naming_style.begins_with_i.required_suffix = 204 | dotnet_naming_style.begins_with_i.word_separator = 205 | dotnet_naming_style.begins_with_i.capitalization = pascal_case -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - "release/*" 7 | 8 | jobs: 9 | build: 10 | runs-on: windows-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup .NET Core 16 | uses: actions/setup-dotnet@v1 17 | 18 | - name: Build with dotnet 19 | run: dotnet build ./src/ --configuration Release 20 | 21 | deploy: 22 | needs: build 23 | runs-on: windows-latest 24 | 25 | steps: 26 | - name: Get Version 27 | id: get_version 28 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/release\//} 29 | shell: bash 30 | 31 | - uses: actions/checkout@v2 32 | 33 | - name: Pack NuGet package 34 | run: dotnet pack ./src/ 35 | --configuration Release 36 | -p:Version=${{ steps.get_version.outputs.VERSION }} 37 | -p:PackageId=Umbraco.Tools.Packages 38 | 39 | - name: Push package to NuGet 40 | run: dotnet nuget push **/*.nupkg 41 | --api-key ${{ secrets.NUGET_DEPLOY_KEY }} 42 | --source https://api.nuget.org/v3/index.json 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | .idea/ 3 | obj/ 4 | bin/ 5 | src/nupkg/ 6 | *.user -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NuGet release](https://img.shields.io/nuget/v/Umbraco.Tools.Packages.svg)](https://www.nuget.org/packages/Umbraco.Tools.Packages/) 2 | 3 | # UmbPack 4 | 5 | UmbPack is a CLI tool to use in CI/CD to upload Umbraco .zip packages to the [our.umbraco.com package repository](https://our.umbraco.com/packages/). 6 | 7 | If you are looking for info on how to use the tool, check out [the documentation](https://our.umbraco.com/documentation/Extending/Packages/UmbPack) for it instead! 8 | 9 | ## Building the tool 10 | 11 | This will create a Nuget package at `src/nupkg` 12 | 13 | ``` 14 | cd src 15 | dotnet pack -c Release 16 | ``` 17 | 18 | ## Installing the tool 19 | 20 | ``` 21 | dotnet tool install --global --add-source ./nupkg UmbPack 22 | ``` 23 | 24 | ### Uninstalling the tool 25 | 26 | ``` 27 | dotnet tool list --global 28 | dotnet tool uninstall --global UmbPack 29 | ``` 30 | -------------------------------------------------------------------------------- /src/Auth/AuthConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Umbraco.Packager.CI.Auth 2 | { 3 | public static class AuthConstants 4 | { 5 | public const string ProjectIdHeader = "OurUmbraco-ProjectId"; 6 | public const string MemberIdHeader = "OurUmbraco-MemberId"; 7 | #if DEBUG 8 | public const string BaseUrl = "http://localhost:24292"; 9 | #else 10 | public const string BaseUrl = "https://our.umbraco.com"; 11 | #endif 12 | } 13 | } -------------------------------------------------------------------------------- /src/Auth/HMACAuthentication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Net.Http; 4 | using System.Net.Http.Headers; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | using Umbraco.Packager.CI.Extensions; 8 | 9 | namespace Umbraco.Packager.CI.Auth 10 | { 11 | /// 12 | /// HMAC Authentication utilities 13 | /// 14 | public static class HMACAuthentication 15 | { 16 | public static string GetSignature(string requestUri, DateTime timestamp, Guid nonce, string secret) 17 | { 18 | return GetSignature(requestUri, timestamp.ToUnixTimestamp().ToString(CultureInfo.InvariantCulture), nonce.ToString(), secret); 19 | } 20 | 21 | private static string GetSignature(string requestUri, string timestamp, string nonce, string secret) 22 | { 23 | var secretBytes = Encoding.UTF8.GetBytes(secret); 24 | 25 | using (var hmac = new HMACSHA256(secretBytes)) 26 | { 27 | var signatureString = $"{requestUri}{timestamp}{nonce}"; 28 | var signatureBytes = Encoding.UTF8.GetBytes(signatureString); 29 | var computedHashBytes = hmac.ComputeHash(signatureBytes); 30 | var computedString = Convert.ToBase64String(computedHashBytes); 31 | return computedString; 32 | } 33 | } 34 | 35 | /// 36 | /// Returns the token authorization header value as a base64 encoded string 37 | /// 38 | /// 39 | /// 40 | /// 41 | /// 42 | public static string GenerateAuthorizationHeader(string signature, Guid nonce, DateTime timestamp) 43 | { 44 | return 45 | Convert.ToBase64String( 46 | Encoding.UTF8.GetBytes( 47 | $"{signature}:{nonce}:{timestamp.ToUnixTimestamp().ToString(CultureInfo.InvariantCulture)}")); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/Extensions/DateTimeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Umbraco.Packager.CI.Extensions 4 | { 5 | public static class DateTimeExtensions 6 | { 7 | /// 8 | /// Gets Unix Timestamp from DateTime object 9 | /// 10 | /// The DateTime object 11 | public static double ToUnixTimestamp(this DateTime dateTime) 12 | { 13 | return (TimeZoneInfo.ConvertTimeToUtc(dateTime) - 14 | new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace Umbraco.Packager.CI.Extensions 5 | { 6 | public static class StringExtensions 7 | { 8 | /// Converts an integer to an invariant formatted string 9 | /// 10 | /// 11 | public static string ToInvariantString(this int str) 12 | { 13 | return str.ToString((IFormatProvider) CultureInfo.InvariantCulture); 14 | } 15 | 16 | /// 17 | /// Ensures an input string starts with the supplied value 18 | /// 19 | /// 20 | /// 21 | /// 22 | public static string EnsureStartsWith(this string input, string toStartWith) 23 | { 24 | return input.StartsWith(toStartWith) ? input : toStartWith + input; 25 | } 26 | 27 | /// 28 | /// Ensures an input string ends with the supplied value 29 | /// 30 | /// 31 | /// 32 | /// 33 | public static string EnsureEndsWith(this string input, string toEndWith) 34 | { 35 | return input.EndsWith(toEndWith) ? input : input + toEndWith; 36 | } 37 | 38 | } 39 | } -------------------------------------------------------------------------------- /src/Extensions/UriExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Umbraco.Packager.CI.Extensions 4 | { 5 | public static class UriExtensions 6 | { 7 | internal static string CleanPathAndQuery(this Uri uri) 8 | { 9 | //sometimes the request path may have double slashes so make sure to normalize this 10 | return uri.PathAndQuery.Replace("//", "/"); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/PackageHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.IO.Compression; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Net.Http.Headers; 10 | using System.Threading.Tasks; 11 | using Newtonsoft.Json; 12 | using Newtonsoft.Json.Linq; 13 | using Umbraco.Packager.CI.Auth; 14 | using Umbraco.Packager.CI.Extensions; 15 | using Umbraco.Packager.CI.Properties; 16 | 17 | namespace Umbraco.Packager.CI 18 | { 19 | public class PackageHelper 20 | { 21 | private readonly IHttpClientFactory httpClientFactory; 22 | 23 | public PackageHelper(IHttpClientFactory httpClientFactory) 24 | { 25 | this.httpClientFactory = httpClientFactory; 26 | } 27 | 28 | /// 29 | /// verify that the package file exists at the specified location 30 | /// 31 | public void EnsurePackageExists(string packagePath) 32 | { 33 | if (File.Exists(packagePath) == false) 34 | { 35 | WriteError(Resources.Push_MissingFile, packagePath); 36 | Environment.Exit(2); // ERROR_FILE_NOT_FOUND=2 37 | } 38 | } 39 | 40 | /// 41 | /// confirm the package file a zip. 42 | /// 43 | public void EnsureIsZip(string packagePath) 44 | { 45 | if (Path.GetExtension(packagePath).ToLowerInvariant() != ".zip") 46 | { 47 | WriteError(Resources.Push_FileNotZip, packagePath); 48 | Environment.Exit(123); // ERROR_INVALID_NAME=123 49 | } 50 | } 51 | 52 | /// 53 | /// confirm that the zip file contains a package.xml file. 54 | /// 55 | public void EnsureContainsPackageXml(string packagePath) 56 | { 57 | using (var archive = ZipFile.OpenRead(packagePath)) 58 | { 59 | var packageXmlFileExists = archive.Entries.Any(x => string.Equals(x.Name, "package.xml", StringComparison.InvariantCultureIgnoreCase)); 60 | if (packageXmlFileExists == false) 61 | { 62 | WriteError(Resources.Push_NoPackageXml, packagePath); 63 | 64 | Environment.Exit(222); // ERROR_BAD_FILE_TYPE=222 65 | } 66 | } 67 | } 68 | 69 | /// 70 | /// returns an array of existing package files. 71 | /// 72 | public async Task GetPackageList(ApiKeyModel keyParts) 73 | { 74 | var url = "Umbraco/Api/ProjectUpload/GetProjectFiles"; 75 | try 76 | { 77 | using (var httpClient = GetClientBase(url, keyParts.Token, keyParts.MemberId, keyParts.ProjectId)) 78 | { 79 | var httpResponse = await httpClient.GetAsync(url); 80 | 81 | if (httpResponse.StatusCode == HttpStatusCode.Unauthorized) 82 | { 83 | WriteError(Resources.Push_ApiKeyInvalid); 84 | Environment.Exit(5); // ERROR_ACCESS_DENIED 85 | } 86 | else if (httpResponse.IsSuccessStatusCode) 87 | { 88 | // Get the JSON string content which gives us a list 89 | // of current Umbraco Package .zips for this project 90 | var apiResponse = await httpResponse.Content.ReadAsStringAsync(); 91 | return JsonConvert.DeserializeObject(apiResponse); 92 | } 93 | } 94 | } 95 | catch (HttpRequestException ex) 96 | { 97 | throw ex; 98 | } 99 | 100 | return null; 101 | } 102 | 103 | public void EnsurePackageDoesntAlreadyExists(JArray packages, string packageFile) 104 | { 105 | if (packages == null) return; 106 | 107 | var packageFileName = Path.GetFileName(packageFile); 108 | 109 | foreach(var package in packages) 110 | { 111 | var packageName = package.Value("Name"); 112 | if (packageName.Equals(packageFileName, StringComparison.InvariantCultureIgnoreCase)) 113 | { 114 | WriteError(Resources.Push_PackageExists, packageFileName); 115 | Environment.Exit(80); // FILE_EXISTS 116 | } 117 | } 118 | } 119 | 120 | public async Task ArchivePackages(ApiKeyModel keyParts, IEnumerable ids) 121 | { 122 | var url = "Umbraco/Api/ProjectUpload/ArchiveProjectFiles"; 123 | try 124 | { 125 | using (var httpClient = GetClientBase(url, keyParts.Token, keyParts.MemberId, keyParts.ProjectId)) 126 | { 127 | var httpResponse = await httpClient.PostAsJsonAsync(url, ids); 128 | 129 | if (httpResponse.StatusCode == HttpStatusCode.Unauthorized) 130 | { 131 | WriteError(Resources.Push_ApiKeyInvalid); 132 | Environment.Exit(5); // ERROR_ACCESS_DENIED 133 | } 134 | } 135 | } 136 | catch (HttpRequestException ex) 137 | { 138 | throw ex; 139 | } 140 | } 141 | 142 | public async Task GetCurrentPackageFileId(ApiKeyModel keyParts) 143 | { 144 | var url = "Umbraco/Api/ProjectUpload/GetCurrentPackageFileId"; 145 | try 146 | { 147 | using (var httpClient = GetClientBase(url, keyParts.Token, keyParts.MemberId, keyParts.ProjectId)) 148 | { 149 | var httpResponse = await httpClient.GetAsync(url); 150 | 151 | if (httpResponse.StatusCode == HttpStatusCode.Unauthorized) 152 | { 153 | WriteError(Resources.Push_ApiKeyInvalid); 154 | Environment.Exit(5); // ERROR_ACCESS_DENIED 155 | } 156 | 157 | var apiResponse = await httpResponse.Content.ReadAsStringAsync(); 158 | return apiResponse; 159 | } 160 | } 161 | catch (HttpRequestException ex) 162 | { 163 | throw ex; 164 | } 165 | } 166 | 167 | /// 168 | /// change the colour of the console, write an error and reset the colour back. 169 | /// 170 | public void WriteError(string error, params object[] parameters) 171 | { 172 | Console.ForegroundColor = ConsoleColor.Red; 173 | Console.Error.WriteLine(error, parameters); 174 | Console.ResetColor(); 175 | } 176 | 177 | /// 178 | /// the API key has the format "packageId-memberId-apiToken", this helper method splits it in the three parts and returns a model with them all 179 | /// 180 | public ApiKeyModel SplitKey(string apiKey) 181 | { 182 | var keyParts = apiKey.Split('-'); 183 | var keyModel = new ApiKeyModel(); 184 | 185 | if (int.TryParse(keyParts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int projectId)) 186 | { 187 | keyModel.ProjectId = projectId; 188 | } 189 | 190 | if (int.TryParse(keyParts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int memberId)) 191 | { 192 | keyModel.MemberId = memberId; 193 | } 194 | 195 | keyModel.Token = keyParts[2]; 196 | 197 | return keyModel; 198 | } 199 | 200 | /// 201 | /// basic http client with Bearer token setup. 202 | /// 203 | public HttpClient GetClientBase(string url, string apiKey, int memberId, int projectId) 204 | { 205 | 206 | var baseUrl = AuthConstants.BaseUrl; 207 | var client = httpClientFactory.CreateClient(); 208 | client.BaseAddress = new Uri(baseUrl); 209 | 210 | var requestPath = new Uri(client.BaseAddress + url).CleanPathAndQuery(); 211 | var timestamp = DateTime.UtcNow; 212 | var nonce = Guid.NewGuid(); 213 | 214 | var signature = HMACAuthentication.GetSignature(requestPath, timestamp, nonce, apiKey); 215 | var headerToken = HMACAuthentication.GenerateAuthorizationHeader(signature, nonce, timestamp); 216 | 217 | client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", headerToken); 218 | client.DefaultRequestHeaders.Add(AuthConstants.MemberIdHeader, memberId.ToInvariantString()); 219 | client.DefaultRequestHeaders.Add(AuthConstants.ProjectIdHeader, projectId.ToInvariantString()); 220 | 221 | return client; 222 | } 223 | } 224 | 225 | public class ApiKeyModel 226 | { 227 | public string Token { get; set; } 228 | public int ProjectId { get; set; } 229 | public int MemberId { get; set; } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/Parse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Compression; 3 | using System.Linq; 4 | using System.Xml; 5 | using Umbraco.Packager.CI.Properties; 6 | 7 | namespace Umbraco.Packager.CI 8 | { 9 | public static class Parse 10 | { 11 | public static PackageInfo PackageXml(string packagePath) 12 | { 13 | var packageDetails = new PackageInfo(); 14 | 15 | using (var archive = ZipFile.OpenRead(packagePath)) 16 | { 17 | var packageXmlFileExists = archive.Entries.Any(x => string.Equals(x.Name, "package.xml", StringComparison.InvariantCultureIgnoreCase)); 18 | if (packageXmlFileExists) 19 | { 20 | var xmlStream = archive.GetEntry("package.xml").Open(); 21 | 22 | var doc = new XmlDocument(); 23 | doc.Load(xmlStream); 24 | 25 | // Do some validation - check if //umbPackage/info/package exists 26 | // Throw error if not valid package.xml schema 27 | var packageInfo = doc.SelectSingleNode("//umbPackage/info/package"); 28 | 29 | if(packageInfo == null) 30 | { 31 | Console.ForegroundColor = ConsoleColor.Red; 32 | Console.Error.WriteLine(Resources.Push_InvalidXml); 33 | Console.Error.WriteLine(Resources.Push_MissingPackageNode); 34 | Console.ResetColor(); 35 | 36 | // ERROR_INVALID_FUNCTION 37 | Environment.Exit(1); 38 | } 39 | 40 | var packageName = packageInfo.SelectSingleNode("//name").InnerText; 41 | var packageVersion = packageInfo.SelectSingleNode("//version").InnerText; 42 | 43 | packageDetails.Name = packageName; 44 | packageDetails.VersionString = packageVersion; 45 | 46 | 47 | } 48 | } 49 | 50 | Console.WriteLine(Resources.Push_Extracting); 51 | Console.WriteLine($"Name: {packageDetails.Name}"); 52 | Console.WriteLine($"Version: {packageDetails.VersionString}\n"); 53 | 54 | return packageDetails; 55 | } 56 | } 57 | 58 | public class PackageInfo 59 | { 60 | public string Name { get; set; } 61 | 62 | public string VersionString { get; set; } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CommandLine; 6 | using CommandLine.Text; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using Umbraco.Packager.CI.Properties; 10 | using Umbraco.Packager.CI.Verbs; 11 | 12 | namespace Umbraco.Packager.CI 13 | { 14 | // Exit code conventions 15 | // https://docs.microsoft.com/en-gb/windows/win32/debug/system-error-codes--0-499-?redirectedfrom=MSDN 16 | 17 | public class Program 18 | { 19 | public static async Task Main(string[] args) 20 | { 21 | var builder = new HostBuilder() 22 | .ConfigureServices((hostContext, services) => 23 | { 24 | services.AddHttpClient(); 25 | services.AddTransient(); 26 | }).UseConsoleLifetime(); 27 | 28 | var host = builder.Build(); 29 | 30 | using (var serviceScope = host.Services.CreateScope()) 31 | { 32 | var services = serviceScope.ServiceProvider; 33 | await InternalMain(args, services.GetRequiredService()); 34 | 35 | } 36 | } 37 | 38 | private static async Task InternalMain(string[] args, PackageHelper packageHelper) 39 | { 40 | 41 | // now uses 'verbs' so each verb is a command 42 | // 43 | // e.g umbpack init or umbpack push 44 | // 45 | // these are handled by the Command classes. 46 | 47 | var parser = new Parser(with => { 48 | with.HelpWriter = null; 49 | // with.HelpWriter = Console.Out; 50 | with.AutoVersion = false; 51 | with.CaseSensitive = false; 52 | } ); 53 | 54 | // TODO: could load the verbs by interface or class 55 | 56 | var parserResults = parser.ParseArguments(args); 57 | 58 | parserResults 59 | .WithParsed(opts => PackCommand.RunAndReturn(opts).Wait()) 60 | .WithParsed(opts => PushCommand.RunAndReturn(opts, packageHelper).Wait()) 61 | .WithParsed(opts => InitCommand.RunAndReturn(opts)) 62 | .WithNotParsed(async errs => await DisplayHelp(parserResults, errs)); 63 | } 64 | 65 | private static async Task DisplayHelp(ParserResult result, IEnumerable errs) 66 | { 67 | var helpText = HelpText.AutoBuild(result, h => 68 | { 69 | h.AutoVersion = false; 70 | h.AutoHelp = false; 71 | return h; 72 | }, e => e, true); 73 | 74 | // Append header with Ascii Art 75 | helpText.Heading = Resources.Ascaii + Environment.NewLine + helpText.Heading; 76 | helpText.AddPostOptionsText(Resources.HelpFooter); 77 | Console.WriteLine(helpText); 78 | 79 | // --version or --help 80 | if (errs.IsVersion() || errs.IsHelp()) 81 | { 82 | // 0 is everything is all OK exit code 83 | Environment.Exit(0); 84 | } 85 | 86 | // ERROR_INVALID_FUNCTION 87 | Environment.Exit(1); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Properties/Defaults.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace UmbPack.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Defaults { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Defaults() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("UmbPack.Properties.Defaults", typeof(Defaults).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Another awesome package. 65 | /// 66 | internal static string Init_Description { 67 | get { 68 | return ResourceManager.GetString("Init_Description", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to MIT. 74 | /// 75 | internal static string Init_License { 76 | get { 77 | return ResourceManager.GetString("Init_License", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to yes. 83 | /// 84 | internal static string Init_Prompt { 85 | get { 86 | return ResourceManager.GetString("Init_Prompt", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to 8.0.0. 92 | /// 93 | internal static string Init_UmbracoVersion { 94 | get { 95 | return ResourceManager.GetString("Init_UmbracoVersion", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to https://our.umbraco.com. 101 | /// 102 | internal static string Init_Url { 103 | get { 104 | return ResourceManager.GetString("Init_Url", resourceCulture); 105 | } 106 | } 107 | 108 | /// 109 | /// Looks up a localized string similar to 1.0.0. 110 | /// 111 | internal static string Init_Version { 112 | get { 113 | return ResourceManager.GetString("Init_Version", resourceCulture); 114 | } 115 | } 116 | 117 | /// 118 | /// Looks up a localized string similar to https://our.umbraco.com. 119 | /// 120 | internal static string Init_Website { 121 | get { 122 | return ResourceManager.GetString("Init_Website", resourceCulture); 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Properties/Defaults.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Another awesome package 122 | 123 | 124 | MIT 125 | 126 | 127 | yes 128 | 129 | 130 | 8.0.0 131 | The default minimum version the package will work on 132 | 133 | 134 | https://our.umbraco.com 135 | 136 | 137 | 1.0.0 138 | 139 | 140 | https://our.umbraco.com 141 | 142 | -------------------------------------------------------------------------------- /src/Properties/HelpTextResource.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Umbraco.Packager.CI.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | public class HelpTextResource { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal HelpTextResource() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | public static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("UmbPack.Properties.HelpTextResource", typeof(HelpTextResource).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | public static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Initializes a new package.xml file from provided defaults 65 | /// 66 | ///Example usage: 67 | ///- umbpack init 68 | ///- umbpack init folder/subfolder. 69 | /// 70 | public static string HelpInit { 71 | get { 72 | return ResourceManager.GetString("HelpInit", resourceCulture); 73 | } 74 | } 75 | 76 | /// 77 | /// Looks up a localized string similar to Folder to save package.xml in (default current folder) 78 | ///The folder should exist prior to running the command. 79 | /// 80 | public static string HelpInitFolder { 81 | get { 82 | return ResourceManager.GetString("HelpInitFolder", resourceCulture); 83 | } 84 | } 85 | 86 | /// 87 | /// Looks up a localized string similar to Use nuspec file as a starting point for the package.xml file. 88 | /// 89 | public static string HelpInitNuspec { 90 | get { 91 | return ResourceManager.GetString("HelpInitNuspec", resourceCulture); 92 | } 93 | } 94 | 95 | /// 96 | /// Looks up a localized string similar to Create an Umbraco package from a folder or existing package.xml file. 97 | /// 98 | public static string HelpPack { 99 | get { 100 | return ResourceManager.GetString("HelpPack", resourceCulture); 101 | } 102 | } 103 | 104 | /// 105 | /// Looks up a localized string similar to The package.xml file or folder you want to create your package from. 106 | /// 107 | public static string HelpPackFile { 108 | get { 109 | return ResourceManager.GetString("HelpPackFile", resourceCulture); 110 | } 111 | } 112 | 113 | /// 114 | /// Looks up a localized string similar to Specified the directory the created package will be saved to. 115 | /// 116 | public static string HelpPackOutput { 117 | get { 118 | return ResourceManager.GetString("HelpPackOutput", resourceCulture); 119 | } 120 | } 121 | 122 | /// 123 | /// Looks up a localized string similar to An explicit file name to give the generated package file. 124 | /// 125 | public static string HelpPackPackageFileName { 126 | get { 127 | return ResourceManager.GetString("HelpPackPackageFileName", resourceCulture); 128 | } 129 | } 130 | 131 | /// 132 | /// Looks up a localized string similar to Properties to replace in the package.xml file. 133 | /// 134 | public static string HelpPackProperties { 135 | get { 136 | return ResourceManager.GetString("HelpPackProperties", resourceCulture); 137 | } 138 | } 139 | 140 | /// 141 | /// Looks up a localized string similar to Override the version defined in the package.xml file. 142 | /// 143 | public static string HelpPackVersion { 144 | get { 145 | return ResourceManager.GetString("HelpPackVersion", resourceCulture); 146 | } 147 | } 148 | 149 | /// 150 | /// Looks up a localized string similar to Push an Umbraco package to our.umbraco.com. 151 | /// 152 | public static string HelpPush { 153 | get { 154 | return ResourceManager.GetString("HelpPush", resourceCulture); 155 | } 156 | } 157 | 158 | /// 159 | /// Looks up a localized string similar to One or more wildcard patterns to match against existing package files to be archived. 160 | /// 161 | public static string HelpPushArchive { 162 | get { 163 | return ResourceManager.GetString("HelpPushArchive", resourceCulture); 164 | } 165 | } 166 | 167 | /// 168 | /// Looks up a localized string similar to Make this package the current package file. 169 | /// 170 | public static string HelpPushCurrent { 171 | get { 172 | return ResourceManager.GetString("HelpPushCurrent", resourceCulture); 173 | } 174 | } 175 | 176 | /// 177 | /// Looks up a localized string similar to Change the required DotNetVersion for the package. 178 | /// 179 | public static string HelpPushDotNet { 180 | get { 181 | return ResourceManager.GetString("HelpPushDotNet", resourceCulture); 182 | } 183 | } 184 | 185 | /// 186 | /// Looks up a localized string similar to Api Key to use . 187 | /// 188 | public static string HelpPushKey { 189 | get { 190 | return ResourceManager.GetString("HelpPushKey", resourceCulture); 191 | } 192 | } 193 | 194 | /// 195 | /// Looks up a localized string similar to Path to the package.zip you wish to push. 196 | /// 197 | public static string HelpPushPackage { 198 | get { 199 | return ResourceManager.GetString("HelpPushPackage", resourceCulture); 200 | } 201 | } 202 | 203 | /// 204 | /// Looks up a localized string similar to Compatible Umbraco versions (in the form v850,v840,v830). 205 | /// 206 | public static string HelpPushWorks { 207 | get { 208 | return ResourceManager.GetString("HelpPushWorks", resourceCulture); 209 | } 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Properties/HelpTextResource.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Initializes a new package.xml file from provided defaults 122 | 123 | Example usage: 124 | - umbpack init 125 | - umbpack init folder/subfolder 126 | 127 | 128 | Folder to save package.xml in (default current folder) 129 | The folder should exist prior to running the command 130 | 131 | 132 | Use nuspec file as a starting point for the package.xml file 133 | 134 | 135 | Create an Umbraco package from a folder or existing package.xml file 136 | 137 | 138 | The package.xml file or folder you want to create your package from 139 | 140 | 141 | Specified the directory the created package will be saved to 142 | 143 | 144 | Override the version defined in the package.xml file 145 | 146 | 147 | Push an Umbraco package to our.umbraco.com 148 | 149 | 150 | Make this package the current package file 151 | 152 | 153 | One or more wildcard patterns to match against existing package files to be archived 154 | 155 | 156 | Change the required DotNetVersion for the package 157 | 158 | 159 | Api Key to use 160 | 161 | 162 | Path to the package.zip you wish to push 163 | 164 | 165 | Compatible Umbraco versions (in the form v850,v840,v830) 166 | 167 | 168 | Properties to replace in the package.xml file 169 | 170 | 171 | An explicit file name to give the generated package file 172 | 173 | -------------------------------------------------------------------------------- /src/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace UmbPack.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("UmbPack.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to 65 | /// 888 66 | /// 888 67 | ///888 888 88888b.d88b. 88888b. 888d888 8888b. .d8888b .d88b. 68 | ///888 888 888 "888 "88b 888 "88b 888P" "88b d88P" d88""88b 69 | ///888 888 888 888 888 888 888 888 .d888888 888 888 888 70 | ///Y88 88Y 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P 71 | /// "Y888P" 888 888 888 88888P" 888 "Y888888 "Y8888P "Y88P" 72 | /// 73 | ///------------------------------------------------------------------ 74 | /// . 75 | /// 76 | internal static string Ascaii { 77 | get { 78 | return ResourceManager.GetString("Ascaii", resourceCulture); 79 | } 80 | } 81 | 82 | /// 83 | /// Looks up a localized string similar to Error: {0}. 84 | /// 85 | internal static string Error { 86 | get { 87 | return ResourceManager.GetString("Error", resourceCulture); 88 | } 89 | } 90 | 91 | /// 92 | /// Looks up a localized string similar to For more information see https://our.umbraco.com/documentation/Extending/Packages/UmbPack . 93 | /// 94 | internal static string HelpFooter { 95 | get { 96 | return ResourceManager.GetString("HelpFooter", resourceCulture); 97 | } 98 | } 99 | 100 | /// 101 | /// Looks up a localized string similar to Author. 102 | /// 103 | internal static string Init_Author { 104 | get { 105 | return ResourceManager.GetString("Init.Author", resourceCulture); 106 | } 107 | } 108 | 109 | /// 110 | /// Looks up a localized string similar to Package file saved. 111 | /// 112 | internal static string Init_Complete { 113 | get { 114 | return ResourceManager.GetString("Init.Complete", resourceCulture); 115 | } 116 | } 117 | 118 | /// 119 | /// Looks up a localized string similar to About to write to {0}. 120 | /// 121 | internal static string Init_Confirm { 122 | get { 123 | return ResourceManager.GetString("Init.Confirm", resourceCulture); 124 | } 125 | } 126 | 127 | /// 128 | /// Looks up a localized string similar to Contributors. 129 | /// 130 | internal static string Init_Contributors { 131 | get { 132 | return ResourceManager.GetString("Init.Contributors", resourceCulture); 133 | } 134 | } 135 | 136 | /// 137 | /// Looks up a localized string similar to Description. 138 | /// 139 | internal static string Init_Description { 140 | get { 141 | return ResourceManager.GetString("Init.Description", resourceCulture); 142 | } 143 | } 144 | 145 | /// 146 | /// Looks up a localized string similar to This utility will walk you through creating a package.xml file. 147 | ///It only covers the most common items, and tries to guess sensible defaults. 148 | /// 149 | ///See https://our.umbraco.com/documentation/Extending/Packages/Creating-a-Package/ for more info on what the fields mean.. 150 | /// 151 | internal static string Init_Header { 152 | get { 153 | return ResourceManager.GetString("Init.Header", resourceCulture); 154 | } 155 | } 156 | 157 | /// 158 | /// Looks up a localized string similar to Invalid Version: '{0}'. 159 | /// 160 | internal static string Init_InvalidVersion { 161 | get { 162 | return ResourceManager.GetString("Init.InvalidVersion", resourceCulture); 163 | } 164 | } 165 | 166 | /// 167 | /// Looks up a localized string similar to License. 168 | /// 169 | internal static string Init_License { 170 | get { 171 | return ResourceManager.GetString("Init.License", resourceCulture); 172 | } 173 | } 174 | 175 | /// 176 | /// Looks up a localized string similar to The location '{0}' where you want to put the package file doesn't exist.. 177 | /// 178 | internal static string Init_MissingFolder { 179 | get { 180 | return ResourceManager.GetString("Init.MissingFolder", resourceCulture); 181 | } 182 | } 183 | 184 | /// 185 | /// Looks up a localized string similar to Package Name. 186 | /// 187 | internal static string Init_PackageName { 188 | get { 189 | return ResourceManager.GetString("Init.PackageName", resourceCulture); 190 | } 191 | } 192 | 193 | /// 194 | /// Looks up a localized string similar to Is this OK?. 195 | /// 196 | internal static string Init_Prompt { 197 | get { 198 | return ResourceManager.GetString("Init.Prompt", resourceCulture); 199 | } 200 | } 201 | 202 | /// 203 | /// Looks up a localized string similar to Umbraco Version. 204 | /// 205 | internal static string Init_UmbracoVersion { 206 | get { 207 | return ResourceManager.GetString("Init.UmbracoVersion", resourceCulture); 208 | } 209 | } 210 | 211 | /// 212 | /// Looks up a localized string similar to Url. 213 | /// 214 | internal static string Init_Url { 215 | get { 216 | return ResourceManager.GetString("Init.Url", resourceCulture); 217 | } 218 | } 219 | 220 | /// 221 | /// Looks up a localized string similar to Version. 222 | /// 223 | internal static string Init_Version { 224 | get { 225 | return ResourceManager.GetString("Init.Version", resourceCulture); 226 | } 227 | } 228 | 229 | /// 230 | /// Looks up a localized string similar to Website. 231 | /// 232 | internal static string Init_Website { 233 | get { 234 | return ResourceManager.GetString("Init.Website", resourceCulture); 235 | } 236 | } 237 | 238 | /// 239 | /// Looks up a localized string similar to - Adding Folder: {0}. 240 | /// 241 | internal static string Pack_AddingFolder { 242 | get { 243 | return ResourceManager.GetString("Pack.AddingFolder", resourceCulture); 244 | } 245 | } 246 | 247 | /// 248 | /// Looks up a localized string similar to - Adding: {0} to {1}. 249 | /// 250 | internal static string Pack_AddingSingle { 251 | get { 252 | return ResourceManager.GetString("Pack.AddingSingle", resourceCulture); 253 | } 254 | } 255 | 256 | /// 257 | /// Looks up a localized string similar to - Adding files to package. 258 | /// 259 | internal static string Pack_AddPackageFiles { 260 | get { 261 | return ResourceManager.GetString("Pack.AddPackageFiles", resourceCulture); 262 | } 263 | } 264 | 265 | /// 266 | /// Looks up a localized string similar to Building package from package.xml. 267 | /// 268 | internal static string Pack_BuildingFile { 269 | get { 270 | return ResourceManager.GetString("Pack.BuildingFile", resourceCulture); 271 | } 272 | } 273 | 274 | /// 275 | /// Looks up a localized string similar to Building package from folder: {0}. 276 | /// 277 | internal static string Pack_BuildingFolder { 278 | get { 279 | return ResourceManager.GetString("Pack.BuildingFolder", resourceCulture); 280 | } 281 | } 282 | 283 | /// 284 | /// Looks up a localized string similar to - Building package. 285 | /// 286 | internal static string Pack_BuildPackage { 287 | get { 288 | return ResourceManager.GetString("Pack.BuildPackage", resourceCulture); 289 | } 290 | } 291 | 292 | /// 293 | /// Looks up a localized string similar to Complete. 294 | /// 295 | internal static string Pack_Complete { 296 | get { 297 | return ResourceManager.GetString("Pack.Complete", resourceCulture); 298 | } 299 | } 300 | 301 | /// 302 | /// Looks up a localized string similar to Build Directory {0} doesn't exist. 303 | /// 304 | internal static string Pack_DirectoryMissing { 305 | get { 306 | return ResourceManager.GetString("Pack.DirectoryMissing", resourceCulture); 307 | } 308 | } 309 | 310 | /// 311 | /// Looks up a localized string similar to - Getting Package Name. 312 | /// 313 | internal static string Pack_GetPackageName { 314 | get { 315 | return ResourceManager.GetString("Pack.GetPackageName", resourceCulture); 316 | } 317 | } 318 | 319 | /// 320 | /// Looks up a localized string similar to - Loading Package File: {0}. 321 | /// 322 | internal static string Pack_LoadingFile { 323 | get { 324 | return ResourceManager.GetString("Pack.LoadingFile", resourceCulture); 325 | } 326 | } 327 | 328 | /// 329 | /// Looks up a localized string similar to Unable to locate the package.xml file {0}. 330 | /// 331 | internal static string Pack_MissingXml { 332 | get { 333 | return ResourceManager.GetString("Pack.MissingXml", resourceCulture); 334 | } 335 | } 336 | 337 | /// 338 | /// Looks up a localized string similar to - Saving Package to {0}. 339 | /// 340 | internal static string Pack_SavingPackage { 341 | get { 342 | return ResourceManager.GetString("Pack.SavingPackage", resourceCulture); 343 | } 344 | } 345 | 346 | /// 347 | /// Looks up a localized string similar to - Updating package version. 348 | /// 349 | internal static string Pack_UpdatingVersion { 350 | get { 351 | return ResourceManager.GetString("Pack.UpdatingVersion", resourceCulture); 352 | } 353 | } 354 | 355 | /// 356 | /// Looks up a localized string similar to Api Key is invalid. 357 | /// 358 | internal static string Push_ApiKeyInvalid { 359 | get { 360 | return ResourceManager.GetString("Push.ApiKeyInvalid", resourceCulture); 361 | } 362 | } 363 | 364 | /// 365 | /// Looks up a localized string similar to The package '{0}' was successfully uploaded to our.umbraco.com. 366 | /// 367 | internal static string Push_Complete { 368 | get { 369 | return ResourceManager.GetString("Push.Complete", resourceCulture); 370 | } 371 | } 372 | 373 | /// 374 | /// Looks up a localized string similar to Extracting info from package.xml file. 375 | /// 376 | internal static string Push_Extracting { 377 | get { 378 | return ResourceManager.GetString("Push.Extracting", resourceCulture); 379 | } 380 | } 381 | 382 | /// 383 | /// Looks up a localized string similar to Umbraco package file '{0}' must be a .zip. 384 | /// 385 | internal static string Push_FileNotZip { 386 | get { 387 | return ResourceManager.GetString("Push.FileNotZip", resourceCulture); 388 | } 389 | } 390 | 391 | /// 392 | /// Looks up a localized string similar to Invalid package.xml. 393 | /// 394 | internal static string Push_InvalidXml { 395 | get { 396 | return ResourceManager.GetString("Push.InvalidXml", resourceCulture); 397 | } 398 | } 399 | 400 | /// 401 | /// Looks up a localized string similar to Cannot find file {0}. 402 | /// 403 | internal static string Push_MissingFile { 404 | get { 405 | return ResourceManager.GetString("Push.MissingFile", resourceCulture); 406 | } 407 | } 408 | 409 | /// 410 | /// Looks up a localized string similar to Unable to find //umbPackage/info/package in XML. 411 | /// 412 | internal static string Push_MissingPackageNode { 413 | get { 414 | return ResourceManager.GetString("Push.MissingPackageNode", resourceCulture); 415 | } 416 | } 417 | 418 | /// 419 | /// Looks up a localized string similar to Umbraco package file '{0}' does not contain a package.xml file. 420 | /// 421 | internal static string Push_NoPackageXml { 422 | get { 423 | return ResourceManager.GetString("Push.NoPackageXml", resourceCulture); 424 | } 425 | } 426 | 427 | /// 428 | /// Looks up a localized string similar to A package file named '{0}' already exists for this package. 429 | /// 430 | internal static string Push_PackageExists { 431 | get { 432 | return ResourceManager.GetString("Push.PackageExists", resourceCulture); 433 | } 434 | } 435 | 436 | /// 437 | /// Looks up a localized string similar to Uploading {0} to our.umbraco.com .... 438 | /// 439 | internal static string Push_Uploading { 440 | get { 441 | return ResourceManager.GetString("Push.Uploading", resourceCulture); 442 | } 443 | } 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /src/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | 888 123 | 888 124 | 888 888 88888b.d88b. 88888b. 888d888 8888b. .d8888b .d88b. 125 | 888 888 888 "888 "88b 888 "88b 888P" "88b d88P" d88""88b 126 | 888 888 888 888 888 888 888 888 .d888888 888 888 888 127 | Y88 88Y 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P 128 | "Y888P" 888 888 888 88888P" 888 "Y888888 "Y8888P "Y88P" 129 | 130 | ------------------------------------------------------------------ 131 | 132 | 133 | 134 | Error: {0} 135 | 136 | 137 | For more information see https://our.umbraco.com/documentation/Extending/Packages/UmbPack 138 | 139 | 140 | Author 141 | 142 | 143 | Contributors 144 | 145 | 146 | Package file saved 147 | 148 | 149 | About to write to {0} 150 | 151 | 152 | Description 153 | 154 | 155 | This utility will walk you through creating a package.xml file. 156 | It only covers the most common items, and tries to guess sensible defaults. 157 | 158 | See https://our.umbraco.com/documentation/Extending/Packages/Creating-a-Package/ for more info on what the fields mean. 159 | 160 | 161 | Invalid Version: '{0}' 162 | 163 | 164 | License 165 | 166 | 167 | The location '{0}' where you want to put the package file doesn't exist. 168 | 169 | 170 | Package Name 171 | 172 | 173 | Is this OK? 174 | 175 | 176 | Umbraco Version 177 | 178 | 179 | Url 180 | 181 | 182 | Version 183 | 184 | 185 | Website 186 | 187 | 188 | - Adding Folder: {0} 189 | 190 | 191 | - Adding: {0} to {1} 192 | 193 | 194 | - Adding files to package 195 | 196 | 197 | Building package from package.xml 198 | 199 | 200 | Building package from folder: {0} 201 | 202 | 203 | - Building package 204 | 205 | 206 | Complete 207 | 208 | 209 | Build Directory {0} doesn't exist 210 | 211 | 212 | - Getting Package Name 213 | 214 | 215 | - Loading Package File: {0} 216 | 217 | 218 | Unable to locate the package.xml file {0} 219 | 220 | 221 | - Saving Package to {0} 222 | 223 | 224 | - Updating package version 225 | 226 | 227 | Api Key is invalid 228 | 229 | 230 | The package '{0}' was successfully uploaded to our.umbraco.com 231 | 232 | 233 | Extracting info from package.xml file 234 | 235 | 236 | Umbraco package file '{0}' must be a .zip 237 | 238 | 239 | Invalid package.xml 240 | 241 | 242 | Cannot find file {0} 243 | 244 | 245 | Unable to find //umbPackage/info/package in XML 246 | 247 | 248 | Umbraco package file '{0}' does not contain a package.xml file 249 | 250 | 251 | A package file named '{0}' already exists for this package 252 | 253 | 254 | Uploading {0} to our.umbraco.com ... 255 | 256 | 257 | -------------------------------------------------------------------------------- /src/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Umbraco.Packager.CI": { 4 | "commandName": "Project", 5 | "commandLineArgs": "" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/UmbPack.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.0 6 | 7 | true 8 | UmbPack 9 | ./nupkg 10 | UmbPack 11 | UmbPack 12 | 1.0.0.0 13 | 1.0.0.0 14 | UmbPack 15 | 16 | UmbPack 17 | UmbPack is a CLI tool for building and deploying Umbraco packages 18 | The Umbraco Community 19 | https://github.com/umbraco/UmbPack 20 | umbraco 21 | https://github.com/umbraco/UmbPack 22 | Git 23 | 24 | true 25 | snupkg 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | True 40 | True 41 | Defaults.resx 42 | 43 | 44 | True 45 | True 46 | HelpTextResource.resx 47 | 48 | 49 | True 50 | True 51 | Resources.resx 52 | 53 | 54 | 55 | 56 | 57 | ResXFileCodeGenerator 58 | Defaults.Designer.cs 59 | 60 | 61 | PublicResXFileCodeGenerator 62 | HelpTextResource.Designer.cs 63 | 64 | 65 | ResXFileCodeGenerator 66 | Resources.Designer.cs 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/UmbPack.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29411.108 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UmbPack", "UmbPack.csproj", "{BD556FC7-85C5-4F7E-A949-115993684BDE}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{019F938D-7521-458E-A4AA-CD06C17B2300}" 9 | ProjectSection(SolutionItems) = preProject 10 | ..\.editorconfig = ..\.editorconfig 11 | ..\.gitignore = ..\.gitignore 12 | ..\README.md = ..\README.md 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {BD556FC7-85C5-4F7E-A949-115993684BDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {BD556FC7-85C5-4F7E-A949-115993684BDE}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {BD556FC7-85C5-4F7E-A949-115993684BDE}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {BD556FC7-85C5-4F7E-A949-115993684BDE}.Release|Any CPU.Build.0 = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(SolutionProperties) = preSolution 27 | HideSolutionNode = FALSE 28 | EndGlobalSection 29 | GlobalSection(ExtensibilityGlobals) = postSolution 30 | SolutionGuid = {D1F77990-EAEB-4F32-9AC3-0417D67F66FA} 31 | EndGlobalSection 32 | EndGlobal 33 | -------------------------------------------------------------------------------- /src/Verbs/InitCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.Xml.Linq; 6 | 7 | using CommandLine; 8 | 9 | using Semver; 10 | 11 | using Umbraco.Packager.CI.Properties; 12 | 13 | namespace Umbraco.Packager.CI.Verbs 14 | { 15 | 16 | /// 17 | /// Command line options for the Init verb 18 | /// 19 | [Verb("init", HelpText = "HelpInit", ResourceType = typeof(HelpTextResource))] 20 | public class InitOptions 21 | { 22 | [Value(0, 23 | MetaName = "Folder", 24 | HelpText = "HelpInitFolder", ResourceType = typeof(HelpTextResource))] 25 | public string Folder { get; set; } 26 | 27 | /* (not supportd yet) 28 | [Option("nuspec", 29 | HelpText = "HelpTextNuspec", 30 | ResourceType = typeof(HelpTextResource))] 31 | public string NuSpecFile { get; set; } 32 | */ 33 | } 34 | 35 | 36 | /// 37 | /// Init command, asks the user some questions makes a package.xml file 38 | /// 39 | /// 40 | /// Works like npm init, makes some guesses as to what defaults to use 41 | /// and lets the user enter values, at the end it writes out a package.xml file 42 | /// 43 | internal static class InitCommand 44 | { 45 | public static void RunAndReturn(InitOptions options) 46 | { 47 | var path = string.IsNullOrWhiteSpace(options.Folder) ? "." : options.Folder; 48 | 49 | var currentFolder = new DirectoryInfo(path); 50 | 51 | var packageFile = GetPackageFile(options.Folder); 52 | 53 | var setup = new PackageSetup(); 54 | 55 | Console.WriteLine(Resources.Init_Header); 56 | Console.WriteLine(); 57 | 58 | // gather all the user input 59 | 60 | setup.Name = GetUserInput(Resources.Init_PackageName, Path.GetFileName(currentFolder.Name)); 61 | 62 | setup.Description = GetUserInput(Resources.Init_Description, Defaults.Init_Description); 63 | 64 | setup.Version = GetVersionString(Resources.Init_Version, Defaults.Init_Version); 65 | 66 | setup.Url = GetUserInput(Resources.Init_Url, Defaults.Init_Url); 67 | 68 | setup.UmbracoVersion = GetVersionString(Resources.Init_UmbracoVersion, Defaults.Init_UmbracoVersion); 69 | 70 | setup.Author = GetUserInput(Resources.Init_Author, Environment.UserName); 71 | 72 | setup.Website = GetUserInput(Resources.Init_Website, Defaults.Init_Website); 73 | 74 | setup.License = GetUserInput(Resources.Init_License, Defaults.Init_License); 75 | 76 | setup.Contributors = GetUserInput(Resources.Init_Contributors, null); 77 | 78 | // play it back for confirmation 79 | Console.WriteLine(); 80 | Console.WriteLine(Resources.Init_Confirm, packageFile); 81 | 82 | var node = MakePackageFile(setup); 83 | 84 | Console.WriteLine(node.Element("info").ToString()); 85 | 86 | // confirm 87 | 88 | var confirm = GetUserInput(Resources.Init_Prompt, Defaults.Init_Prompt).ToUpper(); 89 | if (confirm[0] == 'Y') 90 | { 91 | // save xml to disk. 92 | 93 | node.Save(packageFile); 94 | Console.WriteLine(Resources.Init_Complete); 95 | Environment.Exit(0); 96 | } 97 | else 98 | { 99 | Environment.Exit(1); 100 | } 101 | } 102 | 103 | /// 104 | /// Make a package xml from the options 105 | /// 106 | /// options entered by the user 107 | /// XElement containing the package.xml info 108 | private static XElement MakePackageFile(PackageSetup options) 109 | { 110 | var node = new XElement("umbPackage"); 111 | 112 | var info = new XElement("info"); 113 | 114 | var package = new XElement("package"); 115 | package.Add(new XElement("name", options.Name)); 116 | package.Add(new XElement("version", options.Version)); 117 | package.Add(new XElement("iconUrl", "")); 118 | package.Add(new XElement("license", options.License, 119 | new XAttribute("url", GetLicenseUrl(options.License)))); 120 | 121 | package.Add(new XElement("url", options.Url)); 122 | package.Add(new XElement("requirements", 123 | new XAttribute("type", "strict"), 124 | new XElement("major", options.UmbracoVersion.Major), 125 | new XElement("minor", options.UmbracoVersion.Minor), 126 | new XElement("patch", options.UmbracoVersion.Patch))); 127 | info.Add(package); 128 | 129 | info.Add(new XElement("author", 130 | new XElement("name", options.Author), 131 | new XElement("website", options.Website))); 132 | 133 | var contributors = options.Contributors?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) 134 | .Where(x => !string.IsNullOrWhiteSpace(x)) 135 | .Select(s => s.Trim()) 136 | .ToArray(); 137 | 138 | if (contributors?.Length > 0) 139 | { 140 | info.Add(new XElement("contributors", 141 | contributors.Select(c => new XElement("contributor", c)) 142 | )); 143 | } 144 | 145 | info.Add(new XElement("readme", 146 | new XCData(options.Description))); 147 | 148 | node.Add(info); 149 | 150 | node.Add(new XElement("files")); 151 | node.Add(new XElement("Actions")); 152 | node.Add(new XElement("control")); 153 | node.Add(new XElement("DocumentTypes")); 154 | node.Add(new XElement("Templates")); 155 | node.Add(new XElement("Stylesheets")); 156 | node.Add(new XElement("Macros")); 157 | node.Add(new XElement("DictionaryItems")); 158 | node.Add(new XElement("Languages")); 159 | node.Add(new XElement("DataTypes")); 160 | 161 | return node; 162 | } 163 | 164 | /// 165 | /// Workout the URL for the license based on the string value 166 | /// 167 | /// License Name (e.g MIT) 168 | /// URL for the license file 169 | private static string GetLicenseUrl(string licenseName) 170 | { 171 | // TODO - get license urls from somewhere? 172 | if (licenseName.Equals("MIT", StringComparison.InvariantCultureIgnoreCase)) 173 | { 174 | return "https://opensource.org/licenses/MIT"; 175 | } 176 | 177 | return string.Empty; 178 | } 179 | 180 | /// 181 | /// Prompts the user for version string and validates it. 182 | /// 183 | /// text to put in prompt 184 | /// default value if user just presses enter 185 | /// SemVersion compatible version 186 | private static SemVersion GetVersionString(string prompt, string defaultValue) 187 | { 188 | while (true) 189 | { 190 | var versionString = GetUserInput(prompt, defaultValue); 191 | if (SemVersion.TryParse(versionString, out SemVersion version)) 192 | { 193 | return version; 194 | } 195 | else 196 | { 197 | Console.WriteLine(Resources.Init_InvalidVersion, versionString); 198 | } 199 | } 200 | } 201 | 202 | /// 203 | /// Prompt the user for some input, return a default value if they just press enter 204 | /// 205 | /// Prompt for user 206 | /// Default value if they just press enter 207 | /// user value or default 208 | private static string GetUserInput(string prompt, string defaultValue) 209 | { 210 | Console.Write($"{prompt}: "); 211 | if (!string.IsNullOrWhiteSpace(defaultValue)) 212 | { 213 | Console.Write($"({defaultValue}) "); 214 | } 215 | 216 | var value = Console.ReadLine(); 217 | if (string.IsNullOrWhiteSpace(value)) 218 | return defaultValue; 219 | 220 | return value; 221 | } 222 | 223 | /// 224 | /// Validates the path to where the package file is going to be created 225 | /// 226 | /// path to a package file 227 | private static string GetPackageFile(string packageFile) 228 | { 229 | var filePath = Path.Combine(".", "package.xml"); 230 | 231 | if (!string.IsNullOrWhiteSpace(packageFile)) 232 | { 233 | if (Path.HasExtension(packageFile)) 234 | { 235 | filePath = packageFile; 236 | } 237 | else 238 | { 239 | filePath = Path.Combine(packageFile, "package.xml"); 240 | } 241 | } 242 | 243 | if (!Directory.Exists(Path.GetDirectoryName(filePath))) 244 | { 245 | Console.WriteLine(Resources.Init_MissingFolder, Path.GetDirectoryName(filePath)); 246 | Environment.Exit(2); 247 | } 248 | 249 | return filePath; 250 | 251 | } 252 | 253 | /// 254 | /// Package Setup options 255 | /// 256 | /// 257 | /// Options that are used in building the package.xml file 258 | /// 259 | private class PackageSetup 260 | { 261 | public string Name { get; set; } 262 | public SemVersion Version { get; set; } 263 | public string Url { get; set; } 264 | public string Author { get; set; } 265 | public string Website { get; set; } 266 | public string License { get; set; } 267 | public string Contributors { get; set; } 268 | public SemVersion UmbracoVersion { get; set; } 269 | public string Description { get; set; } 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/Verbs/PackCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using System.Xml.Linq; 7 | 8 | using CommandLine; 9 | using Umbraco.Packager.CI.Extensions; 10 | using Umbraco.Packager.CI.Properties; 11 | 12 | namespace Umbraco.Packager.CI.Verbs 13 | { 14 | [Verb("pack", HelpText = "HelpPack", ResourceType = typeof(HelpTextResource))] 15 | public class PackOptions 16 | { 17 | [Value(0, MetaName = "file/folder", Required = true, 18 | HelpText = "HelpPackFile", ResourceType = typeof(HelpTextResource))] 19 | public string FolderOrFile { get; set; } 20 | 21 | [Option('o', "OutputDirectory", 22 | HelpText = "HelpPackOutput", 23 | ResourceType = typeof(HelpTextResource), 24 | Default = ".")] 25 | public string OutputDirectory { get; set; } 26 | 27 | [Option('v', "Version", 28 | HelpText = "HelpPackVersion", 29 | ResourceType = typeof(HelpTextResource))] 30 | public string Version { get; set; } 31 | 32 | [Option('p', "Properties", 33 | HelpText = "HelpPackProperties", 34 | ResourceType = typeof(HelpTextResource))] 35 | public string Properties { get; set; } 36 | 37 | [Option('n', "PackageFileName", 38 | HelpText = "HelpPackPackageFileName", 39 | ResourceType = typeof(HelpTextResource))] 40 | public string PackageFileName { get; set; } 41 | } 42 | 43 | 44 | /// 45 | /// Pack command, lets you create a package.zip either from a package.xml or a folder 46 | /// 47 | /// 48 | /// you can mark up the package.xml file with a couple of extra things 49 | /// and when its processed by this command, to make the full version 50 | /// 51 | /// Example: 52 | /// 54 | /// 55 | /// 56 | /// 57 | /// ]]> 58 | /// 59 | /// this would copy the file to the bin folder in the package 60 | /// and copy the content of the folder to the App_Plugins folder 61 | /// the structure of the folder beneath this level is also preserved. 62 | /// 63 | internal static class PackCommand 64 | { 65 | public async static Task RunAndReturn(PackOptions options) 66 | { 67 | // make sure the output directory exists 68 | Directory.CreateDirectory(options.OutputDirectory); 69 | 70 | // working dir, is where we build a structure of what the package will do 71 | var workingDir = CreateWorkingFolder(options.OutputDirectory, "__umbpack__tmp"); 72 | 73 | // buildfolder is the things we zip up. 74 | var buildFolder = CreateWorkingFolder(options.OutputDirectory, "__umbpack__build"); 75 | 76 | /* 77 | Console.WriteLine("Option: {0}", options.FolderOrFile); 78 | Console.WriteLine("Output Folder: {0}", options.OutputDirectory); 79 | Console.WriteLine("Working Folder: {0}", workingDir); 80 | Console.WriteLine("Build Folder: {0}", buildFolder); 81 | */ 82 | 83 | var packageFile = options.FolderOrFile; 84 | bool isFolder = false; 85 | 86 | if (!Path.GetExtension(options.FolderOrFile).Equals(".xml", StringComparison.InvariantCultureIgnoreCase)) 87 | { 88 | // a folder - we assume the package.xml is in that folder 89 | isFolder = true; 90 | packageFile = Path.Combine(options.FolderOrFile, "package.xml"); 91 | Console.WriteLine(Resources.Pack_BuildingFolder, options.FolderOrFile); 92 | } 93 | else 94 | { 95 | Console.WriteLine(Resources.Pack_BuildingFile); 96 | } 97 | 98 | if (!File.Exists(packageFile)) 99 | { 100 | Console.WriteLine(Resources.Pack_MissingXml, packageFile); 101 | Environment.Exit(2); 102 | } 103 | 104 | Console.WriteLine(Resources.Pack_LoadingFile, packageFile); 105 | 106 | // load the package xml 107 | XElement packageXml = null; 108 | 109 | if (!string.IsNullOrWhiteSpace(options.Properties)) 110 | { 111 | var packageXmlContents = File.ReadAllText(packageFile); 112 | 113 | var props = options.Properties.Split(";", StringSplitOptions.RemoveEmptyEntries) 114 | .Select(x => x.Split('=')) 115 | .ToDictionary(x => x[0], x => x[1]); 116 | 117 | foreach (var prop in props) 118 | { 119 | packageXmlContents = packageXmlContents.Replace($"${prop.Key}$", prop.Value); 120 | } 121 | 122 | packageXml = XElement.Parse(packageXmlContents); 123 | } 124 | else 125 | { 126 | packageXml = XElement.Load(packageFile); 127 | } 128 | 129 | Console.WriteLine(Resources.Pack_UpdatingVersion); 130 | 131 | // stamp the package version. 132 | var version = GetOrSetPackageVersion(packageXml, options.Version); 133 | 134 | Console.WriteLine(Resources.Pack_GetPackageName); 135 | 136 | // work out what we are going to call the package 137 | var packageFileName = GetPackageFileName(packageXml, version, options.PackageFileName); 138 | 139 | // work out what where we are going to output the package to 140 | var packageOutputPath = Path.Combine(options.OutputDirectory, packageFileName); 141 | 142 | Console.WriteLine(Resources.Pack_AddPackageFiles); 143 | // add any files based on what is already in the package.xml 144 | AddFilesBasedOnPackageXml(packageXml, workingDir); 145 | 146 | // if the source is a folder, grab all the files from that folder 147 | if (isFolder) AddFilesFromFolders(options.FolderOrFile, workingDir); 148 | 149 | Console.WriteLine(Resources.Pack_BuildPackage); 150 | BuildPackageFolder(packageXml, workingDir, buildFolder); 151 | Directory.Delete(workingDir, true); 152 | 153 | CreateZip(buildFolder, packageOutputPath); 154 | 155 | Console.WriteLine(Resources.Pack_Complete); 156 | Directory.Delete(buildFolder, true); 157 | 158 | return 0; 159 | } 160 | 161 | private static string CreateWorkingFolder(string path, string subFolder = "", bool clean = true) 162 | { 163 | var folder = Path.Combine(path, subFolder); 164 | 165 | if (clean && Directory.Exists(folder)) 166 | Directory.Delete(folder, true); 167 | 168 | Directory.CreateDirectory(folder); 169 | return folder; 170 | } 171 | 172 | private static string GetPackageFileName(XElement packageFile, string version, string nameTemplate) 173 | { 174 | var template = !string.IsNullOrWhiteSpace(nameTemplate) 175 | ? nameTemplate.EnsureEndsWith(".zip") 176 | : "{name}_{version}.zip"; 177 | 178 | var nameNode = packageFile.Element("info")?.Element("package")?.Element("name"); 179 | if (nameNode != null) 180 | { 181 | var name = nameNode.Value 182 | //.Replace(".", "_") 183 | .Replace(" ", "_"); 184 | 185 | return template.Replace("{name}", name) 186 | .Replace("{version}", version); 187 | } 188 | 189 | Environment.Exit(2); 190 | return ""; 191 | } 192 | 193 | private static string GetOrSetPackageVersion(XElement packageXml, string version) 194 | { 195 | if (!string.IsNullOrWhiteSpace(version)) 196 | { 197 | var packageNode = packageXml.Element("info")?.Element("package"); 198 | if (packageNode != null) 199 | { 200 | var versionNode = packageNode.Element("version"); 201 | if (versionNode == null) 202 | { 203 | versionNode = new XElement("version"); 204 | packageNode.Add(versionNode); 205 | } 206 | 207 | versionNode.Value = version; 208 | } 209 | return version; 210 | } 211 | else 212 | { 213 | return packageXml?.Element("info")?.Element("package")?.Element("version")?.Value; 214 | } 215 | } 216 | 217 | 218 | /// 219 | /// Adds all the files in one folder (and sub folders) to the package directory. 220 | /// 221 | private static void AddFilesFromFolders(string sourceFolder, string dest, string prefix = "") 222 | { 223 | Console.WriteLine(Resources.Pack_AddingFolder, sourceFolder); 224 | 225 | foreach(var file in Directory.GetFiles(sourceFolder, "*.*", SearchOption.AllDirectories)) 226 | { 227 | var relative = file.Substring(sourceFolder.Length+1); 228 | 229 | var destination = Path.Combine(prefix, dest, relative); 230 | 231 | Console.WriteLine(Resources.Pack_AddingSingle, relative, dest); 232 | 233 | Directory.CreateDirectory(Path.GetDirectoryName(destination)); 234 | File.Copy(file, destination); 235 | } 236 | } 237 | 238 | /// 239 | /// adds a single file to the package folder. 240 | /// 241 | private static void AddFile(string sourceFile, string dest, string prefix = "") 242 | { 243 | var destination = Path.Combine(prefix, dest); 244 | Console.WriteLine(Resources.Pack_AddingSingle, sourceFile, dest); 245 | 246 | Directory.CreateDirectory(Path.GetDirectoryName(destination)); 247 | File.Copy(sourceFile, destination); 248 | } 249 | 250 | private static void AddFilesBasedOnPackageXml(XElement package, string tempFolder) 251 | { 252 | var fileNodes = package.Elements("files"); 253 | 254 | foreach (var node in fileNodes.Elements()) 255 | { 256 | var (path, orgPath) = GetPathAndOrgPath(node); 257 | if (!string.IsNullOrWhiteSpace(path)) 258 | { 259 | switch (node.Name.LocalName) 260 | { 261 | case "file": 262 | AddFile(path, orgPath, tempFolder); 263 | break; 264 | case "folder": 265 | AddFilesFromFolders(path, orgPath, tempFolder); 266 | break; 267 | } 268 | } 269 | } 270 | } 271 | 272 | private static (string path, string orgPath) GetPathAndOrgPath(XElement node) 273 | { 274 | var orgPath = node.Attribute("orgPath")?.Value; 275 | var path = node.Attribute("path")?.Value; 276 | 277 | if (string.IsNullOrWhiteSpace(orgPath)) orgPath = ""; 278 | 279 | // remove leading and trialing slashes from anything we have. 280 | orgPath = orgPath.Replace("/", "\\").Trim('\\'); 281 | path = path.Replace("/", "\\").Trim('\\'); 282 | 283 | return (path, orgPath); 284 | } 285 | 286 | private static void BuildPackageFolder(XElement package, string sourceFolder, string flatFolder) 287 | { 288 | var filesNode = package.Element("files"); 289 | 290 | // clean out any child nodes we might already have 291 | filesNode.RemoveNodes(); 292 | 293 | foreach (var file in Directory.GetFiles(sourceFolder, "*.*", SearchOption.AllDirectories)) 294 | { 295 | var guid = Path.GetFileName(file); 296 | 297 | if (guid.Equals("package.xml", StringComparison.InvariantCultureIgnoreCase)) continue; 298 | 299 | var orgPath = Path.GetDirectoryName(file); 300 | var orgName = guid; 301 | 302 | if (orgPath.Length > sourceFolder.Length) 303 | { 304 | orgPath = orgPath.Substring(sourceFolder.Length) 305 | .Replace("\\", "/"); 306 | } 307 | 308 | var dest = Path.Combine(flatFolder, guid); 309 | if (File.Exists(dest)) 310 | { 311 | guid = $"{Guid.NewGuid()}_{guid}"; 312 | dest = Path.Combine(flatFolder, guid); 313 | } 314 | 315 | filesNode.Add(new XElement("file", 316 | new XElement("guid", guid), 317 | new XElement("orgPath", orgPath), 318 | new XElement("orgName", orgName))); 319 | 320 | File.Copy(file, dest); 321 | } 322 | 323 | package.Save(Path.Combine(flatFolder, "package.xml")); 324 | } 325 | private static void CreateZip(string folder, string zipFileName) 326 | { 327 | if (Directory.Exists(folder)) 328 | { 329 | if (File.Exists(zipFileName)) 330 | File.Delete(zipFileName); 331 | 332 | Console.WriteLine(Resources.Pack_SavingPackage, zipFileName); 333 | 334 | ZipFile.CreateFromDirectory(folder, zipFileName); 335 | } 336 | else 337 | { 338 | Console.WriteLine(Resources.Pack_DirectoryMissing, folder); 339 | } 340 | } 341 | 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/Verbs/PushCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Net.Http.Handlers; 8 | using System.Net.Http.Headers; 9 | using System.Text.RegularExpressions; 10 | using System.Threading.Tasks; 11 | 12 | using CommandLine; 13 | 14 | using Newtonsoft.Json; 15 | using Umbraco.Packager.CI.Auth; 16 | using Umbraco.Packager.CI.Properties; 17 | 18 | namespace Umbraco.Packager.CI.Verbs 19 | { 20 | /// 21 | /// Options for the Push verb 22 | /// 23 | [Verb("push", HelpText = "HelpPush", ResourceType = typeof(HelpTextResource))] 24 | public class PushOptions 25 | { 26 | [Value(0, MetaName = "package.zip", Required = true, 27 | HelpText = "HelpPushPackage", ResourceType = typeof(HelpTextResource))] 28 | public string Package { get; set; } 29 | 30 | [Option('k', "Key", HelpText = "HelpPushKey", ResourceType = typeof(HelpTextResource))] 31 | public string ApiKey { get; set; } 32 | 33 | [Option('c', "Current", Default = "true", 34 | HelpText = "HelpPushCurrent", ResourceType = typeof(HelpTextResource))] 35 | public string Current { get; set; } 36 | 37 | [Option("DotNetVersion", Default = "4.7.2", 38 | HelpText = "HelpPushDotNet", ResourceType = typeof(HelpTextResource))] 39 | public string DotNetVersion { get; set; } 40 | 41 | [Option('w', "WorksWith", Default = "v850", 42 | HelpText = "HelpPushWorks", ResourceType = typeof(HelpTextResource))] 43 | public string WorksWith { get; set; } 44 | 45 | [Option('a', "Archive", Separator = ' ', 46 | HelpText = "HelpPushArchive", ResourceType = typeof(HelpTextResource))] 47 | public IEnumerable Archive { get; set; } 48 | } 49 | 50 | 51 | internal static class PushCommand 52 | { 53 | public static async Task RunAndReturn(PushOptions options, PackageHelper packageHelper) 54 | { 55 | // --package=MyFile.zip 56 | // --package=./MyFile.zip 57 | // --package=../MyParentFolder.zip 58 | var filePath = options.Package; 59 | var apiKey = options.ApiKey; 60 | 61 | var keyParts = packageHelper.SplitKey(apiKey); 62 | 63 | // Check we can find the file 64 | packageHelper.EnsurePackageExists(filePath); 65 | 66 | // Check File is a ZIP 67 | packageHelper.EnsureIsZip(filePath); 68 | 69 | // Check zip contains valid package.xml 70 | packageHelper.EnsureContainsPackageXml(filePath); 71 | 72 | // gets a package list from our.umbraco 73 | // if the api key is invalid we will also find out here. 74 | var packages = await packageHelper.GetPackageList(keyParts); 75 | var currentPackageId = await packageHelper.GetCurrentPackageFileId(keyParts); 76 | 77 | if (packages != null) 78 | { 79 | packageHelper.EnsurePackageDoesntAlreadyExists(packages, filePath); 80 | } 81 | 82 | // Archive packages 83 | var archivePatterns = new List(); 84 | var packagesToArchive = new List(); 85 | 86 | if (options.Archive != null) 87 | { 88 | archivePatterns.AddRange(options.Archive); 89 | } 90 | 91 | if (archivePatterns.Count > 0) 92 | { 93 | foreach (var archivePattern in archivePatterns) 94 | { 95 | if (archivePattern == "current") 96 | { 97 | // If the archive option is "current", then archive the current package 98 | if (currentPackageId != "0") 99 | packagesToArchive.Add(int.Parse(currentPackageId)); 100 | } 101 | else 102 | { 103 | // Convert the archive option to a regex 104 | var archiveRegex = new Regex("^" + archivePattern.Replace(".", "\\.").Replace("*", "(.*)") + "$", RegexOptions.IgnoreCase); 105 | 106 | // Find packages that match the regex and extract their IDs 107 | var archiveIds = packages.Where(x => archiveRegex.IsMatch(x.Value("Name"))).Select(x => x.Value("Id")).ToArray(); 108 | 109 | packagesToArchive.AddRange(archiveIds); 110 | } 111 | } 112 | } 113 | 114 | if (packagesToArchive.Count > 0) 115 | { 116 | await packageHelper.ArchivePackages(keyParts, packagesToArchive.Distinct()); 117 | Console.WriteLine($"Archived {packagesToArchive.Count} packages matching the archive pattern."); 118 | } 119 | 120 | // Parse package.xml before upload to print out info 121 | // and to use for comparison on what is already uploaded 122 | var packageInfo = Parse.PackageXml(filePath); 123 | 124 | // OK all checks passed - time to upload it 125 | await UploadPackage(options, packageHelper, packageInfo); 126 | 127 | return 0; 128 | } 129 | 130 | private static async Task UploadPackage(PushOptions options, PackageHelper packageHelper, PackageInfo packageInfo) 131 | { 132 | try 133 | { 134 | // HttpClient will use this event handler to give us 135 | // Reporting on how its progress the file upload 136 | var processMsgHandler = new ProgressMessageHandler(new HttpClientHandler()); 137 | processMsgHandler.HttpSendProgress += (sender, e) => 138 | { 139 | // Could try to reimplement progressbar - but that library did not work in GH Actions :( 140 | var percent = e.ProgressPercentage; 141 | }; 142 | 143 | var keyParts = packageHelper.SplitKey(options.ApiKey); 144 | var packageFileName = Path.GetFileName(options.Package); 145 | 146 | Console.WriteLine(Resources.Push_Uploading, packageFileName); 147 | 148 | var url = "/Umbraco/Api/ProjectUpload/UpdatePackage"; 149 | 150 | using (var client = packageHelper.GetClientBase(url, keyParts.Token, keyParts.MemberId, keyParts.ProjectId)) 151 | { 152 | MultipartFormDataContent form = new MultipartFormDataContent(); 153 | var fileInfo = new FileInfo(options.Package); 154 | var content = new StreamContent(fileInfo.OpenRead()); 155 | content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") 156 | { 157 | Name = "file", 158 | FileName = fileInfo.Name 159 | }; 160 | form.Add(content); 161 | form.Add(new StringContent(ParseCurrentFlag(options.Current)), "isCurrent"); 162 | form.Add(new StringContent(options.DotNetVersion), "dotNetVersion"); 163 | form.Add(new StringContent("package"), "fileType"); 164 | form.Add(GetVersionCompatibility(options.WorksWith), "umbracoVersions"); 165 | form.Add(new StringContent(packageInfo.VersionString), "packageVersion"); 166 | 167 | var httpResponse = await client.PostAsync(url, form); 168 | if (httpResponse.StatusCode == HttpStatusCode.Unauthorized) 169 | { 170 | packageHelper.WriteError(Resources.Push_ApiKeyInvalid); 171 | Environment.Exit(5); // ERROR_ACCESS_DENIED 172 | } 173 | else if (httpResponse.IsSuccessStatusCode) 174 | { 175 | Console.WriteLine(Resources.Push_Complete, packageFileName); 176 | 177 | // Response is not reported (at the moment) 178 | // var apiReponse = await httpResponse.Content.ReadAsStringAsync(); 179 | // Console.WriteLine(apiReponse); 180 | } 181 | } 182 | } 183 | catch (HttpRequestException ex) 184 | { 185 | // Could get network error or our.umb down 186 | Console.WriteLine(Resources.Error, ex); 187 | throw; 188 | } 189 | } 190 | 191 | 192 | /// 193 | /// returns the version compatibility string for uploading the package 194 | /// 195 | /// 196 | /// 197 | private static StringContent GetVersionCompatibility(string worksWithString) 198 | { 199 | // TODO: Workout how we can get a latest version from our ? 200 | // TODO: Maybe accept wild cards (8.* -> 8.5.0,8.4.0,8.3.0) 201 | // TODO: Work like nuget e.g '> 8.4.0' 202 | var versions = worksWithString 203 | .Split(",", StringSplitOptions.RemoveEmptyEntries) 204 | .Select(x => new UmbracoVersion() { Version = x }); 205 | 206 | return new StringContent(JsonConvert.SerializeObject(versions)); 207 | } 208 | 209 | private static string ParseCurrentFlag(string current) 210 | { 211 | if (bool.TryParse(current, out bool result)) 212 | { 213 | return result.ToString(); 214 | } 215 | 216 | return false.ToString(); 217 | } 218 | 219 | /// 220 | /// taken from the source of our.umbraco.com 221 | /// 222 | private class UmbracoVersion 223 | { 224 | public string Version { get; set; } 225 | 226 | // We don't need to supply name. but it is in the orginal model. 227 | // public string Name { get; set; } 228 | } 229 | } 230 | } 231 | --------------------------------------------------------------------------------