From 50887d6883b706e8e0e0a9a497269ed246d22546 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 6 Jan 2025 12:06:40 +1000 Subject: [PATCH] Support escaped colon in variables --- .../DeployV6/inputCommandBuilder.test.ts | 27 ++++++++++++++++++ .../Deploy/DeployV6/inputCommandBuilder.ts | 8 +++--- .../inputCommandBuilder.test.ts | 27 ++++++++++++++++++ .../TenantedDeployV6/inputCommandBuilder.ts | 6 ++-- source/tasks/Utils/inputs.ts | 28 +++++++++++++++++++ 5 files changed, 89 insertions(+), 7 deletions(-) diff --git a/source/tasks/Deploy/DeployV6/inputCommandBuilder.test.ts b/source/tasks/Deploy/DeployV6/inputCommandBuilder.test.ts index a0e84cec..240ea628 100644 --- a/source/tasks/Deploy/DeployV6/inputCommandBuilder.test.ts +++ b/source/tasks/Deploy/DeployV6/inputCommandBuilder.test.ts @@ -60,6 +60,33 @@ describe("getInputCommand", () => { expect(command.Variables).toStrictEqual({ var1: "value1", var2: "value2" }); }); + test("handles escaped colons in variable names", () => { + task.addVariableString("Space", "Default"); + task.addVariableString("Variables", "Test\\:Variable: Testing3"); + task.addVariableString("Environments", "test"); + task.addVariableString("Project", "Test project"); + task.addVariableString("ReleaseNumber", "1.0.0"); + task.addVariableString("DeployForTenants", "Tenant 1"); + + const command = createCommandFromInputs(logger, task); + expect(command.Variables).toStrictEqual({ "Test:Variable": "Testing3" }); + }); + + test("handles multiple variables with escaped and unescaped colons", () => { + task.addVariableString("Space", "Default"); + task.addVariableString("Variables", "Long\\:Variable\\:Name: Value123\nTest\\:Variable: Value: With: Colons"); + task.addVariableString("Environments", "test"); + task.addVariableString("Project", "Test project"); + task.addVariableString("ReleaseNumber", "1.0.0"); + task.addVariableString("DeployForTenants", "Tenant 1"); + + const command = createCommandFromInputs(logger, task); + expect(command.Variables).toStrictEqual({ + "Long:Variable:Name": "Value123", + "Test:Variable": "Value: With: Colons" + }); + }); + test("multiline environments", () => { task.addVariableString("Space", "Default"); task.addVariableString("Environments", "dev, test\nprod"); diff --git a/source/tasks/Deploy/DeployV6/inputCommandBuilder.ts b/source/tasks/Deploy/DeployV6/inputCommandBuilder.ts index bc0072d5..1c27c6c6 100644 --- a/source/tasks/Deploy/DeployV6/inputCommandBuilder.ts +++ b/source/tasks/Deploy/DeployV6/inputCommandBuilder.ts @@ -1,6 +1,6 @@ import commandLineArgs from "command-line-args"; import shlex from "shlex"; -import { getLineSeparatedItems } from "../../Utils/inputs"; +import { getLineSeparatedItems, parseVariableString } from "../../Utils/inputs"; import { CreateDeploymentUntenantedCommandV1, Logger, PromptedVariableValues } from "@octopusdeploy/api-client"; import { TaskWrapper } from "tasks/Utils/taskInput"; @@ -22,13 +22,13 @@ export function createCommandFromInputs(logger: Logger, task: TaskWrapper): Crea } const variablesField = task.getInput("Variables"); - logger.debug?.("Variables:" + variablesField); + logger.debug?.("Variables: " + variablesField); if (variablesField) { const variables = getLineSeparatedItems(variablesField).map((p) => p.trim()) || undefined; if (variables) { for (const variable of variables) { - const variableMap = variable.split(":").map((x) => x.trim()); - variablesMap[variableMap[0]] = variableMap[1]; + const [name, value] = parseVariableString(variable); + variablesMap[name] = value; } } } diff --git a/source/tasks/DeployTenant/TenantedDeployV6/inputCommandBuilder.test.ts b/source/tasks/DeployTenant/TenantedDeployV6/inputCommandBuilder.test.ts index 1cf15633..b69f3573 100644 --- a/source/tasks/DeployTenant/TenantedDeployV6/inputCommandBuilder.test.ts +++ b/source/tasks/DeployTenant/TenantedDeployV6/inputCommandBuilder.test.ts @@ -65,6 +65,33 @@ describe("getInputCommand", () => { expect(command.Variables).toStrictEqual({ var1: "value1", var2: "value2" }); }); + test("handles escaped colons in variable names", () => { + task.addVariableString("Space", "Default"); + task.addVariableString("Variables", "Test\\:Variable: Testing3"); + task.addVariableString("Environment", "dev"); + task.addVariableString("Project", "Test project"); + task.addVariableString("ReleaseNumber", "1.0.0"); + task.addVariableString("DeployForTenants", "Tenant 1"); + + const command = createCommandFromInputs(logger, task); + expect(command.Variables).toStrictEqual({ "Test:Variable": "Testing3" }); + }); + + test("handles multiple variables with escaped and unescaped colons", () => { + task.addVariableString("Space", "Default"); + task.addVariableString("Variables", "Long\\:Variable\\:Name: Value123\nTest\\:Variable: Value: With: Colons"); + task.addVariableString("Environment", "dev"); + task.addVariableString("Project", "Test project"); + task.addVariableString("ReleaseNumber", "1.0.0"); + task.addVariableString("DeployForTenants", "Tenant 1"); + + const command = createCommandFromInputs(logger, task); + expect(command.Variables).toStrictEqual({ + "Long:Variable:Name": "Value123", + "Test:Variable": "Value: With: Colons" + }); + }); + test("validate tenants and tags", () => { task.addVariableString("Space", "Default"); task.addVariableString("Project", "project 1"); diff --git a/source/tasks/DeployTenant/TenantedDeployV6/inputCommandBuilder.ts b/source/tasks/DeployTenant/TenantedDeployV6/inputCommandBuilder.ts index cd8d80cb..7a78701c 100644 --- a/source/tasks/DeployTenant/TenantedDeployV6/inputCommandBuilder.ts +++ b/source/tasks/DeployTenant/TenantedDeployV6/inputCommandBuilder.ts @@ -1,6 +1,6 @@ import commandLineArgs from "command-line-args"; import shlex from "shlex"; -import { getLineSeparatedItems } from "../../Utils/inputs"; +import { getLineSeparatedItems, parseVariableString } from "../../Utils/inputs"; import { CreateDeploymentTenantedCommandV1, Logger, PromptedVariableValues } from "@octopusdeploy/api-client"; import { TaskWrapper } from "tasks/Utils/taskInput"; @@ -32,8 +32,8 @@ export function createCommandFromInputs(logger: Logger, task: TaskWrapper): Crea const variables = getLineSeparatedItems(variablesField).map((p) => p.trim()) || undefined; if (variables) { for (const variable of variables) { - const variableMap = variable.split(":").map((x) => x.trim()); - variablesMap[variableMap[0]] = variableMap[1]; + const [name, value] = parseVariableString(variable); + variablesMap[name] = value; } } } diff --git a/source/tasks/Utils/inputs.ts b/source/tasks/Utils/inputs.ts index 9805cc1b..6e67d1d9 100644 --- a/source/tasks/Utils/inputs.ts +++ b/source/tasks/Utils/inputs.ts @@ -12,6 +12,34 @@ export function getLineSeparatedItems(value: string): Array { return value ? value.split(/[\r\n]+/g).map((x) => x.trim()) : []; } +export function parseVariableString(input: string): [string, string] { + let escapeNext = false; + let colonIndex = -1; + + for (let i = 0; i < input.length; i++) { + if (input[i] === '\\' && !escapeNext) { + escapeNext = true; + continue; + } + + if (input[i] === ':' && !escapeNext) { + colonIndex = i; + break; + } + + escapeNext = false; + } + + if (colonIndex === -1) { + throw new Error(`Invalid variable format. Expected 'name: value' but got '${input}'`); + } + + const variableName = input.substring(0, colonIndex).replace(/\\:/g, ':').trim(); + const variableValue = input.substring(colonIndex + 1).trim(); + + return [variableName, variableValue]; +} + export function getOverwriteModeFromReplaceInput(replace: string): ReplaceOverwriteMode { return ReplaceOverwriteMode[replace as keyof typeof ReplaceOverwriteMode] || ReplaceOverwriteMode.false; }