diff --git a/source/tasks/Deploy/DeployV6/inputCommandBuilder.test.ts b/source/tasks/Deploy/DeployV6/inputCommandBuilder.test.ts index 240ea62..b7246da 100644 --- a/source/tasks/Deploy/DeployV6/inputCommandBuilder.test.ts +++ b/source/tasks/Deploy/DeployV6/inputCommandBuilder.test.ts @@ -95,4 +95,60 @@ describe("getInputCommand", () => { const command = createCommandFromInputs(logger, task); expect(command.EnvironmentNames).toStrictEqual(["dev", "test", "prod"]); }); + + test("DeployAt populates RunAt", () => { + task.addVariableString("Space", "Default"); + task.addVariableString("Environments", "test"); + task.addVariableString("Project", "project 1"); + task.addVariableString("ReleaseNumber", "1.2.3"); + task.addVariableString("DeployAt", "2026-04-02T09:00:00+10:00"); + + const command = createCommandFromInputs(logger, task); + expect(command.RunAt).toStrictEqual(new Date("2026-04-02T09:00:00+10:00")); + expect(command.NoRunAfter).toBeUndefined(); + }); + + test("DeployAtExpiry populates NoRunAfter", () => { + task.addVariableString("Space", "Default"); + task.addVariableString("Environments", "test"); + task.addVariableString("Project", "project 1"); + task.addVariableString("ReleaseNumber", "1.2.3"); + task.addVariableString("DeployAt", "2026-04-02T09:00:00+10:00"); + task.addVariableString("DeployAtExpiry", "2026-04-02T17:00:00+10:00"); + + const command = createCommandFromInputs(logger, task); + expect(command.RunAt).toStrictEqual(new Date("2026-04-02T09:00:00+10:00")); + expect(command.NoRunAfter).toStrictEqual(new Date("2026-04-02T17:00:00+10:00")); + }); + + test("DeployAt and DeployAtExpiry absent when inputs not provided", () => { + task.addVariableString("Space", "Default"); + task.addVariableString("Environments", "test"); + task.addVariableString("Project", "project 1"); + task.addVariableString("ReleaseNumber", "1.2.3"); + + const command = createCommandFromInputs(logger, task); + expect(command.RunAt).toBeUndefined(); + expect(command.NoRunAfter).toBeUndefined(); + }); + + test("invalid DeployAt throws error", () => { + task.addVariableString("Space", "Default"); + task.addVariableString("Environments", "test"); + task.addVariableString("Project", "project 1"); + task.addVariableString("ReleaseNumber", "1.2.3"); + task.addVariableString("DeployAt", "notadate"); + + expect(() => createCommandFromInputs(logger, task)).toThrowError("DeployAt 'notadate' is not a valid ISO 8601 date-time string."); + }); + + test("invalid DeployAtExpiry throws error", () => { + task.addVariableString("Space", "Default"); + task.addVariableString("Environments", "test"); + task.addVariableString("Project", "project 1"); + task.addVariableString("ReleaseNumber", "1.2.3"); + task.addVariableString("DeployAtExpiry", "notadate"); + + expect(() => createCommandFromInputs(logger, task)).toThrowError("DeployAtExpiry 'notadate' is not a valid ISO 8601 date-time string."); + }); }); diff --git a/source/tasks/Deploy/DeployV6/inputCommandBuilder.ts b/source/tasks/Deploy/DeployV6/inputCommandBuilder.ts index 1c27c6c..b741d75 100644 --- a/source/tasks/Deploy/DeployV6/inputCommandBuilder.ts +++ b/source/tasks/Deploy/DeployV6/inputCommandBuilder.ts @@ -44,6 +44,9 @@ export function createCommandFromInputs(logger: Logger, task: TaskWrapper): Crea } logger.debug?.("Environments:" + environmentsField); + const deployAt = task.getInput("DeployAt"); + const deployAtExpiry = task.getInput("DeployAtExpiry"); + const command: CreateDeploymentUntenantedCommandV1 = { spaceName: task.getInput("Space", true) || "", ProjectName: task.getInput("Project", true) || "", @@ -51,12 +54,20 @@ export function createCommandFromInputs(logger: Logger, task: TaskWrapper): Crea EnvironmentNames: environments, UseGuidedFailure: task.getBoolean("UseGuidedFailure") || undefined, Variables: variablesMap || undefined, + RunAt: deployAt ? new Date(deployAt) : undefined, + NoRunAfter: deployAtExpiry ? new Date(deployAtExpiry) : undefined, }; const errors: string[] = []; if (command.spaceName === "") { errors.push("The Octopus space name is required."); } + if (deployAt && isNaN(new Date(deployAt).getTime())) { + errors.push(`DeployAt '${deployAt}' is not a valid ISO 8601 date-time string.`); + } + if (deployAtExpiry && isNaN(new Date(deployAtExpiry).getTime())) { + errors.push(`DeployAtExpiry '${deployAtExpiry}' is not a valid ISO 8601 date-time string.`); + } if (errors.length > 0) { throw new Error("Failed to successfully build parameters.\n" + errors.join("\n")); diff --git a/source/tasks/Deploy/DeployV6/task.json b/source/tasks/Deploy/DeployV6/task.json index 21643ec..ff35bbd 100644 --- a/source/tasks/Deploy/DeployV6/task.json +++ b/source/tasks/Deploy/DeployV6/task.json @@ -81,6 +81,24 @@ "required": false, "helpMarkDown": "Whether to use guided failure mode if errors occur during the deployment." }, + { + "name": "DeployAt", + "type": "string", + "label": "Scheduled deployment time", + "defaultValue": "", + "required": false, + "helpMarkDown": "Schedule the deployment to run at a specific time. Accepts an ISO 8601 date-time string.", + "groupName": "advanced" + }, + { + "name": "DeployAtExpiry", + "type": "string", + "label": "Deployment expiry time", + "defaultValue": "", + "required": false, + "helpMarkDown": "Cancel the deployment if it has not started by this time. Accepts an ISO 8601 date-time string.", + "groupName": "advanced" + }, { "name": "AdditionalArguments", "type": "string", diff --git a/source/tasks/Deploy/DeployV7/inputCommandBuilder.test.ts b/source/tasks/Deploy/DeployV7/inputCommandBuilder.test.ts index 240ea62..b7246da 100644 --- a/source/tasks/Deploy/DeployV7/inputCommandBuilder.test.ts +++ b/source/tasks/Deploy/DeployV7/inputCommandBuilder.test.ts @@ -95,4 +95,60 @@ describe("getInputCommand", () => { const command = createCommandFromInputs(logger, task); expect(command.EnvironmentNames).toStrictEqual(["dev", "test", "prod"]); }); + + test("DeployAt populates RunAt", () => { + task.addVariableString("Space", "Default"); + task.addVariableString("Environments", "test"); + task.addVariableString("Project", "project 1"); + task.addVariableString("ReleaseNumber", "1.2.3"); + task.addVariableString("DeployAt", "2026-04-02T09:00:00+10:00"); + + const command = createCommandFromInputs(logger, task); + expect(command.RunAt).toStrictEqual(new Date("2026-04-02T09:00:00+10:00")); + expect(command.NoRunAfter).toBeUndefined(); + }); + + test("DeployAtExpiry populates NoRunAfter", () => { + task.addVariableString("Space", "Default"); + task.addVariableString("Environments", "test"); + task.addVariableString("Project", "project 1"); + task.addVariableString("ReleaseNumber", "1.2.3"); + task.addVariableString("DeployAt", "2026-04-02T09:00:00+10:00"); + task.addVariableString("DeployAtExpiry", "2026-04-02T17:00:00+10:00"); + + const command = createCommandFromInputs(logger, task); + expect(command.RunAt).toStrictEqual(new Date("2026-04-02T09:00:00+10:00")); + expect(command.NoRunAfter).toStrictEqual(new Date("2026-04-02T17:00:00+10:00")); + }); + + test("DeployAt and DeployAtExpiry absent when inputs not provided", () => { + task.addVariableString("Space", "Default"); + task.addVariableString("Environments", "test"); + task.addVariableString("Project", "project 1"); + task.addVariableString("ReleaseNumber", "1.2.3"); + + const command = createCommandFromInputs(logger, task); + expect(command.RunAt).toBeUndefined(); + expect(command.NoRunAfter).toBeUndefined(); + }); + + test("invalid DeployAt throws error", () => { + task.addVariableString("Space", "Default"); + task.addVariableString("Environments", "test"); + task.addVariableString("Project", "project 1"); + task.addVariableString("ReleaseNumber", "1.2.3"); + task.addVariableString("DeployAt", "notadate"); + + expect(() => createCommandFromInputs(logger, task)).toThrowError("DeployAt 'notadate' is not a valid ISO 8601 date-time string."); + }); + + test("invalid DeployAtExpiry throws error", () => { + task.addVariableString("Space", "Default"); + task.addVariableString("Environments", "test"); + task.addVariableString("Project", "project 1"); + task.addVariableString("ReleaseNumber", "1.2.3"); + task.addVariableString("DeployAtExpiry", "notadate"); + + expect(() => createCommandFromInputs(logger, task)).toThrowError("DeployAtExpiry 'notadate' is not a valid ISO 8601 date-time string."); + }); }); diff --git a/source/tasks/Deploy/DeployV7/inputCommandBuilder.ts b/source/tasks/Deploy/DeployV7/inputCommandBuilder.ts index 1c27c6c..b741d75 100644 --- a/source/tasks/Deploy/DeployV7/inputCommandBuilder.ts +++ b/source/tasks/Deploy/DeployV7/inputCommandBuilder.ts @@ -44,6 +44,9 @@ export function createCommandFromInputs(logger: Logger, task: TaskWrapper): Crea } logger.debug?.("Environments:" + environmentsField); + const deployAt = task.getInput("DeployAt"); + const deployAtExpiry = task.getInput("DeployAtExpiry"); + const command: CreateDeploymentUntenantedCommandV1 = { spaceName: task.getInput("Space", true) || "", ProjectName: task.getInput("Project", true) || "", @@ -51,12 +54,20 @@ export function createCommandFromInputs(logger: Logger, task: TaskWrapper): Crea EnvironmentNames: environments, UseGuidedFailure: task.getBoolean("UseGuidedFailure") || undefined, Variables: variablesMap || undefined, + RunAt: deployAt ? new Date(deployAt) : undefined, + NoRunAfter: deployAtExpiry ? new Date(deployAtExpiry) : undefined, }; const errors: string[] = []; if (command.spaceName === "") { errors.push("The Octopus space name is required."); } + if (deployAt && isNaN(new Date(deployAt).getTime())) { + errors.push(`DeployAt '${deployAt}' is not a valid ISO 8601 date-time string.`); + } + if (deployAtExpiry && isNaN(new Date(deployAtExpiry).getTime())) { + errors.push(`DeployAtExpiry '${deployAtExpiry}' is not a valid ISO 8601 date-time string.`); + } if (errors.length > 0) { throw new Error("Failed to successfully build parameters.\n" + errors.join("\n")); diff --git a/source/tasks/Deploy/DeployV7/task.json b/source/tasks/Deploy/DeployV7/task.json index 0be3eab..77771a9 100644 --- a/source/tasks/Deploy/DeployV7/task.json +++ b/source/tasks/Deploy/DeployV7/task.json @@ -81,6 +81,24 @@ "required": false, "helpMarkDown": "Whether to use guided failure mode if errors occur during the deployment." }, + { + "name": "DeployAt", + "type": "string", + "label": "Scheduled deployment time", + "defaultValue": "", + "required": false, + "helpMarkDown": "Schedule the deployment to run at a specific time. Accepts an ISO 8601 date-time string.", + "groupName": "advanced" + }, + { + "name": "DeployAtExpiry", + "type": "string", + "label": "Deployment expiry time", + "defaultValue": "", + "required": false, + "helpMarkDown": "Cancel the deployment if it has not started by this time. Accepts an ISO 8601 date-time string.", + "groupName": "advanced" + }, { "name": "AdditionalArguments", "type": "string",