Skip to content

Commit 4312cad

Browse files
marcizhuclaude
andcommitted
Fail the run when a Teamwork request fails
curl ran without --fail, so HTTP errors (a revoked token, a task the token can't see, a Teamwork outage) were only logged and the run still passed. A failed task lookup also carried on with "null" as the project id. Network errors, on the other hand, aborted the run halfway, and a hung API held the runner until the job timed out. Pass --fail-with-body and --max-time 30, log what failed, carry on with the other steps and tasks, then exit 1 if anything failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5d17ed3 commit 4312cad

7 files changed

Lines changed: 80 additions & 52 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Create the next environment vars in your repository:
2121
* `TEAMWORK_URI`: The URL of your installation (e.g.: https://yourcompany.teamwork.com)
2222
* `TEAMWORK_API_TOKEN`: The API token to authenticate the workflow. Follow [this guide](https://support.teamwork.com/projects/using-teamwork/locating-your-api-key) to find your URL and API key.
2323

24-
**Please Note:** The Teamwork account associated with this API key is the account which these comments will be created under. If this user does not have permission to access the project, this action will be ignored.
24+
**Please Note:** The Teamwork account associated with this API key is the account which these comments will be created under. If this user does not have permission to access a task's project, that task will be skipped and the workflow run will be marked as failed.
2525

2626
`GITHUB_TOKEN` doesn't need to be setup in the repository, this var is always available during the workflows execution.
2727

@@ -59,7 +59,7 @@ jobs:
5959
## Usage
6060
When creating a new PR, write in the description of the PR the URL of the task. The action will automatically add a comment in the task. Optionally, you may choose for a lightweight comment that will only include the PR title, URL and the user that performed the action, to enable this option set the `LIGHTWEIGHT_COMMENT` input to `true`.
6161

62-
Please note, the comment will be created in Teamwork under the account you have attached to this action. If the API key of the user you are using does not have permissions to access certain projects, the comment will not be created.
62+
Please note, the comment will be created in Teamwork under the account you have attached to this action. If the API key of the user you are using does not have permissions to access certain projects, the comment will not be created and the workflow run will report an error.
6363

6464
![GitHub pr comment](./.github/assets/github_pr_comment.png)
6565

‎src/main.sh‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,18 @@ main() {
6767
exit 0
6868
fi
6969

70+
TEAMWORK_FAILED_REQUESTS=0
71+
7072
local project_id
7173
IFS=',' read -r -a task_ids <<< "$task_ids_str"
7274
for task_id in "${task_ids[@]}"; do
7375
log::message "Task found with the id: $task_id"
7476

7577
export TEAMWORK_TASK_ID=$task_id
76-
project_id="$(teamwork::get_project_id_from_task "$task_id")"
78+
if ! project_id="$(teamwork::get_project_id_from_task "$task_id")" || [[ ! $project_id =~ ^[0-9]+$ ]]; then
79+
teamwork::request_failed "Failed to get the project of task $task_id, check the task exists and the API token's user can access it"
80+
continue
81+
fi
7782
export TEAMWORK_PROJECT_ID=$project_id
7883

7984
ignored_project_ids=("${IGNORE_PROJECT_IDS:-}")
@@ -86,5 +91,10 @@ main() {
8691
"$handler"
8792
done
8893

89-
exit $?
94+
if ((TEAMWORK_FAILED_REQUESTS > 0)); then
95+
log::error "$TEAMWORK_FAILED_REQUESTS Teamwork request(s) failed, see the errors above"
96+
exit 1
97+
fi
98+
99+
exit 0
90100
}

‎src/teamwork.sh‎

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,28 @@ teamwork::get_task_id_from_body() {
1818
echo "$task_ids_str"
1919
}
2020

21-
# Calls the Teamwork API and prints the response body.
21+
# Calls the Teamwork API and prints the response body. Fails on network errors,
22+
# timeouts and HTTP error statuses, printing the error body in the latter case.
2223
teamwork::request() {
2324
local -r method=$1
2425
local -r path=$2
2526
local -r payload=${3:-}
2627

27-
local args=(-sS -X "$method" -u "$TEAMWORK_API_TOKEN"':')
28+
local args=(-sS --fail-with-body --max-time 30 -X "$method" -u "$TEAMWORK_API_TOKEN"':')
2829
if [ -n "$payload" ]; then
2930
args+=(-H 'Content-Type: application/json; charset=utf-8' -d "$payload")
3031
fi
3132

3233
curl "${args[@]}" "$TEAMWORK_URI$path"
3334
}
3435

36+
# Logs a failed request and records it, so the run fails once every task has
37+
# been processed.
38+
teamwork::request_failed() {
39+
log::error "$1"
40+
TEAMWORK_FAILED_REQUESTS=$((TEAMWORK_FAILED_REQUESTS + 1))
41+
}
42+
3543
teamwork::get_project_id_from_task() {
3644
local -r task_id=$1
3745

@@ -40,17 +48,15 @@ teamwork::get_project_id_from_task() {
4048
return
4149
fi
4250

43-
response=$(
44-
teamwork::request GET "/projects/api/v1/tasks/$task_id.json" |\
45-
jq -r '.["todo-item"]["project-id"]'
46-
)
47-
echo "$response"
51+
teamwork::request GET "/projects/api/v1/tasks/$task_id.json" |\
52+
jq -r '.["todo-item"]["project-id"]'
4853
}
4954

5055
# Resolves a workflow stage name to its "<workflowId> <stageId>" pair within the
5156
# task's project. Matching is a case-sensitive substring match against the stage
5257
# name (mirroring the legacy board-column behaviour), and the first match wins.
53-
# Echoes nothing when no stage name is provided or no stage matches.
58+
# Echoes nothing when no stage name is provided or no stage matches, and fails
59+
# when the stages can't be fetched.
5460
teamwork::get_workflow_stage() {
5561
local -r stage_name=$1
5662

@@ -79,7 +85,11 @@ teamwork::move_task_to_stage() {
7985
return
8086
fi
8187

82-
local -r stage=$(teamwork::get_workflow_stage "$stage_name")
88+
local stage
89+
if ! stage=$(teamwork::get_workflow_stage "$stage_name"); then
90+
teamwork::request_failed "Failed to fetch the workflow stages of project $TEAMWORK_PROJECT_ID"
91+
return
92+
fi
8393
if [ -z "$stage" ]; then
8494
log::message "Failed to find a matching workflow stage for '$stage_name'"
8595
return
@@ -93,8 +103,11 @@ teamwork::move_task_to_stage() {
93103
return
94104
fi
95105

96-
response=$(teamwork::request PATCH "/projects/api/v3/tasks/$TEAMWORK_TASK_ID/workflows/$workflow_id.json" \
97-
"{ \"stageId\": $stage_id, \"positionAfterTask\": -1 }")
106+
if ! response=$(teamwork::request PATCH "/projects/api/v3/tasks/$TEAMWORK_TASK_ID/workflows/$workflow_id.json" \
107+
"{ \"stageId\": $stage_id, \"positionAfterTask\": -1 }"); then
108+
teamwork::request_failed "Failed to move task $TEAMWORK_TASK_ID to the '$stage_name' workflow stage: $response"
109+
return
110+
fi
98111

99112
log::message "$response"
100113
}
@@ -111,7 +124,10 @@ teamwork::add_comment() {
111124
local -r payload=$(jq -n --arg body "$body" \
112125
'{ comment: { body: $body, notify: true, "content-type": "text", isprivate: false } }')
113126

114-
response=$(teamwork::request POST "/tasks/$TEAMWORK_TASK_ID/comments.json" "$payload")
127+
if ! response=$(teamwork::request POST "/tasks/$TEAMWORK_TASK_ID/comments.json" "$payload"); then
128+
teamwork::request_failed "Failed to add a comment to task $TEAMWORK_TASK_ID: $response"
129+
return
130+
fi
115131

116132
log::message "$response"
117133
}
@@ -125,8 +141,11 @@ teamwork::add_tag() {
125141
fi
126142

127143
if [ "$AUTOMATIC_TAGGING" == true ]; then
128-
response=$(teamwork::request PUT "/tasks/$TEAMWORK_TASK_ID/tags.json" \
129-
"{ \"tags\": { \"content\": \"${tag_name//\"/}\" } }")
144+
if ! response=$(teamwork::request PUT "/tasks/$TEAMWORK_TASK_ID/tags.json" \
145+
"{ \"tags\": { \"content\": \"${tag_name//\"/}\" } }"); then
146+
teamwork::request_failed "Failed to add the '$tag_name' tag to task $TEAMWORK_TASK_ID: $response"
147+
return
148+
fi
130149

131150
log::message "$response"
132151
fi
@@ -141,8 +160,11 @@ teamwork::remove_tag() {
141160
fi
142161

143162
if [ "$AUTOMATIC_TAGGING" == true ]; then
144-
response=$(teamwork::request PUT "/tasks/$TEAMWORK_TASK_ID/tags.json" \
145-
"{ \"tags\": { \"content\": \"${tag_name//\"/}\" },\"removeProvidedTags\":\"true\" }")
163+
if ! response=$(teamwork::request PUT "/tasks/$TEAMWORK_TASK_ID/tags.json" \
164+
"{ \"tags\": { \"content\": \"${tag_name//\"/}\" },\"removeProvidedTags\":\"true\" }"); then
165+
teamwork::request_failed "Failed to remove the '$tag_name' tag from task $TEAMWORK_TASK_ID: $response"
166+
return
167+
fi
146168

147169
log::message "$response"
148170
fi

‎tests/cases/comment-fails/expected‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
exit code: 0
1+
exit code: 1
2+
3+
stderr:
4+
curl: (22) The requested URL returned error: 500
5+
Failed to add a comment to task 1003: {"MESSAGE": "Internal server error"}
6+
1 Teamwork request(s) failed, see the errors above
27

38
GET /projects/api/v1/tasks/1003.json -> 200
49

‎tests/cases/task-links/expected‎

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
exit code: 0
1+
exit code: 1
2+
3+
stderr:
4+
curl: (22) The requested URL returned error: 404
5+
Failed to get the project of task 5, check the task exists and the API token's user can access it
6+
curl: (22) The requested URL returned error: 404
7+
Failed to get the project of task 2024, check the task exists and the API token's user can access it
8+
curl: (22) The requested URL returned error: 500
9+
Failed to add a comment to task 1003: {"MESSAGE": "Internal server error"}
10+
3 Teamwork request(s) failed, see the errors above
211

312
GET /projects/api/v1/tasks/1001.json -> 200
413

@@ -20,22 +29,8 @@ POST /tasks/1002/comments.json -> 201
2029

2130
GET /projects/api/v1/tasks/5.json -> 404
2231

23-
POST /tasks/5/comments.json -> 404
24-
comment:
25-
body: "PR [**\"Add OAuth login\"**](https://github.com/acme/webapp/pull/42) opened by **octocat**"
26-
content-type: "text"
27-
isprivate: false
28-
notify: true
29-
3032
GET /projects/api/v1/tasks/2024.json -> 404
3133

32-
POST /tasks/2024/comments.json -> 404
33-
comment:
34-
body: "PR [**\"Add OAuth login\"**](https://github.com/acme/webapp/pull/42) opened by **octocat**"
35-
content-type: "text"
36-
isprivate: false
37-
notify: true
38-
3934
GET /projects/api/v1/tasks/1003.json -> 200
4035

4136
POST /tasks/1003/comments.json -> 500

‎tests/cases/task-not-found/expected‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
exit code: 0
1+
exit code: 1
22

3-
GET /projects/api/v1/tasks/9999.json -> 404
3+
stderr:
4+
curl: (22) The requested URL returned error: 404
5+
Failed to get the project of task 9999, check the task exists and the API token's user can access it
6+
1 Teamwork request(s) failed, see the errors above
47

5-
POST /tasks/9999/comments.json -> 404
6-
comment:
7-
body: "PR [**\"Add OAuth login\"**](https://github.com/acme/webapp/pull/42) opened by **octocat**"
8-
content-type: "text"
9-
isprivate: false
10-
notify: true
8+
GET /projects/api/v1/tasks/9999.json -> 404
119

1210
GET /projects/api/v1/tasks/1001.json -> 200
1311

‎tests/cases/wrong-token/expected‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
exit code: 0
1+
exit code: 1
22

3-
GET /projects/api/v1/tasks/1001.json -> 401
3+
stderr:
4+
curl: (22) The requested URL returned error: 401
5+
Failed to get the project of task 1001, check the task exists and the API token's user can access it
6+
1 Teamwork request(s) failed, see the errors above
47

5-
POST /tasks/1001/comments.json -> 401
6-
comment:
7-
body: "PR [**\"Add OAuth login\"**](https://github.com/acme/webapp/pull/42) opened by **octocat**"
8-
content-type: "text"
9-
isprivate: false
10-
notify: true
8+
GET /projects/api/v1/tasks/1001.json -> 401

0 commit comments

Comments
 (0)