diff --git a/.editorconfig b/.editorconfig index 1e7d255..633668b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -49,6 +49,11 @@ indent_size = 2 [*.sh] indent_size = 2 +# CSharp code style settings: +[*.cs] + +dotnet_diagnostic.cs1591.severity = none # Missing XML comment for publicly visible type or member. + # IDE0055: Fix formatting dotnet_diagnostic.ide0055.severity = warning @@ -159,8 +164,6 @@ dotnet_diagnostic.rs2008.severity = none dotnet_diagnostic.ide0073.severity = warning file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license.\nSee the LICENSE file in the project root for more information. -# CSharp code style settings: -[*.cs] # Newline settings csharp_new_line_before_open_brace = all csharp_new_line_before_else = true @@ -235,53 +238,3 @@ dotnet_diagnostic.rs0037.severity = none [src/CodeStyle/**.{cs,vb}] # warning RS0005: Do not use generic CodeAction.Create to create CodeAction dotnet_diagnostic.rs0005.severity = none - -[src/{Analyzers,CodeStyle,Features,Workspaces,EditorFeatures, VisualStudio}/**/*.{cs,vb}] - -# IDE0011: Add braces -csharp_prefer_braces = when_multiline:warning -# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201 -dotnet_diagnostic.ide0011.severity = warning - -# IDE0035: Remove unreachable code -dotnet_diagnostic.ide0035.severity = warning - -# IDE0036: Order modifiers -dotnet_diagnostic.ide0036.severity = warning - -# IDE0040: Add accessibility modifiers -dotnet_diagnostic.ide0040.severity = warning - -# IDE0043: Format string contains invalid placeholder -dotnet_diagnostic.ide0043.severity = warning - -# IDE0044: Make field readonly -dotnet_diagnostic.ide0044.severity = warning - -# CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings? -# IDE0051: Remove unused private member -dotnet_diagnostic.ide0051.severity = warning - -# IDE0052: Remove unread private member -dotnet_diagnostic.ide0052.severity = warning - -# IDE0059: Unnecessary assignment to a value -dotnet_diagnostic.ide0059.severity = warning - -# IDE0060: Remove unused parameter -dotnet_diagnostic.ide0060.severity = warning - -# CA1822: Make member static -dotnet_diagnostic.ca1822.severity = warning - -# Prefer "var" everywhere -dotnet_diagnostic.ide0007.severity = warning -csharp_style_var_for_built_in_types = true:warning -csharp_style_var_when_type_is_apparent = true:warning -csharp_style_var_elsewhere = true:warning - -[src/{VisualStudio}/**/*.{cs,vb}] -# CA1822: Make member static -# Not enforced as a build 'warning' for 'VisualStudio' layer due to large number of false positives from https://github.com/dotnet/roslyn-analyzers/issues/3857 and https://github.com/dotnet/roslyn-analyzers/issues/3858 -# Additionally, there is a risk of accidentally breaking an internal API that partners rely on though IVT. -dotnet_diagnostic.ca1822.severity = suggestion diff --git a/.project-metadata.json b/.project-metadata.json index 8fc60d4..0ebe7d3 100644 --- a/.project-metadata.json +++ b/.project-metadata.json @@ -2,7 +2,7 @@ "name": "cicee", "description": "Runs continuous integration workloads via docker-compose, locally or on a build server.", "title": "Continuous Integration Containerized Execution Environment (CICEE)", - "version": "1.10.0", + "version": "1.11.0", "ciEnvironment": { "variables": [ { diff --git a/README.md b/README.md index 7238781..030d5b1 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ CICEE also provides a [continuous integration shell function library][cicee-lib] * `bash`: bash shell * `docker`: Docker command-line interface * `docker-compose`: Docker Compose command-line interface (compose file version `3.7` support required) -* `dotnet`: .NET runtime (`6.x` and `7.x` supported) +* `dotnet`: .NET SDK (`6.x`, `7.x`, and `8.x` supported) ## Why use CICEE? diff --git a/ci/libexec/ci-workflows.sh b/ci/libexec/ci-workflows.sh index 90d3d46..2b8411b 100755 --- a/ci/libexec/ci-workflows.sh +++ b/ci/libexec/ci-workflows.sh @@ -67,10 +67,18 @@ ci-compose() { --configuration Release \ --output "${BUILD_UNPACKAGED_DIST}/net7.0" \ -p:Version="${PROJECT_VERSION_DIST}" \ - --framework net7.0 \ + --framework net7.0 && + printf "\nBeginning 'dotnet publish' targeting .NET 8...\n\n" && + dotnet publish "${PROJECT_ROOT}/src" \ + --configuration Release \ + --output "${BUILD_UNPACKAGED_DIST}/net8.0" \ + -p:Version="${PROJECT_VERSION_DIST}" \ + --framework net8.0 \ -p:GenerateDocumentationFile=true && + printf "\nCompleted 'dotnet publish' targeting .NET 8.\n\n" && printf "\nBeginning 'dotnet pack'...\n\n" && - ci-dotnet-pack -p:GenerateDocumentationFile=true + ci-dotnet-pack -p:GenerateDocumentationFile=true && + printf "\nCompleted 'dotnet pack'.\n\n" } #-- diff --git a/docs/use/meta-version-set.md b/docs/use/meta-version-set.md new file mode 100644 index 0000000..1d28fc3 --- /dev/null +++ b/docs/use/meta-version-set.md @@ -0,0 +1,26 @@ +# Set Version in Project Metadata + +Sets the `.version` property in a [project metadata][project-structure] file to a SemVer version (e.g., `2.3.1`). + +> Project metadata JSON files are _modified_, unless `dry-run` option is enabled. This may reformat the document. + +For example: + +> **Note:** When CICEE is installed as a .NET local tool (i.e., your `${PROJECT_ROOT}/.config/dotnet-tools.json` contains a reference to `cicee`), all `$ cicee ..arguments..` commands become `$ dotnet cicee ..arguments..`. Additionally, you may need to run `dotnet tool restore`, to ensure the tool is installed. + +```bash +$ cicee meta version set --help +Description: + Sets version in project metadata. + +Usage: + cicee meta version set [options] + +Options: + -m, --metadata (REQUIRED) Project metadata file path. [default: $(pwd)/.project-metadata.json] + -d, --dry-run Execute a 'dry run', i.e., skip writing files and similar destructive steps. [default: False] + -v, --version (REQUIRED) New version in SemVer 2.0 release format. E.g., '2.3.1'. [] + -?, -h, --help Show help and usage information +``` + +[project-structure]: ./project-structure.md diff --git a/docs/use/using-cicee.md b/docs/use/using-cicee.md index 73290f6..83d5472 100644 --- a/docs/use/using-cicee.md +++ b/docs/use/using-cicee.md @@ -42,6 +42,7 @@ Commands: * [`update`][meta-cienv-variables] - Update a CI environment variable. * [`version`][meta-version] - Gets `.version` in project metadata. * [`bump`][meta-version-bump] - Bumps the `.version` in project metadata by a SemVer increment. + * [`set`][meta-version-set] - Sets the `.version` in project metadata to a SemVer version. * `template` - Commands working with project continuous integration templates. * [`init`][template-init] - Initialize a project repository with continuous integration workflow scripts. * [`lib`][template-lib] - Initialize project CI with CICEE execution library. Supports `cicee exec`-like behavior without CICEE installation. @@ -54,6 +55,7 @@ Commands: [lib]: ./ci-library.md [meta-cienv-variables]: ./meta-cienv-variables.md [meta-version-bump]: ./meta-version-bump.md +[meta-version-set]: ./meta-version-set.md [meta-version]: ./meta-version.md [template-init]: ./template-init.md [template-lib]: ./template-lib.md diff --git a/src/Cicee.csproj b/src/Cicee.csproj index 095aa01..2e666d1 100644 --- a/src/Cicee.csproj +++ b/src/Cicee.csproj @@ -5,7 +5,7 @@ 10 enable Exe - net6.0;net7.0 + net6.0;net7.0;net8.0 diff --git a/src/Commands/Meta/Version/Bump/MetaVersionBumpEntrypoint.cs b/src/Commands/Meta/Version/Bump/MetaVersionBumpEntrypoint.cs index bfd376f..a4139e1 100644 --- a/src/Commands/Meta/Version/Bump/MetaVersionBumpEntrypoint.cs +++ b/src/Commands/Meta/Version/Bump/MetaVersionBumpEntrypoint.cs @@ -8,6 +8,8 @@ public static class MetaVersionBumpEntrypoint { public static Func> CreateHandler(CommandDependencies dependencies) { + return Handle; + async Task Handle(string projectMetadataPath, bool isDryRun, SemVerIncrement semVerIncrement) { return (await MetaVersionBumpHandling.Handle(dependencies, projectMetadataPath, isDryRun, semVerIncrement)) @@ -18,7 +20,5 @@ async Task Handle(string projectMetadataPath, bool isDryRun, SemVerIncremen }) .ToExitCode(); } - - return Handle; } -} \ No newline at end of file +} diff --git a/src/Commands/Meta/Version/MetaVersionCommand.cs b/src/Commands/Meta/Version/MetaVersionCommand.cs index cce0963..aa89600 100644 --- a/src/Commands/Meta/Version/MetaVersionCommand.cs +++ b/src/Commands/Meta/Version/MetaVersionCommand.cs @@ -1,5 +1,6 @@ using System.CommandLine; using Cicee.Commands.Meta.Version.Bump; +using Cicee.Commands.Meta.Version.Set; using Cicee.Dependencies; namespace Cicee.Commands.Meta.Version; @@ -16,6 +17,7 @@ public static Command Create(CommandDependencies dependencies) command.SetHandler(MetaVersionEntrypoint.CreateHandler(dependencies), projectMetadata); command.AddCommand(MetaVersionBumpCommand.Create(dependencies)); + command.AddCommand(MetaVersionSetCommand.Create(dependencies)); return command; } diff --git a/src/Commands/Meta/Version/MetaVersionEntrypoint.cs b/src/Commands/Meta/Version/MetaVersionEntrypoint.cs index aa4587e..7ad32f5 100644 --- a/src/Commands/Meta/Version/MetaVersionEntrypoint.cs +++ b/src/Commands/Meta/Version/MetaVersionEntrypoint.cs @@ -9,6 +9,8 @@ public static class MetaVersionEntrypoint { public static Func> CreateHandler(CommandDependencies dependencies) { + return Handle; + Task Handle(string projectMetadataPath) { return MetaVersionHandling.HandleMetaVersionRequest(dependencies, projectMetadataPath) @@ -20,7 +22,5 @@ Task Handle(string projectMetadataPath) .ToExitCode() .AsTask(); } - - return Handle; } } diff --git a/src/Commands/Meta/Version/Set/MetaVersionSetCommand.cs b/src/Commands/Meta/Version/Set/MetaVersionSetCommand.cs new file mode 100644 index 0000000..c6c5dc0 --- /dev/null +++ b/src/Commands/Meta/Version/Set/MetaVersionSetCommand.cs @@ -0,0 +1,72 @@ +using System.CommandLine; +using System.CommandLine.Parsing; +using System.Linq; +using Cicee.Dependencies; + +namespace Cicee.Commands.Meta.Version.Set; + +public static class MetaVersionSetCommand +{ + public const string CommandName = "set"; + public const string CommandDescription = "Sets version in project metadata."; + + private static Option IncrementOption() + { + return new Option(new[] {"--version", "-v"}, + "New version in SemVer 2.0 release format. E.g., '2.3.1'.") {IsRequired = true}; + } + + private static Option CreateVersionOption() + { + return new Option( + new[] {"--version", "-v"}, + ParseArgument, + isDefault: true, + "New version in SemVer 2.0 release format. E.g., '2.3.1'." + ) {IsRequired = true}; + + System.Version? ParseArgument(ArgumentResult result) + { + var initialTokenValue = result.Tokens.Count > 0 ? result.Tokens[index: 0].Value : string.Empty; + var tokenValue = initialTokenValue; + var countOfPeriods = tokenValue.Count(c => c == '.'); + + if (tokenValue != string.Empty) + { + switch (countOfPeriods) + { + case 0: + if (int.TryParse(tokenValue, out _)) + { + tokenValue += ".0.0"; // Single integer value + } + + break; + case 1: + tokenValue += ".0"; // Two-field version value + break; + } + } + + if (System.Version.TryParse(tokenValue, out var version)) + { + return version; + } + + result.ErrorMessage = + $"Invalid version format '{initialTokenValue}'. Use complete Major.Minor.Patch, e.g., '2.3.1' or '4.0.0'."; + return null; + } + } + + public static Command Create(CommandDependencies dependencies) + { + var projectMetadata = ProjectMetadataOption.Create(dependencies); + var dryRun = DryRunOption.Create(); + var versionOption = CreateVersionOption(); + var command = new Command(CommandName, CommandDescription) {projectMetadata, dryRun, versionOption}; + command.SetHandler(MetaVersionSetEntrypoint.CreateHandler(dependencies), projectMetadata, dryRun, versionOption); + + return command; + } +} diff --git a/src/Commands/Meta/Version/Set/MetaVersionSetEntrypoint.cs b/src/Commands/Meta/Version/Set/MetaVersionSetEntrypoint.cs new file mode 100644 index 0000000..5f58031 --- /dev/null +++ b/src/Commands/Meta/Version/Set/MetaVersionSetEntrypoint.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading.Tasks; +using Cicee.Dependencies; + +namespace Cicee.Commands.Meta.Version.Set; + +public static class MetaVersionSetEntrypoint +{ + public static Func> CreateHandler(CommandDependencies dependencies) + { + return Handle; + + async Task Handle(string projectMetadataPath, bool isDryRun, System.Version? version) + { + // NOTE: Use of version! should be safe due to parameter being required. + return (await MetaVersionSetHandling.Handle(dependencies, projectMetadataPath, isDryRun, version!)) + .TapSuccess(dependencies.StandardOutWriteLine) + .TapFailure(exception => + { + dependencies.StandardErrorWriteLine(exception.ToExecutionFailureMessage()); + }) + .ToExitCode(); + } + } +} diff --git a/src/Commands/Meta/Version/Set/MetaVersionSetHandling.cs b/src/Commands/Meta/Version/Set/MetaVersionSetHandling.cs new file mode 100644 index 0000000..91ef4eb --- /dev/null +++ b/src/Commands/Meta/Version/Set/MetaVersionSetHandling.cs @@ -0,0 +1,75 @@ +using System; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Cicee.CiEnv; +using Cicee.Dependencies; +using LanguageExt.Common; + +namespace Cicee.Commands.Meta.Version.Set; + +public static class MetaVersionSetHandling +{ + internal static string GetVersionString(this System.Version version) + { + return version.ToString(fieldCount: 3); + } + + public static Result<(System.Version Version, ProjectMetadata ProjectMetadata, JsonObject MetadataJson)> + TrySetProjectVersion( + Func> tryLoadFileString, + Func> ensureFileExists, + string projectMetadataPath, + System.Version version + ) + { + Result TryIncrementVersionInProjectMetadata(System.Version incrementedVersion) + { + return tryLoadFileString(projectMetadataPath) + .MapSafe(content => + { + var jsonObject = JsonNode.Parse(content)!.AsObject(); + jsonObject["version"] = incrementedVersion.GetVersionString(); + return jsonObject; + }); + } + + return ProjectMetadataLoader.TryLoadFromFile( + ensureFileExists, + tryLoadFileString, + projectMetadataPath + ) + .Bind(metadata => + TryIncrementVersionInProjectMetadata(version) + .Map(jsonObject => + (version, metadata with {Version = version.GetVersionString()}, jsonObject) + ) + ); + } + + public static Task> Handle( + CommandDependencies dependencies, + string projectMetadataPath, + bool isDryRun, + System.Version version + ) + { + async Task> ConditionallyModifyProjectMetadata( + (System.Version SetedVersion, ProjectMetadata ProjectMetadata, JsonObject MetadataJson) tuple + ) + { + var versionString = tuple.SetedVersion.GetVersionString(); + return isDryRun + ? new Result(versionString) + : (await ProjectMetadataManipulation.UpdateVersionInMetadata(dependencies, projectMetadataPath, + tuple.SetedVersion)).Map(_ => versionString); + } + + return TrySetProjectVersion( + dependencies.TryLoadFileString, + dependencies.EnsureFileExists, + projectMetadataPath, + version + ) + .BindAsync(ConditionallyModifyProjectMetadata); + } +} diff --git a/tests/unit/Commands/Meta/Version/Bump/MetaVersionBumpHandlingTests/TryBumpProjectVersionTests.cs b/tests/unit/Commands/Meta/Version/Bump/MetaVersionBumpHandlingTests/TryBumpProjectVersionTests.cs index e162045..fbc180f 100644 --- a/tests/unit/Commands/Meta/Version/Bump/MetaVersionBumpHandlingTests/TryBumpProjectVersionTests.cs +++ b/tests/unit/Commands/Meta/Version/Bump/MetaVersionBumpHandlingTests/TryBumpProjectVersionTests.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Cicee.CiEnv; -using Cicee.Commands; using Cicee.Commands.Meta.Version.Bump; using Cicee.Dependencies; using Jds.LanguageExt.Extras; diff --git a/tests/unit/Commands/Meta/Version/Set/MetaVersionSetHandlingTests/TrySetProjectVersionTests.cs b/tests/unit/Commands/Meta/Version/Set/MetaVersionSetHandlingTests/TrySetProjectVersionTests.cs new file mode 100644 index 0000000..591a814 --- /dev/null +++ b/tests/unit/Commands/Meta/Version/Set/MetaVersionSetHandlingTests/TrySetProjectVersionTests.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using Cicee.CiEnv; +using Cicee.Commands.Meta.Version.Set; +using Cicee.Dependencies; +using Jds.LanguageExt.Extras; +using LanguageExt.Common; +using Xunit; + +namespace Cicee.Tests.Unit.Commands.Meta.Version.Set.MetaVersionSetHandlingTests; + +public class TrySetProjectVersionTests +{ + public static IEnumerable CreateVersionTestCases() + { + object[] CreateTestCase( + CommandDependencies dependencies, + string projectMetadataPath, + System.Version version, + System.Version expectedVersion + ) + { + return new object[] {dependencies, projectMetadataPath, version, expectedVersion}; + } + + + var arrangedMetadataPath = "/not-real/repo/package.json"; + var currentVersion = new System.Version(major: 5, minor: 2, build: 9); + var expectedMajor = new System.Version(major: 6, minor: 0, build: 0); + var expectedMinor = new System.Version(major: 5, minor: 3, build: 0); + var expectedPatch = new System.Version(major: 5, minor: 2, build: 10); + var dependencies = DependencyHelper.CreateMockDependencies() with + { + TryLoadFileString = path => + path == arrangedMetadataPath + ? new Result( + MockMetadata.GeneratePackageJson(new ProjectMetadata + { + Name = "fake-project", Version = currentVersion.ToString(fieldCount: 3) + })) + : new Result(new Exception("Not found")) + }; + + yield return CreateTestCase(dependencies, arrangedMetadataPath, expectedMajor, expectedMajor); + yield return CreateTestCase(dependencies, arrangedMetadataPath, expectedMinor, expectedMinor); + yield return CreateTestCase(dependencies, arrangedMetadataPath, expectedPatch, expectedPatch); + } + + [Theory] + [MemberData(nameof(CreateVersionTestCases))] + public void ReturnsExpectedVersion( + CommandDependencies dependencies, + string projectMetadataPath, + System.Version version, + System.Version expectedVersion + ) + { + var actualTuple = MetaVersionSetHandling.TrySetProjectVersion( + dependencies.TryLoadFileString, + dependencies.EnsureFileExists, + projectMetadataPath, + version + ) + .IfFailThrow(); + + Assert.Equal(expectedVersion, actualTuple.Version); + Assert.Equal(expectedVersion.ToString(fieldCount: 3), actualTuple.ProjectMetadata.Version); + } +}