├── .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 | [](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 |
--------------------------------------------------------------------------------