Compare commits

..
Author SHA1 Message Date
Andy Butland c6ab394947 Merge branch 'release/10.8.11' into v10/dev
# Conflicts:
#	version.json
2025-07-02 06:32:18 +02:00
Andy Butland 9f912aea0e Merge branch 'release/10.8.10' into v10/dev
# Conflicts:
#	version.json
2025-05-06 07:15:10 +02:00
Andy Butland 1cfa5a225e Merge branch 'release/10.8.9' into v10/dev
# Conflicts:
#	version.json
2025-03-11 09:20:51 +01:00
Nikolaj Geisle 75cc017a18 Merge branch 'release/10.8.8' into v10/dev
# Conflicts:
#	version.json
2025-01-23 11:08:09 +01:00
c7014e159b Sort manifest file paths alphabetically (#14466)
* Sort manifest file paths alphabetically

* Update src/Umbraco.Infrastructure/Manifest/ManifestParser.cs

Co-authored-by: Ronald Barendse <ronald@barend.se>

---------

Co-authored-by: Ronald Barendse <ronald@barend.se>
2024-11-21 16:19:48 +01:00
Zeegaan 31e1acce67 Merge remote-tracking branch 'origin/release/10.8.7' into v10/dev
# Conflicts:
#	version.json
2024-10-23 08:48:08 +02:00
Andreas ZerbstandGitHub 11270eaaf5 Updated message pack (#17320) 2024-10-21 11:00:47 +02:00
Bjarke Berg 0ee0db8071 Merge remote-tracking branch 'refs/remotes/origin/release/10.8' into v10/dev
# Conflicts:
#	version.json
2024-05-21 08:44:27 +02:00
119fde2033 V10: Fix for fallback file upload (#14892) (#15868)
* Fix for fallback file upload (#14892)

* Added check for file type

* Removed unneeded null checks and fixed tabs

* Cleaning

* Cleanups, cleanups, and removal of unneeded null checks

* Reverted removal of relationshipservice

* Revert null check removals (too risky)

---------

Co-authored-by: Ambert van Unen <AvanUnen@ilionx.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

(cherry picked from commit 0b5d1f8aa6)

* Fix up formatting

---------

Co-authored-by: Ambert van Unen <ambertvu@gmail.com>
2024-04-24 13:44:20 +02:00
Andy ButlandandGitHub 52c21b0fca Updates JSON schema for Umbraco 10 with latest references for Forms and Deploy (#15918) 2024-03-20 13:20:40 +01:00
Bjarke Berg 99d5a7e609 Merge remote-tracking branch 'origin/release/10.8' into v10/dev 2024-03-19 08:43:59 +01:00
b743f6a2df Merge pull request from GHSA-552f-97wf-pmpq
Co-authored-by: jey <jey@umbraco.dk>
2024-03-18 08:27:41 +01:00
2c23e67c65 Fixing locking issues for document type saves. (#15854)
* Added  ExecuteNonQuery(DbCommand command) on database to ensure we call OnExecutingCommand and OnExecutedCommand when executing DbCommands

* Added Cache Instructions lock, to avoid deadlocks

* Optimized read locks for nucache when only one content type is rebuilt

* Optimized the SqlServer locks, so only one command is executed (and thereby roundtrip) per lock instead of two

* Avoid breaking changes

* Cosmetic changes

* Take locks if everything is rebuild

* Use same lock in scopes, to avoid potential deadlocks between the two

* Use eager locks in PublishedSnapshotService.cs

* Added timeouts to some of the application locks

* Revert "Use eager locks in PublishedSnapshotService.cs"

This reverts commit 01873aae97.

* Revert "Added Cache Instructions lock, to avoid deadlocks"

This reverts commit e3fca7c12a.

* Use single readlock call to lock many

* Use eager locks for reads

* Eager write locks

* Ignore test of lazy locks

* Unique timeout exception messages

---------

Co-authored-by: kjac <kja@umbraco.dk>
2024-03-15 10:56:02 +01:00
4 changed files with 91 additions and 69 deletions
+2 -2
View File
@@ -13,7 +13,7 @@
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
<PackageReference Include="Umbraco.Deploy.Core" Version="10.3.3" />
<PackageReference Include="Umbraco.Forms.Core" Version="10.5.3" />
<PackageReference Include="Umbraco.Deploy.Core" Version="10.4.0" />
<PackageReference Include="Umbraco.Forms.Core" Version="10.5.4" />
</ItemGroup>
</Project>
@@ -250,6 +250,11 @@ public class ManifestParser : IManifestParser
return Array.Empty<string>();
}
return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories);
var files = Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories);
// Ensure a consistent, alphabetical sorting of paths, because this is not guaranteed to be the same between file systems or OSes
Array.Sort(files);
return files;
}
}
@@ -189,7 +189,7 @@ public class MediaController : ContentControllerBase
if (mapped is not null)
{
//remove the listview app if it exists
// remove the listview app if it exists
mapped.ContentApps = mapped.ContentApps.Where(x => x.Alias != "umbListView").ToList();
}
@@ -205,7 +205,7 @@ public class MediaController : ContentControllerBase
var apps = new List<ContentApp>
{
ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, "recycleBin", "media",
Constants.DataTypes.DefaultMediaListView)
Constants.DataTypes.DefaultMediaListView)
};
apps[0].Active = true;
var display = new MediaItemDisplay
@@ -238,7 +238,8 @@ public class MediaController : ContentControllerBase
if (foundMedia == null)
{
HandleContentNotFound(id);
//HandleContentNotFound will throw an exception
// HandleContentNotFound will throw an exception
return null;
}
@@ -306,8 +307,8 @@ public class MediaController : ContentControllerBase
public PagedResult<ContentItemBasic<ContentPropertyBasic>> GetChildFolders(int id, int pageNumber = 1,
int pageSize = 1000)
{
//Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it...
//if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder"
// Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it...
// if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder"
var folderTypes = _mediaTypeService
.GetAll()
.Where(x => x.Alias.EndsWith("Folder"))
@@ -320,7 +321,8 @@ public class MediaController : ContentControllerBase
}
IEnumerable<IMedia> children = _mediaService.GetPagedChildren(id, pageNumber - 1, pageSize, out long total,
//lookup these content types
// lookup these content types
_sqlContext.Query<IMedia>().Where(x => folderTypes.Contains(x.ContentTypeId)),
Ordering.By("Name"));
@@ -336,6 +338,7 @@ public class MediaController : ContentControllerBase
/// </summary>
[FilterAllowedOutgoingMedia(typeof(IEnumerable<ContentItemBasic<ContentPropertyBasic>>))]
public IEnumerable<ContentItemBasic<ContentPropertyBasic>> GetRootMedia() =>
// TODO: Add permissions check!
_mediaService.GetRootMedia()?
.Select(_umbracoMapper.Map<IMedia, ContentItemBasic<ContentPropertyBasic>>).WhereNotNull() ??
@@ -357,7 +360,7 @@ public class MediaController : ContentControllerBase
return HandleContentNotFound(id);
}
//if the current item is in the recycle bin
// if the current item is in the recycle bin
if (foundMedia.Trashed == false)
{
Attempt<OperationResult?> moveResult = _mediaService.MoveToRecycleBin(foundMedia,
@@ -390,8 +393,10 @@ public class MediaController : ContentControllerBase
{
// Authorize...
var requirement = new MediaPermissionsResourceRequirement();
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeAsync(User,
new MediaPermissionsResource(_mediaService.GetById(move.Id)), requirement);
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeAsync(
User,
new MediaPermissionsResource(_mediaService.GetById(move.Id)),
requirement);
if (!authorizationResult.Succeeded)
{
return Forbid();
@@ -404,18 +409,20 @@ public class MediaController : ContentControllerBase
return convertToActionResult.Convert();
}
var destinationParentID = move.ParentId;
var sourceParentID = toMove?.ParentId;
var destinationParentId = move.ParentId;
var sourceParentId = toMove?.ParentId;
var moveResult = toMove is null
? false
: _mediaService.Move(toMove, move.ParentId,
_backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1);
if (sourceParentID == destinationParentID)
if (sourceParentId == destinationParentId)
{
return ValidationProblem(new SimpleNotificationModel(new BackOfficeNotification("",
_localizedTextService.Localize("media", "moveToSameFolderFailed"), NotificationStyle.Error)));
return ValidationProblem(new SimpleNotificationModel(new BackOfficeNotification(
string.Empty,
_localizedTextService.Localize("media", "moveToSameFolderFailed"),
NotificationStyle.Error)));
}
if (moveResult == false)
@@ -437,9 +444,9 @@ public class MediaController : ContentControllerBase
public ActionResult<MediaItemDisplay?>? PostSave(
[ModelBinder(typeof(MediaItemBinder))] MediaItemSave contentItem)
{
//Recent versions of IE/Edge may send in the full client side file path instead of just the file name.
//To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all
//uploaded files to being *only* the actual file name (as it should be).
// Recent versions of IE/Edge may send in the full client side file path instead of just the file name.
// To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all
// uploaded files to being *only* the actual file name (as it should be).
if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any())
{
foreach (ContentPropertyFile file in contentItem.UploadedFiles)
@@ -448,14 +455,14 @@ public class MediaController : ContentControllerBase
}
}
//If we've reached here it means:
// If we've reached here it means:
// * Our model has been bound
// * and validated
// * any file attachments have been saved to their temporary location for us to use
// * we have a reference to the DTO object and the persisted object
// * Permissions are valid
//Don't update the name if it is empty
// Don't update the name if it is empty
if (contentItem.Name.IsNullOrWhiteSpace() == false && contentItem.PersistedContent is not null)
{
contentItem.PersistedContent.Name = contentItem.Name;
@@ -468,14 +475,14 @@ public class MediaController : ContentControllerBase
(save, property, v) => property?.SetValue(v), //set prop val
null); // media are all invariant
//we will continue to save if model state is invalid, however we cannot save if critical data is missing.
//TODO: Allowing media to be saved when it is invalid is odd - media doesn't have a publish phase so suddenly invalid data is allowed to be 'live'
// we will continue to save if model state is invalid, however we cannot save if critical data is missing.
// TODO: Allowing media to be saved when it is invalid is odd - media doesn't have a publish phase so suddenly invalid data is allowed to be 'live'
if (!ModelState.IsValid)
{
//check for critical data validation issues, we can't continue saving if this data is invalid
// check for critical data validation issues, we can't continue saving if this data is invalid
if (!RequiredForPersistenceAttribute.HasRequiredValuesForPersistence(contentItem))
{
//ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue!
// ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue!
// add the model state to the outgoing object and throw validation response
MediaItemDisplay? forDisplay = _umbracoMapper.Map<MediaItemDisplay>(contentItem.PersistedContent);
return ValidationProblem(forDisplay, ModelState);
@@ -487,20 +494,20 @@ public class MediaController : ContentControllerBase
return null;
}
//save the item
// save the item
Attempt<OperationResult?> saveStatus = _mediaService.Save(contentItem.PersistedContent,
_backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1);
//return the updated model
// return the updated model
MediaItemDisplay? display = _umbracoMapper.Map<MediaItemDisplay>(contentItem.PersistedContent);
//lastly, if it is not valid, add the model state to the outgoing object and throw a 403
// lastly, if it is not valid, add the model state to the outgoing object and throw a 403
if (!ModelState.IsValid)
{
return ValidationProblem(display, ModelState, StatusCodes.Status403Forbidden);
}
//put the correct msgs in
// put the correct msgs in
switch (contentItem.Action)
{
case ContentSaveAction.Save:
@@ -515,7 +522,7 @@ public class MediaController : ContentControllerBase
{
AddCancelMessage(display);
//If the item is new and the operation was cancelled, we need to return a different
// If the item is new and the operation was cancelled, we need to return a different
// status code so the UI can handle it since it won't be able to redirect since there
// is no Id to redirect to!
if (saveStatus.Result?.Result == OperationResultType.FailedCancelledByEvent &&
@@ -557,7 +564,7 @@ public class MediaController : ContentControllerBase
return NotFound();
}
//if there's nothing to sort just return ok
// if there's nothing to sort just return ok
if (sorted.IdSortOrder?.Length == 0)
{
return Ok();
@@ -599,7 +606,7 @@ public class MediaController : ContentControllerBase
public async Task<ActionResult<MediaItemDisplay?>> PostAddFolder(PostedFolder folder)
{
ActionResult<int?>? parentIdResult = await GetParentIdAsIntAsync(folder.ParentId, true);
if (!(parentIdResult?.Result is null))
if (parentIdResult?.Result is not null)
{
return new ActionResult<MediaItemDisplay?>(parentIdResult.Result);
}
@@ -637,15 +644,15 @@ public class MediaController : ContentControllerBase
//ensure it exists
Directory.CreateDirectory(root);
//must have a file
// must have a file
if (file is null || file.Count == 0)
{
return NotFound("No file was uploaded");
}
//get the string json from the request
// get the string json from the request
ActionResult<int?>? parentIdResult = await GetParentIdAsIntAsync(currentFolder, true);
if (!(parentIdResult?.Result is null))
if (parentIdResult?.Result is not null)
{
return parentIdResult.Result;
}
@@ -658,7 +665,7 @@ public class MediaController : ContentControllerBase
var tempFiles = new PostedFiles();
//in case we pass a path with a folder in it, we will create it and upload media to it.
// in case we pass a path with a folder in it, we will create it and upload media to it.
if (!string.IsNullOrEmpty(path))
{
if (!IsFolderCreationAllowedHere(parentId.Value))
@@ -674,16 +681,16 @@ public class MediaController : ContentControllerBase
var folderName = folders[i];
IMedia? folderMediaItem;
//if uploading directly to media root and not a subfolder
// if uploading directly to media root and not a subfolder
if (parentId == Constants.System.Root)
{
//look for matching folder
// look for matching folder
folderMediaItem =
_mediaService.GetRootMedia()?.FirstOrDefault(x =>
x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder);
if (folderMediaItem == null)
{
//if null, create a folder
// if null, create a folder
folderMediaItem =
_mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder);
_mediaService.Save(folderMediaItem);
@@ -691,10 +698,10 @@ public class MediaController : ContentControllerBase
}
else
{
//get current parent
// get current parent
IMedia? mediaRoot = _mediaService.GetById(parentId.Value);
//if the media root is null, something went wrong, we'll abort
// if the media root is null, something went wrong, we'll abort
if (mediaRoot == null)
{
return Problem(
@@ -702,7 +709,7 @@ public class MediaController : ContentControllerBase
" returned null");
}
//look for matching folder
// look for matching folder
folderMediaItem = FindInChildren(mediaRoot.Id, folderName, Constants.Conventions.MediaTypes.Folder);
if (folderMediaItem == null)
@@ -714,7 +721,7 @@ public class MediaController : ContentControllerBase
}
}
//set the media root to the folder id so uploaded files will end there.
// set the media root to the folder id so uploaded files will end there.
parentId = folderMediaItem.Id;
}
}
@@ -754,7 +761,7 @@ public class MediaController : ContentControllerBase
}
}
//Only set the permission-based mediaType if we only allow 1 specific file under this parent.
// Only set the permission-based mediaType if we only allow 1 specific file under this parent.
if (allowedContentTypes.Count == 1 && mediaTypeItem != null)
{
mediaTypeAlias = mediaTypeItem.Alias;
@@ -767,7 +774,7 @@ public class MediaController : ContentControllerBase
allowedContentTypes.UnionWith(typesAllowedAtRoot);
}
//get the files
// get the files
foreach (IFormFile formFile in file)
{
var fileName = formFile.FileName.Trim(Constants.CharArrays.DoubleQuote).TrimEnd();
@@ -826,6 +833,11 @@ public class MediaController : ContentControllerBase
continue;
}
if (allowedContentTypes.Any(x => x.Alias == mediaTypeItem.Alias) == false)
{
continue;
}
mediaTypeAlias = mediaTypeItem.Alias;
break;
}
@@ -871,8 +883,8 @@ public class MediaController : ContentControllerBase
IMedia createdMediaItem = _mediaService.CreateMedia(mediaItemName, parentId.Value, mediaTypeAlias,
_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1);
createdMediaItem.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper,
_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, fileName, stream);
createdMediaItem.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper,
_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, fileName, stream);
Attempt<OperationResult?> saveResult = _mediaService.Save(createdMediaItem,
_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1);
@@ -883,13 +895,13 @@ public class MediaController : ContentControllerBase
}
}
//Different response if this is a 'blueimp' request
// Different response if this is a 'blueimp' request
if (HttpContext.Request.Query.Any(x => x.Key == "origin"))
{
KeyValuePair<string, StringValues> origin = HttpContext.Request.Query.First(x => x.Key == "origin");
if (origin.Value == "blueimp")
{
return new JsonResult(tempFiles); //Don't output the angular xsrf stuff, blue imp doesn't like that
return new JsonResult(tempFiles); // Don't output the angular xsrf stuff, blue imp doesn't like that
}
}
@@ -928,7 +940,11 @@ public class MediaController : ContentControllerBase
var total = long.MaxValue;
while (page * pageSize < total)
{
IEnumerable<IMedia> children = _mediaService.GetPagedChildren(mediaId, page++, pageSize, out total,
IEnumerable<IMedia> children = _mediaService.GetPagedChildren(
mediaId,
page++,
pageSize,
out total,
_sqlContext.Query<IMedia>().Where(x => x.Name == nameToFind));
IMedia? match = children.FirstOrDefault(c => c.ContentType.Alias == contentTypeAlias);
if (match != null)
@@ -951,14 +967,13 @@ public class MediaController : ContentControllerBase
/// <returns></returns>
private async Task<ActionResult<int?>?> GetParentIdAsIntAsync(string? parentId, bool validatePermissions)
{
// test for udi
if (UdiParser.TryParse(parentId, out GuidUdi? parentUdi))
{
parentId = parentUdi?.Guid.ToString();
}
//if it's not an INT then we'll check for GUID
// if it's not an INT then we'll check for GUID
if (int.TryParse(parentId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int intParentId) == false)
{
// if a guid then try to look up the entity
@@ -982,7 +997,7 @@ public class MediaController : ContentControllerBase
}
// Authorize...
//ensure the user has access to this folder by parent id!
// ensure the user has access to this folder by parent id!
if (validatePermissions)
{
var requirement = new MediaPermissionsResourceRequirement();
@@ -1023,14 +1038,14 @@ public class MediaController : ContentControllerBase
if (model.ParentId < 0)
{
//cannot move if the content item is not allowed at the root unless there are
//none allowed at root (in which case all should be allowed at root)
// cannot move if the content item is not allowed at the root unless there are
// none allowed at root (in which case all should be allowed at root)
IMediaTypeService mediaTypeService = _mediaTypeService;
if (toMove.ContentType.AllowedAsRoot == false && mediaTypeService.GetAll().Any(ct => ct.AllowedAsRoot))
{
var notificationModel = new SimpleNotificationModel();
notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedAtRoot"),
"");
string.Empty);
return ValidationProblem(notificationModel);
}
}
@@ -1042,7 +1057,7 @@ public class MediaController : ContentControllerBase
return NotFound();
}
//check if the item is allowed under this one
// check if the item is allowed under this one
IMediaType? parentContentType = _mediaTypeService.Get(parent.ContentTypeId);
if (parentContentType?.AllowedContentTypes?.Select(x => x.Id).ToArray()
.Any(x => x.Value == toMove.ContentType.Id) == false)
@@ -1054,12 +1069,12 @@ public class MediaController : ContentControllerBase
}
// Check on paths
if (string.Format(",{0},", parent.Path)
.IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1)
if ($",{parent.Path},"
.IndexOf($",{toMove.Id},", StringComparison.Ordinal) > -1)
{
var notificationModel = new SimpleNotificationModel();
notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedByPath"),
"");
string.Empty);
return ValidationProblem(notificationModel);
}
}
@@ -1115,7 +1130,8 @@ public class MediaController : ContentControllerBase
/// Returns the child media objects - using the entity INT id
/// </summary>
[FilterAllowedOutgoingMedia(typeof(IEnumerable<ContentItemBasic<ContentPropertyBasic>>), "Items")]
public PagedResult<ContentItemBasic<ContentPropertyBasic>> GetChildren(int id,
public PagedResult<ContentItemBasic<ContentPropertyBasic>> GetChildren(
int id,
int pageNumber = 0,
int pageSize = 0,
string orderBy = "SortOrder",
@@ -1123,7 +1139,7 @@ public class MediaController : ContentControllerBase
bool orderBySystemField = true,
string filter = "")
{
//if a request is made for the root node data but the user's start node is not the default, then
// if a request is made for the root node data but the user's start node is not the default, then
// we need to return their start nodes
if (id == Constants.System.Root && UserStartNodes.Length > 0 &&
UserStartNodes.Contains(Constants.System.Root) == false)
@@ -1153,7 +1169,6 @@ public class MediaController : ContentControllerBase
}
// else proceed as usual
long totalChildren;
List<IMedia> children;
if (pageNumber > 0 && pageSize > 0)
@@ -1161,7 +1176,7 @@ public class MediaController : ContentControllerBase
IQuery<IMedia>? queryFilter = null;
if (filter.IsNullOrWhiteSpace() == false)
{
//add the default text filter
// add the default text filter
queryFilter = _sqlContext.Query<IMedia>()
.Where(x => x.Name != null)
.Where(x => x.Name!.Contains(filter));
@@ -1169,14 +1184,16 @@ public class MediaController : ContentControllerBase
children = _mediaService
.GetPagedChildren(
id, pageNumber - 1, pageSize,
id,
pageNumber - 1,
pageSize,
out totalChildren,
queryFilter,
Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField)).ToList();
}
else
{
//better to not use this without paging where possible, currently only the sort dialog does
// better to not use this without paging where possible, currently only the sort dialog does
children = _mediaService.GetPagedChildren(id, 0, int.MaxValue, out var total).ToList();
totalChildren = children.Count;
}
@@ -1189,7 +1206,7 @@ public class MediaController : ContentControllerBase
var pagedResult = new PagedResult<ContentItemBasic<ContentPropertyBasic>>(totalChildren, pageNumber, pageSize)
{
Items = children
.Select(_umbracoMapper.Map<IMedia, ContentItemBasic<ContentPropertyBasic>>).WhereNotNull()
.Select(_umbracoMapper.Map<IMedia, ContentItemBasic<ContentPropertyBasic>>).WhereNotNull()
};
return pagedResult;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
"version": "10.8.11",
"version": "10.9.0-rc",
"assemblyVersion": {
"precision": "build"
},