From 3219399fac2c7145f641c46be8ebc0af1b44875d Mon Sep 17 00:00:00 2001 From: Pedro Pinho Date: Wed, 22 Jul 2026 15:16:52 +0000 Subject: [PATCH 1/3] chore: validate step's attribute correct enum parsing when reading from XML manifest --- core/Enums/MessageType.cs | 6 +- core/Services/CmfPackageController.cs | 37 ++++++-- tests/Specs/CmfPackageController_FromXml.cs | 96 +++++++++++++++++++++ 3 files changed, 127 insertions(+), 12 deletions(-) diff --git a/core/Enums/MessageType.cs b/core/Enums/MessageType.cs index 2f092614e..f963d8301 100644 --- a/core/Enums/MessageType.cs +++ b/core/Enums/MessageType.cs @@ -5,9 +5,7 @@ /// public enum MessageType { - /// - /// The import object - /// - ImportObject = 0 + ImportObject = 0, + ExecuteActionCode = 1 } } \ No newline at end of file diff --git a/core/Services/CmfPackageController.cs b/core/Services/CmfPackageController.cs index 700f426d5..67b148d96 100644 --- a/core/Services/CmfPackageController.cs +++ b/core/Services/CmfPackageController.cs @@ -281,6 +281,25 @@ private static List CheckForUnknownAttributes(XElement element, IEnumera var allAttributes = element.Attributes().Select(a => a.Name.LocalName); return allAttributes.Except(knownAttributes).ToList(); } + + private static TEnum? ParseStepEnumAttribute(XElement element, string attributeName, bool strictStepParsing, ref bool hasParsingErrors) where TEnum : struct, Enum + { + var rawValue = element.Attribute(attributeName)?.Value; + if (rawValue == null) + { + return null; + } + + if (Enum.TryParse(rawValue, out var parsedValue)) + { + return parsedValue; + } + + hasParsingErrors = true; + var logMsg = $"Step attribute '{attributeName}' could not be parsed as {typeof(TEnum).Name}: '{rawValue}'"; + if (strictStepParsing) Log.Error(logMsg); else Log.Debug(logMsg); + return null; + } private static CmfPackageV1 FromXmlManifest(string manifest, bool setDefaultValues = false) { @@ -373,8 +392,10 @@ public static CmfPackageV1 FromXml(XDocument xml) throw new CliException("Invalid manifest"); } + Log.Debug($"Parsing '{rootNode.Element("packageId", true)?.Value}@{rootNode.Element("version", true)?.Value}' package's XML manifest."); + var strictStepParsing = Environment.GetEnvironmentVariable("cmf_cli_internal_strict_step_parsing") != null; - var parsingErrors = new List(); + bool hasParsingErrors = false; PackageType cliPackageType = PackageType.Generic; if (!Enum.TryParse(rootNode.Element("clipackagetype", true)?.Value, out cliPackageType)) @@ -382,7 +403,7 @@ public static CmfPackageV1 FromXml(XDocument xml) if (!Enum.TryParse(rootNode.Element("packageType", true)?.Value, out cliPackageType)) { var logMsg = $"Unknown packageType: '{rootNode.Element("packageType", true)?.Value}'"; - parsingErrors.Add(logMsg); + hasParsingErrors = true; if (strictStepParsing) Log.Error(logMsg); else Log.Debug(logMsg); } @@ -401,14 +422,14 @@ public static CmfPackageV1 FromXml(XDocument xml) if (!typeParsed) { var logMsg = $"Step has an unknown type: {typeAttributeValue}"; - parsingErrors.Add(logMsg); + hasParsingErrors = true; if (strictStepParsing) Log.Error(logMsg); else Log.Debug(logMsg); } if (unknownAttributes.Count > 0) { var logMsg = $"Step (type: {typeAttributeValue}) has unknown attributes. Attributes: {string.Join(", ", unknownAttributes)}"; - parsingErrors.Add(logMsg); + hasParsingErrors = true; if (strictStepParsing) Log.Error(logMsg); else Log.Debug(logMsg); } @@ -421,7 +442,7 @@ public static CmfPackageV1 FromXml(XDocument xml) File = element.Attribute("file")?.Value, TagFile = bool.TryParse(element.Attribute("tagFile")?.Value, out bool tagFile) ? tagFile : null, TargetDatabase = element.Attribute("targetDatabase")?.Value, - MessageType = Enum.TryParse(element.Attribute("messageType")?.Value, out MessageType messageType) ? messageType : null, + MessageType = ParseStepEnumAttribute(element, "messageType", strictStepParsing, ref hasParsingErrors), RelativePath = null, FilePath = element.Attribute("filePath")?.Value, OldSystemName = element.Attribute("oldSystemName")?.Value, @@ -433,7 +454,7 @@ public static CmfPackageV1 FromXml(XDocument xml) ImportXMLObjectPath = element.Attribute("importXMLObjectPath")?.Value, AutomationWorkflowFileBasePath = element.Attribute("automationWorkflowFileBasePath")?.Value, CreateInCollection = bool.TryParse(element.Attribute("createInCollection")?.Value, out bool createInCollection) ? createInCollection : null, - TargetPlatform = Enum.TryParse(element.Attribute("targetPlatform")?.Value, out MasterDataTargetPlatformType targetPlatform) ? targetPlatform : null, + TargetPlatform = ParseStepEnumAttribute(element, "targetPlatform", strictStepParsing, ref hasParsingErrors), UserKey = element.Attribute("userKey")?.Value, Quota = element.Attribute("quota")?.Value, Id = element.Attribute("id")?.Value, @@ -469,9 +490,9 @@ public static CmfPackageV1 FromXml(XDocument xml) } } - if (strictStepParsing && parsingErrors.Count > 0) + if (strictStepParsing && hasParsingErrors) { - throw new CliException("CLI encountered unknown metadata when parsing package's manifest.xml! Check error messages for details."); + throw new CliException($"CLI encountered unknown metadata when parsing '{rootNode.Element("packageId", true)?.Value}@{rootNode.Element("version", true)?.Value}' package's manifest.xml! Check error messages for details."); } DependencyCollection deps = new(); diff --git a/tests/Specs/CmfPackageController_FromXml.cs b/tests/Specs/CmfPackageController_FromXml.cs index 3488b7e9a..0f5215e4e 100644 --- a/tests/Specs/CmfPackageController_FromXml.cs +++ b/tests/Specs/CmfPackageController_FromXml.cs @@ -799,4 +799,100 @@ public void FromXml_ShouldDefaultToGenericAndNotThrow_WhenStrictStepParsingDisab pkg.Steps.Should().ContainSingle(); pkg.Steps.Single().Type.Should().Be(StepType.Generic); } + + [Fact] + public void FromXml_ShouldKeepStepEnumAttributeNull_WhenAttributeIsMissing() + { + var xml = XDocument.Parse( + """ + + Cmf.Custom.Data + 1.0.0 + + + + + """); + + var pkg = CmfPackageController.FromXml(xml); + + pkg.Steps.Single().MessageType.Should().BeNull(); + pkg.Steps.Single().TargetPlatform.Should().BeNull(); + } + + [Fact] + public void FromXml_ShouldKeepStepEnumAttributeNullAndNotThrow_WhenValueIsInvalidAndStrictStepParsingDisabled() + { + Environment.SetEnvironmentVariable("cmf_cli_internal_strict_step_parsing", null); + + var xml = XDocument.Parse( + """ + + Cmf.Custom.Data + 1.0.0 + + + + + """); + + var pkg = CmfPackageController.FromXml(xml); + + pkg.Steps.Single().MessageType.Should().BeNull(); + pkg.Steps.Single().TargetPlatform.Should().BeNull(); + } + + [Fact] + public void FromXml_ShouldThrowException_WhenStrictStepParsingEnabledAndMessageTypeIsInvalid() + { + Environment.SetEnvironmentVariable("cmf_cli_internal_strict_step_parsing", "1"); + try + { + var xml = XDocument.Parse( + """ + + Cmf.Custom.Data + 1.0.0 + + + + + """); + + Action act = () => CmfPackageController.FromXml(xml); + + act.Should().Throw().WithMessage("*CLI encountered unknown metadata*"); + } + finally + { + Environment.SetEnvironmentVariable("cmf_cli_internal_strict_step_parsing", null); + } + } + + [Fact] + public void FromXml_ShouldThrowException_WhenStrictStepParsingEnabledAndTargetPlatformIsInvalid() + { + Environment.SetEnvironmentVariable("cmf_cli_internal_strict_step_parsing", "1"); + try + { + var xml = XDocument.Parse( + """ + + Cmf.Custom.Data + 1.0.0 + + + + + """); + + Action act = () => CmfPackageController.FromXml(xml); + + act.Should().Throw().WithMessage("*CLI encountered unknown metadata*"); + } + finally + { + Environment.SetEnvironmentVariable("cmf_cli_internal_strict_step_parsing", null); + } + } } From a6b834b69b033e72b31a0ecc9292a122a94b7da6 Mon Sep 17 00:00:00 2001 From: Pedro Pinho Date: Thu, 23 Jul 2026 11:12:20 +0000 Subject: [PATCH 2/3] chore: add vscode scaffold-cli helper tasks --- .vscode/launch.json | 5 +-- .vscode/scaffold-cli-project.sh | 75 +++++++++++++++++++++++++++++++++ .vscode/tasks.json | 38 +++++++++++++++++ 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100755 .vscode/scaffold-cli-project.sh diff --git a/.vscode/launch.json b/.vscode/launch.json index a67ee461a..556634c4b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,10 +17,9 @@ "env": { "cmf_cli_feature__use_repository_clients": "true" }, - "cwd": "${workspaceFolder}/repo", + "cwd": "${workspaceFolder}", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console - "console": "internalConsole", - "stopAtEntry": false + "console": "internalConsole" }, { "name": ".NET Core Attach", diff --git a/.vscode/scaffold-cli-project.sh b/.vscode/scaffold-cli-project.sh new file mode 100755 index 000000000..3c3817157 --- /dev/null +++ b/.vscode/scaffold-cli-project.sh @@ -0,0 +1,75 @@ +#!/bin/bash +set -euo pipefail + +scaffold_project() { + local projectVersion="${1:-1.0.0}" + local mesVersion="${2:-12.0.0}" + + # Determine the workspace folder (parent directory of this script) + local scriptDir + scriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + local workspaceFolder + workspaceFolder="$(dirname "$scriptDir")" + + local cmfPath="${workspaceFolder}/cmf-cli/bin/Debug/cmf" + + if [ ! -x "$cmfPath" ]; then + echo "cmf executable not found at $cmfPath" + return 1 + fi + + # Create a fresh temp directory for this run + local tmpDir + tmpDir="$(mktemp -d -t cli-project-XXXXXX)" + cd "$tmpDir" + + local deploymentBaseDir="$tmpDir/deployment" + mkdir -p "$deploymentBaseDir" + + # Create infra.json + cat > infra.json << 'EOF' +{ + "NPMRegistry": "https://dev.criticalmanufacturing.io/repository/npm-public/", + "CmfPipelineRepository": "https://dev.criticalmanufacturing.io/repository/npm-public/", + "NuGetRegistry": "https://dev.criticalmanufacturing.io/repository/nuget-hosted/index.json" +} +EOF + + # Create env.json + cat > env.json << 'EOF' +{ + "SYSTEM_NAME": "cmftraining", + "TENANT_NAME": "cmftraining", + "APPLICATION_PUBLIC_HTTP_ADDRESS": "cmftraining.local", + "APPLICATION_PUBLIC_HTTP_PORT": "80", + "APPLICATION_PUBLIC_HTTP_TLS_ENABLED": "false", + "DATABASE_ONLINE_MSSQL_ADDRESS": "db", + "DATABASE_ONLINE_MSSQL_ADDRESS": "db", + "SECURITY_PORTAL_STRATEGY_LOCAL_AD_DEFAULT_DOMAIN": "", + "DATABASE_MSSQL_ALWAYS_ON_ENABLED": "false" +} +EOF + + # Run the cmf init command + local cmd=( + "$cmfPath" init ExampleProject + --version "$projectVersion" + --infra infra.json + --config env.json + --MESVersion "$mesVersion" + --deploymentDir "$deploymentBaseDir" + ) + echo "Executing: ${cmd[*]}" + "${cmd[@]}" + + cd "$tmpDir" + echo "Scaffolded project in: $tmpDir" + echo "Working directory is now: $(pwd)" + + if [[ "${BASH_SOURCE[0]}" == "$0" ]] && [[ -t 0 ]]; then + echo "Opening an interactive shell in the scaffolded project..." + exec "${SHELL:-/bin/bash}" -i + fi +} + +scaffold_project "$@" diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6c49d638b..87a8aff26 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -49,6 +49,32 @@ "type": "shell", "command": "dotnet test --configuration Release -verbosity diagnostic", "dependsOn": "use-node" + }, + { + "type": "shell", + "label": "scaffold-cli", + "command": "./.vscode/scaffold-cli-project.sh \"${input:ScaffoldProjectVersion}\" \"${input:ScaffoldMesVersion}\" && exec zsh -i", + "problemMatcher": [], + "presentation": { + "echo": false, + "focus": true, + "group": "scaffold-cli", + "panel": "dedicated" + }, + "runOptions": { + "runOn": "folderOpen" + } + }, + { + "type": "shell", + "label": "clean-all-scaffold-cli", + "command": "find /tmp -maxdepth 1 -type d -name 'cli-project*' -exec rm -rf {} +", + "problemMatcher": [], + "presentation": { + "echo": true, + "focus": false, + "panel": "dedicated" + } } ], "inputs": [ @@ -61,6 +87,18 @@ "20" ], "default": "18" + }, + { + "id": "ScaffoldProjectVersion", + "type": "promptString", + "description": "Insert scaffold project's version", + "default": "12.0.0" + }, + { + "id": "ScaffoldMesVersion", + "type": "promptString", + "description": "Insert scaffold project's MES version", + "default": "1.0.0" } ] } \ No newline at end of file From 07bcebfc05ec759ebe75482a6856ebd64ffb7293 Mon Sep 17 00:00:00 2001 From: Pedro Pinho Date: Wed, 22 Jul 2026 18:21:56 +0000 Subject: [PATCH 3/3] chore: bump version 6.0.0-2 --- CHANGES.md | 18 ++++++++++++++++++ cmf-cli/cmf.csproj | 2 +- core/core.csproj | 2 +- npm/package-lock.json | 4 ++-- npm/package.json | 2 +- 5 files changed, 23 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 00bedc824..cc4c1dd64 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,24 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## [6.0.0-2](https://github.com/criticalmanufacturing/cli/compare/6.0.0-1...6.0.0-2) (2026-07-22) + + +### Features + +* add intelisense to biz scenarios scripts ([d1ac39e](https://github.com/criticalmanufacturing/cli/commit/d1ac39ed5c31e11042425b3f60493a6d8ebb81a1)) +* update iot task scaffolding with new task bases ([#765](https://github.com/criticalmanufacturing/cli/issues/765)) ([6af4eaa](https://github.com/criticalmanufacturing/cli/commit/6af4eaab73962d4138039776c68cd98663449770)) +* update iot test package scaffolding to use test orchestrator ([9ca59d5](https://github.com/criticalmanufacturing/cli/commit/9ca59d5db7a46763688678353ba3708b292492f2)) +* update new iot driver scaffolding to handle file access ([28f7afc](https://github.com/criticalmanufacturing/cli/commit/28f7afc93210f973e79305cad13890d27d90dc36)) + + +### Bug Fixes + +* html config overrides oob config.json on deploy ([e7dede5](https://github.com/criticalmanufacturing/cli/commit/e7dede5906b4f1f93dd63be9d042e595e81eaa58)) +* iot tasks scaffolding handle empty choices ([860e6a4](https://github.com/criticalmanufacturing/cli/commit/860e6a47cd940b341c643f200984c0cfbe1e2602)) +* remove deprecated always_auth from npmrc sync ([692d8f6](https://github.com/criticalmanufacturing/cli/commit/692d8f6484cbf9a1cc12ed7de92a4195d46bc06c)) +* **scaffold:** use dynamic TargetFramework for data package Actions project ([a8fa808](https://github.com/criticalmanufacturing/cli/commit/a8fa80871a3a01191a07c7a28ebe568f11694952)), closes [#770](https://github.com/criticalmanufacturing/cli/issues/770) + ## [6.0.0-1](https://github.com/criticalmanufacturing/cli/compare/6.0.0-0...6.0.0-1) (2026-07-06) diff --git a/cmf-cli/cmf.csproj b/cmf-cli/cmf.csproj index 277d3cb3b..51c4bb68b 100644 --- a/cmf-cli/cmf.csproj +++ b/cmf-cli/cmf.csproj @@ -15,7 +15,7 @@ Name Namespace Namespaces - 6.0.0-1 + 6.0.0-2 diff --git a/core/core.csproj b/core/core.csproj index 7fab5077f..6875b304e 100644 --- a/core/core.csproj +++ b/core/core.csproj @@ -4,7 +4,7 @@ net10.0 CriticalManufacturing.CLI.Core Cmf.CLI.Core - 6.0.0-1 + 6.0.0-2 CriticalManufacturing CriticalManufacturing BSD-3-Clause diff --git a/npm/package-lock.json b/npm/package-lock.json index d44bacb1b..3fc615400 100644 --- a/npm/package-lock.json +++ b/npm/package-lock.json @@ -1,12 +1,12 @@ { "name": "@criticalmanufacturing/cli", - "version": "6.0.0-1", + "version": "6.0.0-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@criticalmanufacturing/cli", - "version": "6.0.0-1", + "version": "6.0.0-2", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { diff --git a/npm/package.json b/npm/package.json index 801588608..d1ee78152 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "@criticalmanufacturing/cli", - "version": "6.0.0-1", + "version": "6.0.0-2", "description": "Critical Manufacturing command line client", "bin": { "cmf": "run.js"