diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 31783f348..0686bb30c 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,4 +1,4 @@ -name: Pack and publish nugets +name: Pack and publish on: release: @@ -6,7 +6,8 @@ on: - published jobs: - build-pack: + release-nugets: + if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -17,7 +18,6 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: | - 5.0.x 6.0.x - name: Install deps run: | @@ -33,4 +33,37 @@ jobs: dotnet --version - name: Publish run: | - dotnet nuget push src/**/bin/Release/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} \ No newline at end of file + dotnet nuget push src/**/bin/Release/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} + release-upgrade-tool: + if: startsWith(github.ref, 'refs/tags/altinn-app-cli') + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./cli-tools/altinn-app-cli + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Install dotnet6 + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.0.x + - name: Build bundles + run: | + make bundles + - name: Upload files to release + uses: softprops/action-gh-release@v1 + with: + files: | + publish/archives/osx-x64.tar.gz + publish/archives/osx-arm64.tar.gz + publish/archives/linux-x64.tar.gz + publish/archives/linux-arm64.tar.gz + publish/archives/win-x64.zip + publish/archives/osx-x64.tar.gz.sha512 + publish/archives/osx-arm64.tar.gz.sha512 + publish/archives/linux-x64.tar.gz.sha512 + publish/archives/linux-arm64.tar.gz.sha512 + publish/archives/win-x64.zip.sha512 + \ No newline at end of file diff --git a/cli-tools/altinn-app-cli/Makefile b/cli-tools/altinn-app-cli/Makefile new file mode 100644 index 000000000..6cae575a1 --- /dev/null +++ b/cli-tools/altinn-app-cli/Makefile @@ -0,0 +1,42 @@ +# Path: Makefile + +build: + dotnet build + +executable-osx-x64: + dotnet publish -c Release -o publish/osx-x64 -r osx-x64 --self-contained + +executable-osx-arm64: + dotnet publish -c Release -o publish/osx-arm64 -r osx-arm64 --self-contained + +executable-win-x64: + dotnet publish -c Release -o publish/win-x64 -r win-x64 --self-contained + +executable-linux-x64: + dotnet publish -c Release -o publish/linux-x64 -r linux-x64 --self-contained + +executable-linux-arm64: + dotnet publish -c Release -o publish/linux-arm64 -r linux-arm64 --self-contained + +executables: executable-osx-x64 executable-osx-arm64 executable-win-x64 executable-linux-x64 executable-linux-arm64 + +archives: + mkdir -p publish/archives + tar -czvf publish/archives/osx-x64.tar.gz publish/osx-x64/altinn-app-cli + tar -czvf publish/archives/osx-arm64.tar.gz publish/osx-arm64/altinn-app-cli + tar -czvf publish/archives/linux-x64.tar.gz publish/linux-x64/altinn-app-cli + tar -czvf publish/archives/linux-arm64.tar.gz publish/linux-arm64/altinn-app-cli + zip -r publish/archives/win-x64.zip publish/win-x64/altinn-app-cli.exe + +checksums: + sha512sum publish/archives/osx-x64.tar.gz > publish/archives/osx-x64.tar.gz.sha512 + sha512sum publish/archives/osx-arm64.tar.gz > publish/archives/osx-arm64.tar.gz.sha512 + sha512sum publish/archives/linux-x64.tar.gz > publish/archives/linux-x64.tar.gz.sha512 + sha512sum publish/archives/linux-arm64.tar.gz > publish/archives/linux-arm64.tar.gz.sha512 + sha512sum publish/archives/win-x64.zip > publish/archives/win-x64.zip.sha512 + +bundles: executables archives checksums + +clean: + dotnet clean + rm -rf publish/ \ No newline at end of file diff --git a/cli-tools/altinn-app-cli/Program.cs b/cli-tools/altinn-app-cli/Program.cs new file mode 100644 index 000000000..17a257d7a --- /dev/null +++ b/cli-tools/altinn-app-cli/Program.cs @@ -0,0 +1,191 @@ +using System.CommandLine; +using System.Reflection; +using altinn_app_cli.v7Tov8.CodeRewriters; +using altinn_app_cli.v7Tov8.ProcessRewriter; +using altinn_app_cli.v7Tov8.ProjectChecks; +using altinn_app_cli.v7Tov8.ProjectRewriters; +using Microsoft.Build.Locator; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.MSBuild; + +namespace altinn_app_upgrade_cli; + +class Program +{ + static async Task Main(string[] args) + { + int returnCode = 0; + var projectFolderOption = new Option(name: "--folder", description: "The project folder to read", getDefaultValue: () => "CurrentDirectory"); + var projectFileOption = new Option(name: "--project", description: "The project file to read relative to --folder", getDefaultValue: () => "App/App.csproj"); + var processFileOption = new Option(name: "--process", description: "The process file to read relative to --folder", getDefaultValue: () => "App/config/process/process.bpmn"); + var targetVersionOption = new Option(name: "--target-version", description: "The target version to upgrade to", getDefaultValue: () => "8.0.0-preview.9"); + var skipCsprojUpgradeOption = new Option(name: "--skip-csproj-upgrade", description: "Skip csproj file upgrade", getDefaultValue: () => false); + var skipCodeUpgradeOption = new Option(name: "--skip-code-upgrade", description: "Skip code upgrade", getDefaultValue: () => false); + var skipProcessUpgradeOption = new Option(name: "--skip-process-upgrade", description: "Skip process file upgrade", getDefaultValue: () => false); + var rootCommand = new RootCommand("Command line interface for working with Altinn 3 Applications"); + var upgradeCommand = new Command("upgrade", "Upgrade an app from v7 to v8") + { + projectFolderOption, + projectFileOption, + processFileOption, + targetVersionOption, + skipCsprojUpgradeOption, + skipCodeUpgradeOption, + skipProcessUpgradeOption, + }; + rootCommand.AddCommand(upgradeCommand); + var versionCommand = new Command("version", "Print version of altinn-app-cli"); + rootCommand.AddCommand(versionCommand); + + upgradeCommand.SetHandler(async (projectFolder, projectFile, processFile, targetVersion, skipCodeUpgrade, skipProcessUpgrade, skipCsprojUpgrade) => + { + if (projectFolder == "CurrentDirectory") + { + projectFolder = Directory.GetCurrentDirectory(); + } + + if (File.Exists(projectFolder)) + { + Console.WriteLine($"Project folder {projectFolder} does not exist. Please supply location of project with --folder [path/to/project]"); + returnCode = 1; + return; + } + + FileAttributes attr = File.GetAttributes(projectFolder); + if ((attr & FileAttributes.Directory) != FileAttributes.Directory) + { + Console.WriteLine($"Project folder {projectFolder} is a file. Please supply location of project with --folder [path/to/project]"); + returnCode = 1; + return; + } + + if (!Path.IsPathRooted(projectFolder)) + { + projectFile = Path.Combine(Directory.GetCurrentDirectory(), projectFolder, projectFile); + processFile = Path.Combine(Directory.GetCurrentDirectory(), projectFolder, processFile); + } + else + { + projectFile = Path.Combine(projectFolder, projectFile); + processFile = Path.Combine(projectFolder, processFile); + } + + var projectChecks = new ProjectChecks(projectFile); + if (!projectChecks.SupportedSourceVersion()) + { + Console.WriteLine($"Version(s) in project file {projectFile} is not supported. Please upgrade to version 7.0.0 or higher."); + returnCode = 2; + return; + } + if (!skipCsprojUpgrade) + { + returnCode = await UpgradeNugetVersions(projectFile, targetVersion); + } + + if (!skipCodeUpgrade && returnCode == 0) + { + returnCode = await UpgradeCode(projectFile); + } + + if (!skipProcessUpgrade && returnCode == 0) + { + returnCode = await UpgradeProcess(processFile); + } + + if (returnCode == 0) + { + Console.WriteLine("Upgrade completed without errors. Please verify that the application is still working as expected."); + } + else + { + Console.WriteLine("Upgrade completed with errors. Please check for errors in the log above."); + } + }, + projectFolderOption, projectFileOption, processFileOption, targetVersionOption, skipCodeUpgradeOption, skipProcessUpgradeOption, skipCsprojUpgradeOption); + versionCommand.SetHandler(() => + { + var version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion ?? "Unknown"; + Console.WriteLine($"altinn-app-cli v{version}"); + }); + await rootCommand.InvokeAsync(args); + return returnCode; + } + + static async Task UpgradeNugetVersions(string projectFile, string targetVersion) + { + if (!File.Exists(projectFile)) + { + Console.WriteLine($"Project file {projectFile} does not exist. Please supply location of project with --project [path/to/project.csproj]"); + return 1; + } + + Console.WriteLine("Trying to upgrade nuget versions in project file"); + var rewriter = new ProjectFileRewriter(projectFile, targetVersion); + await rewriter.Upgrade(); + Console.WriteLine("Nuget versions upgraded"); + return 0; + } + + static async Task UpgradeCode(string projectFile) + { + if (!File.Exists(projectFile)) + { + Console.WriteLine($"Project file {projectFile} does not exist. Please supply location of project with --project [path/to/project.csproj]"); + return 1; + } + + Console.WriteLine("Trying to upgrade references and using in code"); + + MSBuildLocator.RegisterDefaults(); + var workspace = MSBuildWorkspace.Create(); + var project = await workspace.OpenProjectAsync(projectFile); + var comp = await project.GetCompilationAsync(); + if (comp == null) + { + Console.WriteLine("Could not get compilation"); + return 1; + } + foreach (var sourceTree in comp.SyntaxTrees) + { + SemanticModel sm = comp.GetSemanticModel(sourceTree); + TypesRewriter rewriter = new(sm); + SyntaxNode newSource = rewriter.Visit(await sourceTree.GetRootAsync()); + if (newSource != await sourceTree.GetRootAsync()) + { + await File.WriteAllTextAsync(sourceTree.FilePath, newSource.ToFullString()); + } + UsingRewriter usingRewriter = new(); + var newUsingSource = usingRewriter.Visit(newSource); + if (newUsingSource != newSource) + { + await File.WriteAllTextAsync(sourceTree.FilePath, newUsingSource.ToFullString()); + } + } + + Console.WriteLine("References and using upgraded"); + return 0; + } + + static async Task UpgradeProcess(string processFile) + { + if (!File.Exists(processFile)) + { + Console.WriteLine($"Process file {processFile} does not exist. Please supply location of project with --process [path/to/project.csproj]"); + return 1; + } + + Console.WriteLine("Trying to upgrade process file"); + ProcessUpgrader parser = new(processFile); + parser.Upgrade(); + await parser.Write(); + var warnings = parser.GetWarnings(); + foreach (var warning in warnings) + { + Console.WriteLine(warning); + } + + Console.WriteLine(warnings.Any() ? "Process file upgraded with warnings. Review the warnings above and make sure that the process file is still valid." : "Process file upgraded"); + + return 0; + } +} diff --git a/cli-tools/altinn-app-cli/altinn-app-cli.csproj b/cli-tools/altinn-app-cli/altinn-app-cli.csproj new file mode 100644 index 000000000..4798b1dd1 --- /dev/null +++ b/cli-tools/altinn-app-cli/altinn-app-cli.csproj @@ -0,0 +1,42 @@ + + + + Exe + net6.0 + altinn_app_cli + latest + enable + enable + win-x64;linux-x64;osx-x64 + osx-x64 + altinn-app-cli + true + false + true + + + + + + + + + + + + + + + $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) + preview + altinn-app-cli + true + 10.0 + + + + + $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) + + + diff --git a/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/TypesRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/TypesRewriter.cs new file mode 100644 index 000000000..8e7836248 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/TypesRewriter.cs @@ -0,0 +1,152 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace altinn_app_cli.v7Tov8.CodeRewriters; + +public class TypesRewriter: CSharpSyntaxRewriter +{ + private readonly SemanticModel semanticModel; + private readonly Dictionary fieldDescendantsMapping = new Dictionary() + { + {"Altinn.App.Core.Interface.IAppEvents", SyntaxFactory.IdentifierName("IAppEvents")}, + {"Altinn.App.Core.Interface.IApplication", SyntaxFactory.IdentifierName("IApplicationClient")}, + {"Altinn.App.Core.Interface.IAppResources", SyntaxFactory.IdentifierName("IAppResources")}, + {"Altinn.App.Core.Interface.IAuthentication", SyntaxFactory.IdentifierName("IAuthenticationClient")}, + {"Altinn.App.Core.Interface.IAuthorization", SyntaxFactory.IdentifierName("IAuthorizationClient")}, + {"Altinn.App.Core.Interface.IData", SyntaxFactory.IdentifierName("IDataClient")}, + {"Altinn.App.Core.Interface.IDSF", SyntaxFactory.IdentifierName("IPersonClient")}, + {"Altinn.App.Core.Interface.IER", SyntaxFactory.IdentifierName("IOrganizationClient")}, + {"Altinn.App.Core.Interface.IEvents", SyntaxFactory.IdentifierName("IEventsClient")}, + {"Altinn.App.Core.Interface.IInstance", SyntaxFactory.IdentifierName("IInstanceClient")}, + {"Altinn.App.Core.Interface.IInstanceEvent", SyntaxFactory.IdentifierName("IInstanceEventClient")}, + {"Altinn.App.Core.Interface.IPersonLookup", SyntaxFactory.IdentifierName("IPersonClient")}, + {"Altinn.App.Core.Interface.IPersonRetriever", SyntaxFactory.IdentifierName("IPersonClient")}, + {"Altinn.App.Core.Interface.IPrefill", SyntaxFactory.IdentifierName("IPrefill")}, + {"Altinn.App.Core.Interface.IProcess", SyntaxFactory.IdentifierName("IProcessClient")}, + {"Altinn.App.Core.Interface.IProfile", SyntaxFactory.IdentifierName("IProfileClient")}, + {"Altinn.App.Core.Interface.IRegister", SyntaxFactory.IdentifierName("IAltinnPartyClient")}, + {"Altinn.App.Core.Interface.ISecrets", SyntaxFactory.IdentifierName("ISecretsClient")}, + {"Altinn.App.Core.Interface.ITaskEvents", SyntaxFactory.IdentifierName("ITaskEvents")}, + {"Altinn.App.Core.Interface.IUserTokenProvider", SyntaxFactory.IdentifierName("IUserTokenProvider")} + }; + private readonly IEnumerable statementsToRemove = new List() + { + "app.UseDefaultSecurityHeaders();", + "app.UseRouting();", + "app.UseStaticFiles('/' + applicationId);", + "app.UseAuthentication();", + "app.UseAuthorization();", + "app.UseEndpoints(endpoints", + "app.UseHealthChecks(\"/health\");", + "app.UseAltinnAppCommonConfiguration();" + }; + + public TypesRewriter(SemanticModel semanticModel) + { + this.semanticModel = semanticModel; + } + + public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) + { + return UpdateField(node); + } + + public override SyntaxNode? VisitParameter(ParameterSyntax node) + { + var parameterTypeName = node.Type; + if(parameterTypeName is null) + { + return node; + } + var parameterType = (ITypeSymbol?)semanticModel.GetSymbolInfo(parameterTypeName).Symbol; + if(parameterType?.ToString() != null && fieldDescendantsMapping.TryGetValue(parameterType.ToString()!, out var newType)) + { + var newTypeName = newType.WithLeadingTrivia(parameterTypeName.GetLeadingTrivia()).WithTrailingTrivia(parameterTypeName.GetTrailingTrivia()); + return node.ReplaceNode(parameterTypeName, newTypeName); + } + + return node; + } + + public override SyntaxNode? VisitGlobalStatement(GlobalStatementSyntax node) + { + if (node.Statement is LocalFunctionStatementSyntax localFunctionStatementSyntax) + { + if(localFunctionStatementSyntax.Identifier.Text == "Configure" && !localFunctionStatementSyntax.ParameterList.Parameters.Any() && localFunctionStatementSyntax.Body != null) + { + SyntaxTriviaList leadingTrivia = SyntaxFactory.TriviaList(); + SyntaxTriviaList trailingTrivia = SyntaxFactory.TriviaList(); + var newBody = SyntaxFactory.Block().WithoutLeadingTrivia().WithTrailingTrivia(localFunctionStatementSyntax.Body.GetTrailingTrivia()); + foreach (var childNode in localFunctionStatementSyntax.Body.ChildNodes()) + { + if(childNode is IfStatementSyntax ifStatementSyntax && ifStatementSyntax.Condition.ToString()!="app.Environment.IsDevelopment()") + { + newBody = AddStatementWithTrivia(newBody, ifStatementSyntax); + } + if(childNode is ExpressionStatementSyntax statementSyntax){ + leadingTrivia = statementSyntax.GetLeadingTrivia(); + trailingTrivia = statementSyntax.GetTrailingTrivia(); + if (!ShouldRemoveStatement(statementSyntax)) + { + newBody = AddStatementWithTrivia(newBody, statementSyntax); + } + } + if(childNode is LocalDeclarationStatementSyntax localDeclarationStatement) + { + newBody = AddStatementWithTrivia(newBody, localDeclarationStatement); + } + } + newBody = newBody.AddStatements(SyntaxFactory.ParseStatement("app.UseAltinnAppCommonConfiguration();").WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia)); + return node.ReplaceNode(localFunctionStatementSyntax.Body, newBody); + } + } + + return node; + } + + public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node) + { + if (node.Identifier.Text == "FilterAsync" && + node.Parent is ClassDeclarationSyntax { BaseList: not null } classDeclarationSyntax + && classDeclarationSyntax.BaseList.Types.Any(x => x.Type.ToString() == "IProcessExclusiveGateway") + && node.ParameterList.Parameters.All(x => x.Type?.ToString() != "ProcessGatewayInformation")) + { + return node.AddParameterListParameters( + SyntaxFactory.Parameter(SyntaxFactory.Identifier("processGatewayInformation").WithLeadingTrivia(SyntaxFactory.ElasticSpace)).WithType(SyntaxFactory.ParseTypeName("ProcessGatewayInformation").WithLeadingTrivia(SyntaxFactory.ElasticSpace)) + ); + } + + return node; + } + + private FieldDeclarationSyntax UpdateField(FieldDeclarationSyntax node) + { + var variableTypeName = node.Declaration.Type; + var variableType = (ITypeSymbol?)semanticModel.GetSymbolInfo(variableTypeName).Symbol; + if(variableType?.ToString() != null && fieldDescendantsMapping.TryGetValue(variableType.ToString()!, out var newType)) + { + var newTypeName = newType.WithLeadingTrivia(variableTypeName.GetLeadingTrivia()).WithTrailingTrivia(variableTypeName.GetTrailingTrivia()); + node = node.ReplaceNode(variableTypeName, newTypeName); + Console.WriteLine($"Updated field {node.Declaration.Variables.First().Identifier.Text} from {variableType} to {newType}"); + } + return node; + } + + private bool ShouldRemoveStatement(StatementSyntax statementSyntax) + { + foreach (var statementToRemove in statementsToRemove) + { + var s = statementSyntax.ToString(); + if(s == statementToRemove || s.StartsWith(statementToRemove)) + { + return true; + } + } + return false; + } + private static BlockSyntax AddStatementWithTrivia(BlockSyntax block, StatementSyntax statement) + { + return block.AddStatements(statement).WithLeadingTrivia(statement.GetLeadingTrivia()).WithTrailingTrivia(statement.GetTrailingTrivia()); + } +} diff --git a/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/UsingRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/UsingRewriter.cs new file mode 100644 index 000000000..1d692a3a7 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/UsingRewriter.cs @@ -0,0 +1,91 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace altinn_app_cli.v7Tov8.CodeRewriters; + +public class UsingRewriter: CSharpSyntaxRewriter +{ + private const string CommonInterfaceNamespace = "Altinn.App.Core.Interface"; + + private readonly Dictionary usingMappings = new Dictionary() + { + {"IAppEvents", "Altinn.App.Core.Internal.App"}, + {"IApplication", "Altinn.App.Core.Internal.App"}, + {"IAppResources", "Altinn.App.Core.Internal.App"}, + {"IAuthenticationClient", "Altinn.App.Core.Internal.Auth"}, + {"IAuthorizationClient", "Altinn.App.Core.Internal.Auth"}, + {"IDataClient", "Altinn.App.Core.Internal.Data"}, + {"IPersonClient", "Altinn.App.Core.Internal.Registers"}, + {"IOrganizationClient", "Altinn.App.Core.Internal.Registers"}, + {"IEventsClient", "Altinn.App.Core.Internal.Events"}, + {"IInstanceClient", "Altinn.App.Core.Internal.Instances"}, + {"IInstanceEventClient", "Altinn.App.Core.Internal.Instances"}, + {"IPrefill", "Altinn.App.Core.Internal.Prefill"}, + {"IProcessClient", "Altinn.App.Core.Internal.Process"}, + {"IProfileClient", "Altinn.App.Core.Internal.Profile"}, + {"IAltinnPartyClient", "Altinn.App.Core.Internal.Registers"}, + {"ISecretsClient", "Altinn.App.Core.Internal.Secrets"}, + {"ITaskEvents", "Altinn.App.Core.Internal.Process"}, + {"IUserTokenProvider", "Altinn.App.Core.Internal.Auth"}, + }; + + public override SyntaxNode? VisitCompilationUnit(CompilationUnitSyntax node) + { + foreach (var mapping in usingMappings) + { + if (HasFieldOfType(node, mapping.Key)) + { + node = AddUsing(node, mapping.Value); + } + } + + if (ImplementsIProcessExclusiveGateway(node)) + { + node = AddUsing(node, "Altinn.App.Core.Models.Process"); + } + + return RemoveOldUsing(node); + } + + private bool HasFieldOfType(CompilationUnitSyntax node, string typeName) + { + var fieldDecendants = node.DescendantNodes().OfType(); + return fieldDecendants.Any(f => f.Declaration.Type.ToString() == typeName); + } + + private bool ImplementsIProcessExclusiveGateway(CompilationUnitSyntax node) + { + var classDecendants = node.DescendantNodes().OfType(); + return classDecendants.Any(c => c.BaseList?.Types.Any(t => t.Type.ToString() == "IProcessExclusiveGateway") == true); + } + + private CompilationUnitSyntax AddUsing(CompilationUnitSyntax node, string usingString) + { + if (HasUsingDefined(node, usingString)) + { + return node; + } + var usingName = SyntaxFactory.ParseName(usingString); + var usingDirective = SyntaxFactory.UsingDirective(usingName).NormalizeWhitespace().WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed); + return node.AddUsings(usingDirective); + } + + private bool HasUsingDefined(CompilationUnitSyntax node, string usingName) + { + var usingDirectiveSyntaxes = node.DescendantNodes().OfType(); + return usingDirectiveSyntaxes.Any(u => u.Name?.ToString() == usingName); + } + + private CompilationUnitSyntax? RemoveOldUsing(CompilationUnitSyntax node) + { + var usingDirectiveSyntaxes = node.DescendantNodes().OfType(); + var usingDirectiveSyntax = usingDirectiveSyntaxes.FirstOrDefault(u => u.Name?.ToString() == CommonInterfaceNamespace); + if (usingDirectiveSyntax != null) + { + return node.RemoveNode(usingDirectiveSyntax, SyntaxRemoveOptions.KeepNoTrivia); + } + return node; + } + +} diff --git a/cli-tools/altinn-app-cli/v7Tov8/ProcessRewriter/ProcessUpgrader.cs b/cli-tools/altinn-app-cli/v7Tov8/ProcessRewriter/ProcessUpgrader.cs new file mode 100644 index 000000000..e23be8615 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/ProcessRewriter/ProcessUpgrader.cs @@ -0,0 +1,168 @@ +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace altinn_app_cli.v7Tov8.ProcessRewriter; + +public class ProcessUpgrader +{ + private XDocument doc; + private readonly string processFile; + private readonly XNamespace newAltinnNs = "http://altinn.no/process"; + private readonly XNamespace origAltinnNs = "http://altinn.no"; + private readonly XNamespace bpmnNs = "http://www.omg.org/spec/BPMN/20100524/MODEL"; + private readonly IList warnings = new List(); + + public ProcessUpgrader(string processFile) + { + this.processFile = processFile; + var xmlString = File.ReadAllText(processFile); + xmlString = xmlString.Replace($"xmlns:altinn=\"{origAltinnNs}\"", $"xmlns:altinn=\"{newAltinnNs}\""); + doc = XDocument.Parse(xmlString); + } + + public void Upgrade() + { + var definitions = doc.Root; + var process = definitions?.Elements().Single(e => e.Name.LocalName == "process"); + var processElements = process?.Elements() ?? Enumerable.Empty(); + foreach (var processElement in processElements) + { + if (processElement.Name.LocalName == "task") + { + UpgradeTask(processElement); + } + else if (processElement.Name.LocalName == "sequenceFlow") + { + UpgradeSequenceFlow(processElement); + } + } + } + + private void UpgradeTask(XElement processElement) + { + var taskTypeAttr = processElement.Attribute(newAltinnNs + "tasktype"); + var taskType = taskTypeAttr?.Value; + if (taskType == null) + { + return; + } + XElement extensionElements = processElement.Element(bpmnNs + "extensionElements") ?? new XElement(bpmnNs + "extensionElements"); + XElement taskExtensionElement = extensionElements.Element(newAltinnNs + "taskExtension") ?? new XElement(newAltinnNs + "taskExtension"); + XElement taskTypeElement = new XElement(newAltinnNs + "taskType"); + taskTypeElement.Value = taskType; + taskExtensionElement.Add(taskTypeElement); + extensionElements.Add(taskExtensionElement); + processElement.Add(extensionElements); + taskTypeAttr?.Remove(); + if (taskType.Equals("confirmation")) + { + AddAction(processElement, "confirm"); + } + } + + private void UpgradeSequenceFlow(XElement processElement) + { + var flowTypeAttr = processElement.Attribute(newAltinnNs + "flowtype"); + flowTypeAttr?.Remove(); + if (flowTypeAttr?.Value != "AbandonCurrentReturnToNext") + { + return; + } + + var sourceRefAttr = processElement.Attribute("sourceRef"); + SetSequenceFlowAsDefaultIfGateway(sourceRefAttr?.Value!, processElement.Attribute("id")?.Value!); + var sourceTask = FollowGatewaysAndGetSourceTask(sourceRefAttr?.Value!); + AddAction(sourceTask, "reject"); + var conditionExpression = processElement.Elements().FirstOrDefault(e => e.Name.LocalName == "conditionExpression"); + if(conditionExpression == null) + { + conditionExpression = new XElement(bpmnNs + "conditionExpression"); + processElement.Add(conditionExpression); + } + conditionExpression.Value = "[\"equals\", [\"gatewayAction\"],\"reject\"]"; + warnings.Add($"SequenceFlow {processElement.Attribute("id")?.Value!} has flowtype {flowTypeAttr.Value} upgrade tool has tried to add reject action to source task. \nPlease verify that process flow is correct and that layoutfiels are updated to use ActionButtons\nRefere to docs.altinn.studio for how actions in v8 work"); + } + + private void SetSequenceFlowAsDefaultIfGateway(string elementRef, string sequenceFlowRef) + { + var sourceElement = doc.Root?.Elements().Single(e => e.Name.LocalName == "process").Elements().Single(e => e.Attribute("id")?.Value == elementRef); + if (sourceElement?.Name.LocalName == "exclusiveGateway") + { + if (sourceElement.Attribute("default") == null) + { + sourceElement.Add(new XAttribute("default", sequenceFlowRef)); + } + else + { + warnings.Add($"Default sequence flow already set for gateway {elementRef}. Process is most likely not correct. Please correct it manually and test it."); + } + } + } + + private XElement FollowGatewaysAndGetSourceTask(string sourceRef) + { + var processElement = doc.Root?.Elements().Single(e => e.Name.LocalName == "process"); + var sourceElement = processElement?.Elements().Single(e => e.Attribute("id")?.Value == sourceRef); + if (sourceElement?.Name.LocalName == "task") + { + return sourceElement; + } + + if (sourceElement?.Name.LocalName == "exclusiveGateway") + { + var incomingSequenceFlow = sourceElement.Elements().Single(e => e.Name.LocalName == "incoming").Value; + var incomingSequenceFlowRef = processElement?.Elements().Single(e => e.Attribute("id")!.Value == incomingSequenceFlow).Attribute("sourceRef")?.Value; + return FollowGatewaysAndGetSourceTask(incomingSequenceFlowRef!); + } + + throw new Exception("Unexpected element type"); + } + + private void AddAction(XElement sourceTask, string actionName) + { + var extensionElements = sourceTask.Element(bpmnNs + "extensionElements"); + if (extensionElements == null) + { + extensionElements = new XElement(bpmnNs + "extensionElements"); + sourceTask.Add(extensionElements); + } + + var taskExtensionElement = extensionElements.Element(newAltinnNs + "taskExtension"); + if (taskExtensionElement == null) + { + taskExtensionElement = new XElement(newAltinnNs + "taskExtension"); + extensionElements.Add(taskExtensionElement); + } + + var actions = taskExtensionElement.Element(newAltinnNs + "actions"); + if (actions == null) + { + actions = new XElement(newAltinnNs + "actions"); + taskExtensionElement.Add(actions); + } + if(actions.Elements().Any(e => e.Value == actionName)) + { + return; + } + var action = new XElement(newAltinnNs + "action"); + action.Value = actionName; + actions.Add(action); + } + + public async Task Write() + { + XmlWriterSettings xws = new XmlWriterSettings(); + xws.Async = true; + xws.OmitXmlDeclaration = false; + xws.Indent = true; + xws.Encoding = Encoding.UTF8; + await using XmlWriter xw = XmlWriter.Create(processFile, xws); + await doc.WriteToAsync(xw, CancellationToken.None); + } + + public IList GetWarnings() + { + return warnings; + } +} diff --git a/cli-tools/altinn-app-cli/v7Tov8/ProjectChecks/ProjectChecks.cs b/cli-tools/altinn-app-cli/v7Tov8/ProjectChecks/ProjectChecks.cs new file mode 100644 index 000000000..4c4ddc73b --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/ProjectChecks/ProjectChecks.cs @@ -0,0 +1,71 @@ +using System.Xml.Linq; + +namespace altinn_app_cli.v7Tov8.ProjectChecks; + +public class ProjectChecks +{ + private XDocument doc; + + public ProjectChecks(string projectFilePath) + { + var xmlString = File.ReadAllText(projectFilePath); + doc = XDocument.Parse(xmlString); + } + + public bool SupportedSourceVersion() + { + var altinnAppCoreElements = GetAltinnAppCoreElement(); + var altinnAppApiElements = GetAltinnAppApiElement(); + if (altinnAppCoreElements == null || altinnAppApiElements == null) + { + return false; + } + + if (altinnAppApiElements.Select(apiElement => apiElement.Attribute("Version")?.Value).Any(altinnAppApiVersion => !SupportedSourceVersion(altinnAppApiVersion))) + { + return false; + } + + return altinnAppCoreElements.Select(coreElement => coreElement.Attribute("Version")?.Value).All(altinnAppCoreVersion => SupportedSourceVersion(altinnAppCoreVersion)); + + } + + private List? GetAltinnAppCoreElement() + { + return doc.Root?.Elements("ItemGroup").Elements("PackageReference").Where(x => x.Attribute("Include")?.Value == "Altinn.App.Core").ToList(); + } + + private List? GetAltinnAppApiElement() + { + return doc.Root?.Elements("ItemGroup").Elements("PackageReference").Where(x => x.Attribute("Include")?.Value == "Altinn.App.Api").ToList(); + } + + /// + /// Check that version is >=7.0.0 + /// + /// + /// + private bool SupportedSourceVersion(string? version) + { + if (version == null) + { + return false; + } + + var versionParts = version.Split('.'); + if (versionParts.Length < 3) + { + return false; + } + + if (int.TryParse(versionParts[0], out int major)) + { + if (major >= 7) + { + return true; + } + } + + return false; + } +} diff --git a/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs new file mode 100644 index 000000000..11def034d --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs @@ -0,0 +1,53 @@ +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace altinn_app_cli.v7Tov8.ProjectRewriters; + +public class ProjectFileRewriter +{ + private XDocument doc; + private readonly string projectFilePath; + private readonly string targetVersion; + + public ProjectFileRewriter(string projectFilePath, string targetVersion = "8.0.0") + { + this.projectFilePath = projectFilePath; + this.targetVersion = targetVersion; + var xmlString = File.ReadAllText(projectFilePath); + doc = XDocument.Parse(xmlString); + } + + public async Task Upgrade() + { + var altinnAppCoreElements = GetAltinnAppCoreElement(); + var altinnAppApiElements = GetAltinnAppApiElement(); + if (altinnAppCoreElements != null && altinnAppApiElements != null) + { + altinnAppCoreElements.ForEach(c => c.Attribute("Version")?.SetValue(targetVersion)); + altinnAppApiElements.ForEach(a => a.Attribute("Version")?.SetValue(targetVersion)); + await Save(); + } + } + + private List? GetAltinnAppCoreElement() + { + return doc.Root?.Elements("ItemGroup").Elements("PackageReference").Where(x => x.Attribute("Include")?.Value == "Altinn.App.Core").ToList(); + } + + private List? GetAltinnAppApiElement() + { + return doc.Root?.Elements("ItemGroup").Elements("PackageReference").Where(x => x.Attribute("Include")?.Value == "Altinn.App.Api").ToList(); + } + + private async Task Save() + { + XmlWriterSettings xws = new XmlWriterSettings(); + xws.Async = true; + xws.OmitXmlDeclaration = true; + xws.Indent = true; + xws.Encoding = Encoding.UTF8; + await using XmlWriter xw = XmlWriter.Create(projectFilePath, xws); + await doc.WriteToAsync(xw, CancellationToken.None); + } +}