blob: 401d635a80ce78d121d7c25b6df1c7d1d1812f7a [file]
#!/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)
#
# 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')
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*$//')"
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