Skip to content

Commit fdadc7d

Browse files
authored
feat: add support for deploy_at and deploy_at_expiry inputs (#296)
1 parent 0eb1457 commit fdadc7d

File tree

5 files changed

+116
-2
lines changed

5 files changed

+116
-2
lines changed

__tests__/integration/integration.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
RunCondition,
1919
RunConditionForAction,
2020
ServerTask,
21+
ServerTaskRepository,
22+
SpaceServerTaskRepository,
2123
StartTrigger,
2224
ServerTaskWaiter
2325
} from '@octopusdeploy/api-client'
@@ -184,6 +186,60 @@ describe('integration tests', () => {
184186
}
185187
})
186188

189+
test('can deploy a release with scheduled start time', async () => {
190+
const output = new CaptureOutput()
191+
192+
const logger: Logger = {
193+
debug: message => output.debug(message),
194+
info: message => output.info(message),
195+
warn: message => output.warn(message),
196+
error: (message, err) => {
197+
if (err !== undefined) {
198+
output.error(err.message)
199+
} else {
200+
output.error(message)
201+
}
202+
}
203+
}
204+
205+
const config: ClientConfiguration = {
206+
userAgentApp: 'Test',
207+
instanceURL: apiClientConfig.instanceURL,
208+
apiKey: apiClientConfig.apiKey,
209+
logging: logger
210+
}
211+
212+
const client = await Client.create(config)
213+
214+
await createReleaseForTest(client)
215+
const runAt = new Date(Date.now() + 5 * 60 * 1000)
216+
const noRunAfter = new Date(Date.now() + 10 * 60 * 1000)
217+
218+
const result = await createDeploymentFromInputs(client, {
219+
...standardInputParameters,
220+
releaseNumber: localReleaseNumber,
221+
environments: ['Dev'],
222+
runAt,
223+
noRunAfter
224+
})
225+
226+
expect(result.length).toBe(1)
227+
expect(result[0].serverTaskId).toContain('ServerTasks-')
228+
expect(output.getAllMessages()).toContain(`[INFO] 🎉 1 Deployment queued successfully!`)
229+
230+
const spaceTaskRepository = new SpaceServerTaskRepository(client, spaceName)
231+
for (const { serverTaskId } of result) {
232+
const task = await spaceTaskRepository.getById(serverTaskId)
233+
expect(new Date(task.QueueTime!)).toStrictEqual(runAt)
234+
expect(new Date(task.QueueTimeExpiry!)).toStrictEqual(noRunAfter)
235+
}
236+
237+
const taskRepository = new ServerTaskRepository(client)
238+
for (const { serverTaskId } of result) {
239+
await taskRepository.cancel(serverTaskId)
240+
}
241+
})
242+
187243
test('can deploy a release', async () => {
188244
const output = new CaptureOutput()
189245

__tests__/unit/input-parsing.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,42 @@ test('get input parameters', () => {
99
expect(inputParameters.variables).toBeDefined()
1010
expect(inputParameters.variables?.['foo']).toBe('quux')
1111
expect(inputParameters.variables?.['bar']).toBe('xyzzy')
12+
expect(inputParameters.runAt).toBeUndefined()
13+
expect(inputParameters.noRunAfter).toBeUndefined()
14+
})
15+
16+
test('deploy_at and deploy_at_expiry are parsed as dates', () => {
17+
const original = process.env
18+
process.env = Object.assign({}, process.env, {
19+
INPUT_DEPLOY_AT: '2026-04-02T09:00:00+10:00',
20+
INPUT_DEPLOY_AT_EXPIRY: '2026-04-02T17:00:00+10:00'
21+
})
22+
23+
const inputParameters = getInputParameters()
24+
expect(inputParameters.runAt).toStrictEqual(new Date('2026-04-02T09:00:00+10:00'))
25+
expect(inputParameters.noRunAfter).toStrictEqual(new Date('2026-04-02T17:00:00+10:00'))
26+
27+
process.env = original
28+
})
29+
30+
test('invalid deploy_at throws error', () => {
31+
const original = process.env
32+
process.env = Object.assign({}, process.env, {
33+
INPUT_DEPLOY_AT: 'notadate'
34+
})
35+
36+
expect(() => getInputParameters()).toThrowError("deploy_at 'notadate' is not a valid ISO 8601 date-time string.")
37+
38+
process.env = original
39+
})
40+
41+
test('invalid deploy_at_expiry throws error', () => {
42+
const original = process.env
43+
process.env = Object.assign({}, process.env, {
44+
INPUT_DEPLOY_AT_EXPIRY: 'notadate'
45+
})
46+
47+
expect(() => getInputParameters()).toThrowError("deploy_at_expiry 'notadate' is not a valid ISO 8601 date-time string.")
48+
49+
process.env = original
1250
})

action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ inputs:
2020
description: 'Whether to use guided failure mode if errors occur during the deployment.'
2121
variables:
2222
description: 'A multi-line list of prompted variable values. Format: name:value'
23+
deploy_at:
24+
description: 'Schedule the deployment to run at a specific time. Provide an ISO 8601 date-time string (e.g. 2026-04-01T09:00:00+10:00). Leave blank to deploy immediately.'
25+
deploy_at_expiry:
26+
description: 'Cancel the deployment if it has not started by this time. Provide an ISO 8601 date-time string. Leave blank for no expiry.'
2327
server:
2428
description: 'The instance URL hosting Octopus Deploy (i.e. "https://octopus.example.com/"). The instance URL is required, but you may also use the OCTOPUS_URL environment variable.'
2529
api_key:

src/api-wrapper.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ export async function createDeploymentFromInputs(
2727
ReleaseVersion: parameters.releaseNumber,
2828
EnvironmentNames: parameters.environments,
2929
UseGuidedFailure: parameters.useGuidedFailure,
30-
Variables: parameters.variables
30+
Variables: parameters.variables,
31+
RunAt: parameters.runAt,
32+
NoRunAfter: parameters.noRunAfter
3133
}
3234

3335
const deploymentRepository = new DeploymentRepository(client, parameters.space)

src/input-parameters.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export interface InputParameters {
2525
// Optional
2626
useGuidedFailure?: boolean
2727
variables?: PromptedVariableValues
28+
runAt?: Date
29+
noRunAfter?: Date
2830
}
2931

3032
export function getInputParameters(): InputParameters {
@@ -47,7 +49,9 @@ export function getInputParameters(): InputParameters {
4749
releaseNumber: getInput('release_number', { required: true }),
4850
environments: getMultilineInput('environments', { required: true }).map(p => p.trim()),
4951
useGuidedFailure: getBooleanInput('use_guided_failure') || undefined,
50-
variables: variablesMap
52+
variables: variablesMap,
53+
runAt: getInput('deploy_at') ? new Date(getInput('deploy_at')) : undefined,
54+
noRunAfter: getInput('deploy_at_expiry') ? new Date(getInput('deploy_at_expiry')) : undefined
5155
}
5256

5357
const errors: string[] = []
@@ -69,6 +73,16 @@ export function getInputParameters(): InputParameters {
6973
)
7074
}
7175

76+
const deployAt = getInput('deploy_at')
77+
if (deployAt && isNaN(new Date(deployAt).getTime())) {
78+
errors.push(`deploy_at '${deployAt}' is not a valid ISO 8601 date-time string.`)
79+
}
80+
81+
const deployAtExpiry = getInput('deploy_at_expiry')
82+
if (deployAtExpiry && isNaN(new Date(deployAtExpiry).getTime())) {
83+
errors.push(`deploy_at_expiry '${deployAtExpiry}' is not a valid ISO 8601 date-time string.`)
84+
}
85+
7286
if (errors.length > 0) {
7387
throw new Error(errors.join('\n'))
7488
}

0 commit comments

Comments
 (0)