From 731e6810b4c0bd6ec71ce6ac2da26a68cb704670 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Tue, 30 Apr 2024 20:01:55 +1000 Subject: [PATCH] Move GitHub custom actions from apple-infra (#6) --- actions/Package.swift | 11 ++ actions/asana-create-pr-subtask/action.yml | 45 ++++++ .../asana-create-pr-subtask.sh | 149 ++++++++++++++++++ .../action.yml | 27 ++++ .../github-asana-user-id-mapping.yml | 30 ++++ 5 files changed, 262 insertions(+) create mode 100644 actions/Package.swift create mode 100644 actions/asana-create-pr-subtask/action.yml create mode 100755 actions/asana-create-pr-subtask/asana-create-pr-subtask.sh create mode 100644 actions/asana-get-user-id-for-github-handle/action.yml create mode 100644 actions/asana-get-user-id-for-github-handle/github-asana-user-id-mapping.yml diff --git a/actions/Package.swift b/actions/Package.swift new file mode 100644 index 0000000..a5074ea --- /dev/null +++ b/actions/Package.swift @@ -0,0 +1,11 @@ +// swift-tools-version:5.7 + +// Leave blank. This is only here so that Xcode doesn't display it. + +import PackageDescription + +let package = Package( + name: "actions", + products: [], + targets: [] +) diff --git a/actions/asana-create-pr-subtask/action.yml b/actions/asana-create-pr-subtask/action.yml new file mode 100644 index 0000000..b3c10a1 --- /dev/null +++ b/actions/asana-create-pr-subtask/action.yml @@ -0,0 +1,45 @@ +name: Create Asana PR subtask +description: | + Creates a PR subtask and assigns it to the team member in charge of the GitHub review. + If the PR subtask already exists, it ensures the reviewer is assigned and the task is not marked 'completed'. +inputs: + access-token: + description: "Asana access token" + required: true + type: string + asana-task-id: + description: "Asana Task ID for the PR" + required: string + type: string + github-reviewer-user: + description: "GitHub username for the requested reviewer" + required: true + type: string +runs: + using: "composite" + steps: + - name: Get Asana user ID + id: get-asana-user-id + uses: duckduckgo/apple-infra/actions/asana-get-user-id-for-github-handle@main + with: + access-token: ${{ inputs.access-token }} + github-handle: ${{ inputs.github-reviewer-user }} + + - name: Get PR URL + id: get-pr-url + shell: bash + run: | + echo "url=https://github.com/${{ github.repository }}/pull/${{ github.event.number }}" >> "$GITHUB_OUTPUT" + + - name: "Create PR Subtask" + shell: bash + env: + ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} + run: | + # If the Asana User ID is not found, exit with an error + if [[ -z "${{ steps.get-asana-user-id.outputs.user-id }}" ]]; then + echo "Error: Failed to get Asana user ID for GitHub user ${{ inputs.github-reviewer-user }}" + exit 1 + else + ${{ github.action_path }}/asana-create-pr-subtask.sh ${{ inputs.asana-task-id }} ${{ steps.get-asana-user-id.outputs.user-id }} ${{ steps.get-pr-url.outputs.url }} + fi diff --git a/actions/asana-create-pr-subtask/asana-create-pr-subtask.sh b/actions/asana-create-pr-subtask/asana-create-pr-subtask.sh new file mode 100755 index 0000000..3bc25fd --- /dev/null +++ b/actions/asana-create-pr-subtask/asana-create-pr-subtask.sh @@ -0,0 +1,149 @@ +#!/bin/bash +# +# This script creates a PR subtask and assign it to a team member when a review is requested on GitHub +# + +set -e -o pipefail + +asana_api_url="https://app.asana.com/api/1.0" +pr_prefix="PR:" + +# Fetch the subtasks of a task with the given Asana task ID. +_fetch_subtasks() { + local asana_task_id="$1" + local url="${asana_api_url}/tasks/${asana_task_id}/subtasks?opt_fields=name,completed,parent.name,assignee" + + local response + response="$(curl -fLSs "$url" -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}")" + + # extract the task id, task name, task completed status, assignee id and parent name + jq -c '[ + .data[] | { + task_id: .gid, + task_name: .name, + task_completed: .completed, + assignee: .assignee.gid, + parent_name: .parent.name + } + ]' <<< "$response" +} + +# Sets the parent task name +_set_parent_task_name() { + local subtasks="$1" + local asana_task_id="$2" + + # extracts the parent name from the first object + parent_task_name=$(echo "$subtasks" | jq -r '.[0].parent_name') + + # if parent_task_name is nil it means that there are no subtask in the current task so we fetch the parent name + if [ -z "$parent_task_name" ] || [ "$parent_task_name" = "null" ]; then + local url="${asana_api_url}/tasks/${asana_task_id}" + parent_task_name="$(curl -fLSs "$url" -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" | jq -r '.data.name')" + fi +} + +# Checks if a subtask for the PR already exists. +_check_pr_subtask_exist() { + local response="$1" + local asana_assignee_id="$2" + + # read each line of the array + # extract the task name and trim leading and trailing white spaces + # extract the parent name and trim leading and trailing white spaces + # extract the assignee name + # checks if the task name has 'PR:' prefix and if contains the parent name and if it's assigned to the reviewer + echo "$response" | jq -c '.[]' | while read -r item; do + task_name=$(jq -r '.task_name' <<< "$item" | awk '{$1=$1};1') + parent_name=$(jq -r '.parent_name' <<< "$item" | awk '{$1=$1};1') + assignee=$(jq -r '.assignee' <<< "$item") + + if [[ "$task_name" == "${pr_prefix}"* && "$task_name" == *"$parent_name"* && "$assignee" == "$asana_assignee_id" ]]; then + echo "$item" + fi + done + +} + +# Creates a subtask called PR: ${task_title}, set the PR URL as description and assign to the requested reviewer +_create_pr_subtask() { + local asana_task_id="$1" + local asana_assignee_id="$2" + local github_pr_url="$3" + local url="${asana_api_url}/tasks/${asana_task_id}/subtasks?opt_fields=gid" + + local payload + payload=$(cat <<-EOF + { + "data": { + "assignee": "${asana_assignee_id}", + "notes": "${pr_prefix} ${github_pr_url}", + "name": "${pr_prefix} ${parent_task_name}" + } + } + EOF + ) + + _execute_create_or_update_asana_task_request POST "$url" "$payload" +} + +# Assigns a reviewer to the existing PR subtask and update the task status if it is marked 'completed' +_mark_task_uncompleted_if_needed() { + local pr_subtask="$1" + + # get the task id + local task_id + task_id=$(echo "$pr_subtask" | jq -r '.task_id') + # get the completed status + local task_status_completed + task_status_completed=$(echo "$pr_subtask" | jq -r '.task_completed') + + # if the status is completed mark the task uncompleted. + if [ "$task_status_completed" = true ]; then + local url="${asana_api_url}/tasks/${task_id}?opt_fields=gid" + local payload='{"data":{"completed":false}}' + _execute_create_or_update_asana_task_request PUT "$url" "$payload" + fi +} + +# Executes an Asana request to create or update a Subtask +_execute_create_or_update_asana_task_request() { + local method="$1" + local url="$2" + local payload="$3" + + local task_id + task_id="$(curl -fLSs -X "$method" "$url" \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + -H 'accept: application/json' \ + -H 'content-type: application/json' \ + --data "${payload}" \ + | jq -r .data.gid)" +} + +main() { + local asana_task_id="$1" + local asana_assignee_id="$2" + local github_pr_url="$3" + + # fetch the task subtasks + local subtasks + subtasks=$(_fetch_subtasks "$asana_task_id") + + # set the parent task name + _set_parent_task_name "$subtasks" "$asana_task_id" + + # check if the PR subtask already exist + local pr_subtask + pr_subtask=$(_check_pr_subtask_exist "$subtasks" "$asana_assignee_id") + + # if the PR subtask exist, mark the task uncompleted if it the task is marked completed + # otherwise, create the PR subtask and assign it to the reviewer + if [[ -n "$pr_subtask" ]]; then + _mark_task_uncompleted_if_needed "$pr_subtask" + else + _create_pr_subtask "$asana_task_id" "$asana_assignee_id" "$github_pr_url" + fi +} + +main "$@" diff --git a/actions/asana-get-user-id-for-github-handle/action.yml b/actions/asana-get-user-id-for-github-handle/action.yml new file mode 100644 index 0000000..1acf4ce --- /dev/null +++ b/actions/asana-get-user-id-for-github-handle/action.yml @@ -0,0 +1,27 @@ +name: Get Asana user ID matching GitHub handle +description: Returns Asana user ID that matches GitHub user handle +inputs: + access-token: + description: "Asana access token" + required: true + type: string + github-handle: + description: "GitHub user handle" + required: true + type: string +outputs: + user-id: + description: "Asana user ID" + value: ${{ steps.get-asana-user-id.outputs.user-id }} +runs: + using: "composite" + steps: + - id: get-asana-user-id + run: | + user_id="$(yq ."${{ inputs.github-handle }}" ${{ github.action_path }}/github-asana-user-id-mapping.yml)" + if [[ -z "$user_id" || "$user_id" == "null" ]]; then + echo "::warning::Asana User ID not found for GitHub handle: ${{ inputs.github-handle }}" + else + echo "user-id=${user_id}" >> $GITHUB_OUTPUT + fi + shell: bash diff --git a/actions/asana-get-user-id-for-github-handle/github-asana-user-id-mapping.yml b/actions/asana-get-user-id-for-github-handle/github-asana-user-id-mapping.yml new file mode 100644 index 0000000..85435bf --- /dev/null +++ b/actions/asana-get-user-id-for-github-handle/github-asana-user-id-mapping.yml @@ -0,0 +1,30 @@ +aataraxiaa: "1206488453854239" +afterxleep: "1204051006248664" +alessandroboron: "1206329551987270" +amddg44: "1201462886797771" +ayoy: "1201621708091911" +brindy: "33604954490307" +Bunn: "1201011656757987" +bwaresiak: "856498666990313" +dharb: "246491496396026" +diegoreymendez: "1203108348749442" +dus7: "1206226850447383" +federicocappelli: "1205736672764360" +GioSensation: "1144596632862977" +graeme: "1202926619863343" +jaceklyp: "1201392122268514" +jonathanKingston: "1199237043579003" +jotaemepereira: "1203972458584419" +ladamski: "1171671023737946" +loremattei: "1202204871961729" +mallexxx: "1202406491309495" +miasma13: "1200848783415037" +muodov: "1201807839780469" +quanganhdo: "1205591970852428" +SabrinaTardio: "1204024359086948" +samsymons: "1193060753332998" +shakyShane: "1201141132903155" +THISISDINOSAUR: "1187352150204278" +tomasstrba: "1148564398679340" +viktorjansson: "970019367293722" +vinay-nadig-0042: "1202096681806170"