| #!/bin/sh | |
| # ---------------------------------------------------------------------------- | |
| # Licensed to the Apache Software Foundation (ASF) under one | |
| # or more contributor license agreements. See the NOTICE file | |
| # distributed with this work for additional information | |
| # regarding copyright ownership. The ASF licenses this file | |
| # to you under the Apache License, Version 2.0 (the | |
| # "License"); you may not use this file except in compliance | |
| # with the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, | |
| # software distributed under the License is distributed on an | |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| # KIND, either express or implied. See the License for the | |
| # specific language governing permissions and limitations | |
| # under the License. | |
| # Minimal issue manager script to handle a subset of prow-style commands | |
| # Reads the GitHub issue_comment event JSON pointed to by GITHUB_EVENT_PATH | |
| # Requires GITHUB_TOKEN env and GITHUB_REPOSITORY env (owner/repo) | |
| # PR /assign, /close, and /lgtm commands are handled by dedicated workflows. | |
| # | |
| # Supported Commands: | |
| # ------------------- | |
| # Command | Function | Example | Status | |
| # --------------- | --------------------------- | ---------------------------- | ------ | |
| # /assign | Assign issue to users | /assign or /assign @user | Normal | |
| # /unassign | Unassign users | /unassign or /unassign @user | Normal | |
| # /lgtm | Add lgtm label | /lgtm | Normal | |
| # /approve | Add approved label | /approve | Normal | |
| # /area | Add area label | /area plugin | Normal | |
| # /priority | Add priority label | /priority high | Normal | |
| # /remove | Remove label | /remove bug | Normal | |
| # /hold | Add hold label | /hold | Normal | |
| # /unhold | Remove hold label | /unhold | Normal | |
| # /close | Close issue | /close | Normal | |
| # /reopen | Reopen issue | /reopen | Normal | |
| # /lock | Lock issue | /lock | Normal | |
| # /unlock | Unlock issue | /unlock | Normal | |
| # /milestone | Set milestone | /milestone 5 | Normal | |
| # /cc | Mention users (comment) | /cc @user1 @user2 | Normal | |
| # /uncc | Unmention users (comment) | /uncc @user1 | Normal | |
| set -euo pipefail | |
| GITHUB_API=${GITHUB_API:-https://api.github.com} | |
| if [ -z "${GITHUB_TOKEN:-}" ]; then | |
| echo "GITHUB_TOKEN is required" >&2 | |
| exit 1 | |
| fi | |
| if [ -z "${GITHUB_EVENT_PATH:-}" ] || [ ! -f "${GITHUB_EVENT_PATH}" ]; then | |
| echo "GITHUB_EVENT_PATH not set or file missing: ${GITHUB_EVENT_PATH}" >&2 | |
| exit 1 | |
| fi | |
| if [ -z "${GITHUB_REPOSITORY:-}" ]; then | |
| echo "GITHUB_REPOSITORY is required (owner/repo)" >&2 | |
| exit 1 | |
| fi | |
| read_event() { | |
| jq -r "$1" "${GITHUB_EVENT_PATH}" | |
| } | |
| COMMENT_BODY=$(read_event '.comment.body') | |
| ISSUE_NUMBER=$(read_event '.issue.number') | |
| COMMENT_USER=$(read_event '.comment.user.login') | |
| IS_PULL_REQUEST=$(read_event '.issue.pull_request != null') | |
| OWNER=$(echo "${GITHUB_REPOSITORY}" | cut -d/ -f1) | |
| REPO=$(echo "${GITHUB_REPOSITORY}" | cut -d/ -f2) | |
| auth_header() { | |
| echo "Authorization: token ${GITHUB_TOKEN}" | |
| } | |
| api() { | |
| local method=$1 path=$2 data=${3:-} | |
| if [ -n "${data}" ]; then | |
| curl -sS -X "${method}" -H "$(auth_header)" -H "Accept: application/vnd.github+json" \ | |
| -H "Content-Type: application/json" --data "${data}" "${GITHUB_API}${path}" | |
| else | |
| curl -sS -X "${method}" -H "$(auth_header)" -H "Accept: application/vnd.github+json" "${GITHUB_API}${path}" | |
| fi | |
| } | |
| users_json_from_bash_array() { | |
| # read users from args and output compact JSON array like ["user1","user2"] | |
| if [ $# -eq 0 ]; then | |
| echo '[]' | |
| return | |
| fi | |
| printf "%s\n" "$@" | sed 's/^@//' | jq -R -s -c 'split("\n") | map(select(length>0))' | |
| } | |
| add_label() { | |
| local label=$1 | |
| api POST "/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}/labels" "$(jq -nc --arg l "$label" '{labels:[$l]}')" >/dev/null || true | |
| } | |
| remove_label() { | |
| local label=$1 | |
| encoded=$(printf "%s" "$label" | jq -s -R -r @uri) | |
| api DELETE "/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}/labels/${encoded}" >/dev/null || true | |
| } | |
| assign() { | |
| local users_json | |
| users_json=$(users_json_from_bash_array "$@") | |
| api POST "/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}/assignees" "$(jq -nc --argjson a "$users_json" '{assignees:$a}')" >/dev/null || true | |
| } | |
| unassign() { | |
| local users_json | |
| users_json=$(users_json_from_bash_array "$@") | |
| api DELETE "/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}/assignees" "$(jq -nc --argjson a "$users_json" '{assignees:$a}')" >/dev/null || true | |
| } | |
| close_issue() { | |
| api PATCH "/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}" '{"state":"closed"}' >/dev/null || true | |
| } | |
| reopen_issue() { | |
| api PATCH "/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}" '{"state":"open"}' >/dev/null || true | |
| } | |
| lock_issue() { | |
| api PUT "/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}/lock" '{"lock_reason":"resolved"}' >/dev/null || true | |
| } | |
| unlock_issue() { | |
| api DELETE "/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}/lock" >/dev/null || true | |
| } | |
| set_milestone() { | |
| local ms=$1 | |
| if [ -z "$ms" ]; then | |
| echo "No milestone number provided to /milestone" >&2 | |
| return | |
| fi | |
| api PATCH "/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}" "$(jq -nc --arg m "$ms" '{milestone:($m|tonumber)}')" >/dev/null || true | |
| } | |
| comment() { | |
| local body=$1 | |
| api POST "/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}/comments" "$(jq -nc --arg b "$body" '{body:$b}')" >/dev/null || true | |
| } | |
| normalize_user() { | |
| local u=$1 | |
| echo "${u#@}" | |
| } | |
| process_command() { | |
| local line="$1" | |
| line="$(echo "$line" | sed -e 's/^\s*//' -e 's/\s*$//')" | |
| # Do not replay PR commands owned by the dedicated created-comment workflows. | |
| if [ "$IS_PULL_REQUEST" = true ]; then | |
| case "$line" in | |
| \/assign*|\/close|\/lgtm) return ;; | |
| esac | |
| fi | |
| case "$line" in | |
| \/assign*) | |
| args="${line#\/assign}" | |
| args="$(echo "$args" | sed -e 's/^\s*//' -e 's/\s*$//')" | |
| if [ -z "$args" ]; then | |
| # If no users specified, assign to the commenter | |
| assign "$COMMENT_USER" | |
| else | |
| read -ra users <<<"$args" | |
| for i in "${!users[@]}"; do | |
| users[$i]=$(normalize_user "${users[$i]}") | |
| done | |
| assign "${users[@]}" | |
| fi | |
| ;; | |
| \/unassign*) | |
| args="${line#\/unassign}" | |
| args="$(echo "$args" | sed -e 's/^\s*//' -e 's/\s*$//')" | |
| if [ -z "$args" ]; then | |
| # If no users specified, unassign the commenter | |
| unassign "$COMMENT_USER" | |
| else | |
| read -ra users <<<"$args" | |
| for i in "${!users[@]}"; do | |
| users[$i]=$(normalize_user "${users[$i]}") | |
| done | |
| unassign "${users[@]}" | |
| fi | |
| ;; | |
| \/lgtm) | |
| add_label "lgtm" | |
| ;; | |
| \/approve) | |
| add_label "approved" | |
| ;; | |
| \/area*) | |
| lbl="${line#\/area}" | |
| lbl="$(echo "$lbl" | sed -e 's/^\s*//' -e 's/\s*$//')" | |
| if [ -n "$lbl" ]; then | |
| add_label "area/$lbl" | |
| else | |
| echo "Error: /area requires an area name (e.g., /area plugin)" >&2 | |
| fi | |
| ;; | |
| \/priority*) | |
| lbl="${line#\/priority}" | |
| lbl="$(echo "$lbl" | sed -e 's/^\s*//' -e 's/\s*$//')" | |
| if [ -n "$lbl" ]; then | |
| add_label "priority/$lbl" | |
| else | |
| echo "Error: /priority requires a priority level (e.g., /priority high)" >&2 | |
| fi | |
| ;; | |
| \/remove*) | |
| lbl="${line#\/remove}" | |
| lbl="$(echo "$lbl" | sed -e 's/^\s*//' -e 's/\s*$//')" | |
| if [ -n "$lbl" ]; then | |
| remove_label "$lbl" | |
| else | |
| echo "Error: /remove requires a label name (e.g., /remove bug)" >&2 | |
| fi | |
| ;; | |
| \/close) | |
| close_issue | |
| ;; | |
| \/reopen) | |
| reopen_issue | |
| ;; | |
| \/lock) | |
| lock_issue | |
| ;; | |
| \/unlock) | |
| unlock_issue | |
| ;; | |
| \/milestone*) | |
| ms="${line#\/milestone}" | |
| ms="$(echo "$ms" | sed -e 's/^\s*//' -e 's/\s*$//')" | |
| if [ -n "$ms" ] && [[ "$ms" =~ ^[0-9]+$ ]]; then | |
| set_milestone "$ms" | |
| else | |
| echo "Error: /milestone requires a valid milestone number (e.g., /milestone 5)" >&2 | |
| fi | |
| ;; | |
| \/hold) | |
| add_label "hold" | |
| ;; | |
| \/unhold) | |
| remove_label "hold" | |
| ;; | |
| \/cc*) | |
| users_str="${line#\/cc}" | |
| users_str="$(echo "$users_str" | sed -e 's/^\s*//' -e 's/\s*$//')" | |
| if [ -n "$users_str" ]; then | |
| comment "cc: $users_str" | |
| else | |
| echo "Warning: /cc requires usernames (e.g., /cc @user1 @user2)" >&2 | |
| fi | |
| ;; | |
| \/uncc*) | |
| users_str="${line#\/uncc}" | |
| users_str="$(echo "$users_str" | sed -e 's/^\s*//' -e 's/\s*$//')" | |
| if [ -n "$users_str" ]; then | |
| comment "uncc: $users_str" | |
| else | |
| echo "Warning: /uncc requires usernames (e.g., /uncc @user1 @user2)" >&2 | |
| fi | |
| ;; | |
| *) | |
| echo "No recognized command: $line" >&2 | |
| ;; | |
| esac | |
| } | |
| main() { | |
| while IFS= read -r line; do | |
| if [[ "$line" =~ ^/ ]]; then | |
| echo "Processing command: $line" | |
| process_command "$line" | |
| fi | |
| done <<<"$COMMENT_BODY" | |
| } | |
| main |