Skip to content

Commit a9ec347

Browse files
feat: Add retry parameters to OctopusAwaitTask (#351)
* Add retry support for AwaitTask * bump api-client version
1 parent ed7c868 commit a9ec347

File tree

8 files changed

+185
-82
lines changed

8 files changed

+185
-82
lines changed

package-lock.json

Lines changed: 38 additions & 69 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"yargs": "^17.5.1"
3939
},
4040
"dependencies": {
41-
"@octopusdeploy/api-client": "^3.7.0",
41+
"@octopusdeploy/api-client": "^3.11.0",
4242
"azure-devops-node-api": "11.2.0",
4343
"azure-pipelines-task-lib": "^4.13.0",
4444
"azure-pipelines-tool-lib": "^2.0.7",

source/tasks/AwaitTask/AwaitTaskV6/input-parameters.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export interface InputParameters {
1010
timeout: number;
1111
showProgress: boolean;
1212
cancelOnTimeout: boolean;
13+
maxRetries: number;
14+
retryBackoffSeconds: number;
1315
}
1416

1517
export function getInputParameters(logger: Logger, task: TaskWrapper): InputParameters {
@@ -51,14 +53,38 @@ export function getInputParameters(logger: Logger, task: TaskWrapper): InputPara
5153

5254
const cancelOnTimeout = task.getBoolean("CancelOnTimeout") ?? false;
5355

56+
let maxRetries = 3;
57+
const maxRetriesField = task.getInput("MaxRetries");
58+
if (maxRetriesField) {
59+
const parsed = parseInt(maxRetriesField, 10);
60+
if (!isNaN(parsed)) {
61+
maxRetries = parsed;
62+
} else {
63+
logger.warn?.(`Invalid MaxRetries value '${maxRetriesField}', using default: 3`);
64+
}
65+
}
66+
67+
let retryBackoffSeconds = 5;
68+
const retryBackoffField = task.getInput("RetryBackoffSeconds");
69+
if (retryBackoffField) {
70+
const parsed = parseInt(retryBackoffField, 10);
71+
if (!isNaN(parsed)) {
72+
retryBackoffSeconds = parsed;
73+
} else {
74+
logger.warn?.(`Invalid RetryBackoffSeconds value '${retryBackoffField}', using default: 5`);
75+
}
76+
}
77+
5478
const parameters: InputParameters = {
5579
space: task.getInput("Space") || "",
5680
step: step,
5781
tasks: tasks,
5882
showProgress: showProgress,
5983
pollingInterval: pollingInterval,
6084
timeout: timeoutSeconds,
61-
cancelOnTimeout: cancelOnTimeout
85+
cancelOnTimeout: cancelOnTimeout,
86+
maxRetries: maxRetries,
87+
retryBackoffSeconds: retryBackoffSeconds
6288
};
6389

6490
const errors: string[] = [];

source/tasks/AwaitTask/AwaitTaskV6/task.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@
7373
"defaultValue": "false",
7474
"required": false,
7575
"helpMarkDown": "Cancel the Octopus task and mark this task as failed if the timeout is reached. (Default: false)"
76+
},
77+
{
78+
"name": "MaxRetries",
79+
"type": "int",
80+
"label": "Max Retries",
81+
"defaultValue": "3",
82+
"required": false,
83+
"helpMarkDown": "Number of times to retry failed HTTP requests due to network issues before failing. (Default: 3)"
84+
},
85+
{
86+
"name": "RetryBackoffSeconds",
87+
"type": "int",
88+
"label": "Retry Backoff (seconds)",
89+
"defaultValue": "5",
90+
"required": false,
91+
"helpMarkDown": "Initial delay between retries. Delay doubles with each retry (exponential backoff). (Default: 5s, then 10s, 20s, etc.)"
7692
}
7793
],
7894
"outputVariables": [

source/tasks/AwaitTask/AwaitTaskV6/waiter.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,31 @@ export class Waiter {
4848
this.task.setOutputVariable("server_task_results", JSON.stringify(waitExecutionResults));
4949

5050
} catch (error) {
51-
if (error instanceof Error && error.message.includes("Timeout reached") && error.message.includes("cancelled")) {
52-
this.task.setFailure(error.message);
51+
if (error instanceof Error) {
52+
if (error.message.includes("Timeout reached") && error.message.includes("cancelled")) {
53+
this.task.setFailure(error.message);
54+
this.task.setOutputVariable("completed_successfully", "false");
55+
return;
56+
}
57+
58+
if (error.message.includes("Failed to connect to Octopus server after")) {
59+
this.task.setFailure(
60+
`${error.message}\n\n` +
61+
`This indicates repeated network failures. You can:\n` +
62+
`- Check network connectivity between ADO agent and Octopus server\n` +
63+
`- Increase MaxRetries or RetryBackoffSeconds parameters\n` +
64+
`- Check Octopus server health and logs`
65+
);
66+
} else if (error.message.includes("Unknown task Id")) {
67+
this.task.setFailure(error.message);
68+
} else {
69+
this.task.setFailure(`Failed to wait for tasks: ${error.message}`);
70+
}
71+
5372
this.task.setOutputVariable("completed_successfully", "false");
5473
return;
5574
}
56-
75+
5776
this.task.setFailure(`Failed to wait for tasks: ${error}`);
5877
this.task.setOutputVariable("completed_successfully", "false");
5978
}
@@ -71,7 +90,10 @@ export class Waiter {
7190
}
7291

7392
async waitWithoutProgress(client: Client, inputParameters: InputParameters): Promise<WaitExecutionResult[]> {
74-
const waiter = new ServerTaskWaiter(client, inputParameters.space);
93+
const waiter = new ServerTaskWaiter(client, inputParameters.space, {
94+
maxRetries: inputParameters.maxRetries,
95+
retryBackoffMs: inputParameters.retryBackoffSeconds * 1000,
96+
});
7597
const taskIds = inputParameters.tasks.map((t) => t.serverTaskId);
7698
const lookup = new Map(inputParameters.tasks.map((t) => [t.serverTaskId, t]));
7799

@@ -97,7 +119,10 @@ export class Waiter {
97119
}
98120

99121
async waitWithProgress(client: Client, inputParameters: InputParameters): Promise<WaitExecutionResult[]> {
100-
const waiter = new ServerTaskWaiter(client, inputParameters.space);
122+
const waiter = new ServerTaskWaiter(client, inputParameters.space, {
123+
maxRetries: inputParameters.maxRetries,
124+
retryBackoffMs: inputParameters.retryBackoffSeconds * 1000,
125+
});
101126
const taskIds = inputParameters.tasks.map((t) => t.serverTaskId);
102127
const taskLookup = new Map(inputParameters.tasks.map((t) => [t.serverTaskId, t]));
103128

0 commit comments

Comments
 (0)