This commit is contained in:
2025-11-25 10:51:51 -05:00
parent 2521a5a5be
commit 305dd49dde
3 changed files with 389 additions and 143 deletions
+44 -14
View File
@@ -1,13 +1,16 @@
name: "Wait For Manual Approval (Gitea/GitHub + Apprise)" name: "Wait For Manual Approval (Gitea/GitHub + Apprise)"
description: "Pauses workflow until designated approvers comment with approval or denial keywords." description: >
Pauses workflow until designated approvers comment (or react) with approval or denial keywords,
optionally posts an initial comment, auto-closes the issue, and provides outputs:
approval_status, issue_number, issue_url.
inputs: inputs:
token: token:
description: "API token for GitHub or Gitea" description: "API token for GitHub or Gitea (GITHUB_TOKEN or a personal token)"
required: true required: true
api_url: api_url:
description: "Root API URL (e.g., https://git.example.com/api/v1)" description: "Root API URL (e.g., https://git.example.com/api/v1 or https://api.github.com)"
required: true required: true
repo_owner: repo_owner:
@@ -19,7 +22,7 @@ inputs:
required: true required: true
approvers: approvers:
description: "Comma-separated list of approver usernames" description: "Comma-separated list of approver usernames (e.g. ops-team,john.doe)"
required: true required: true
approval_keywords: approval_keywords:
@@ -30,8 +33,12 @@ inputs:
description: "Comma-separated keywords meaning DENIED" description: "Comma-separated keywords meaning DENIED"
default: "deny,denied,no" default: "deny,denied,no"
enable_reactions:
description: "Enable reaction-based approvals (:+1: / -1). Default true"
default: "true"
poll_interval: poll_interval:
description: "Seconds between each comment check" description: "Seconds between each comment/reaction check"
default: "10" default: "10"
reminder_interval: reminder_interval:
@@ -39,28 +46,47 @@ inputs:
default: "0" default: "0"
apprise_api_url: apprise_api_url:
description: "URL to Apprise API (optional)" description: "URL to Apprise API (optional; e.g. https://apprise.example.com)"
required: false required: false
default: ""
initial_comment: initial_comment:
description: "Optional comment to post immediately on the newly created issue" description: "Optional comment to post immediately on the newly created/reused issue (supports markdown)"
required: false
default: ""
auto_close:
description: "Auto-close the issue once a final decision has been reached (true/false)"
default: "true"
issue_title:
description: "Optional issue title. If empty the action generates a default title."
required: false required: false
default: "" default: ""
outputs: outputs:
approval_status: approval_status:
description: "Final approval status: approved or denied" description: "Final approval status: approved or denied"
value: ${{ steps.wait-for-manual-approval.outputs.approval_status }} value: ${{ steps.wait.outputs.approval_status }}
issue_number:
description: "The issue number that was used"
value: ${{ steps.wait.outputs.issue_number }}
issue_url:
description: "A URL to the approval issue"
value: ${{ steps.wait.outputs.issue_url }}
runs: runs:
using: "composite" using: "composite"
steps: steps:
- name: Make scripts executable - name: Make helper scripts executable
shell: bash shell: bash
run: chmod +x "$GITHUB_ACTION_PATH/wait-for-approval.sh" "$GITHUB_ACTION_PATH/notify" run: |
chmod +x "$GITHUB_ACTION_PATH/wait-for-approval.sh" "$GITHUB_ACTION_PATH/notify"
- name: Run wait-for-approval - name: Wait for manual approval
id: wait-for-manual-approval id: wait
shell: bash shell: bash
env: env:
TOKEN: ${{ inputs.token }} TOKEN: ${{ inputs.token }}
@@ -70,10 +96,14 @@ runs:
APPROVERS: ${{ inputs.approvers }} APPROVERS: ${{ inputs.approvers }}
APPROVAL_KEYWORDS: ${{ inputs.approval_keywords }} APPROVAL_KEYWORDS: ${{ inputs.approval_keywords }}
DENIAL_KEYWORDS: ${{ inputs.denial_keywords }} DENIAL_KEYWORDS: ${{ inputs.denial_keywords }}
ENABLE_REACTIONS: ${{ inputs.enable_reactions }}
POLL_INTERVAL: ${{ inputs.poll_interval }} POLL_INTERVAL: ${{ inputs.poll_interval }}
REMINDER_INTERVAL: ${{ inputs.reminder_interval }} REMINDER_INTERVAL: ${{ inputs.reminder_interval }}
APPRISE_API_URL: ${{ inputs.apprise_api_url }} APPRISE_API_URL: ${{ inputs.apprise_api_url }}
INITIAL_COMMENT: ${{ inputs.initial_comment }} INITIAL_COMMENT: ${{ inputs.initial_comment }}
AUTO_CLOSE: ${{ inputs.auto_close }}
ISSUE_TITLE: ${{ inputs.issue_title }}
run: | run: |
status=$(bash "$GITHUB_ACTION_PATH/wait-for-approval.sh") # The script will write outputs directly to $GITHUB_OUTPUT:
echo "approval_status=$status" >> "$GITHUB_OUTPUT" # approval_status, issue_number, issue_url
bash "$GITHUB_ACTION_PATH/wait-for-approval.sh"
+7 -8
View File
@@ -2,17 +2,16 @@
set -euo pipefail set -euo pipefail
msg="$1" msg="$1"
APPRISE_API_URL="${APPRISE_API_URL:-}"
# Apprise CLI # Local Apprise CLI if available
if command -v apprise >/dev/null 2>&1; then if command -v apprise >/dev/null 2>&1; then
apprise -b "$msg" >/dev/null 2>&1 || true apprise -b "$msg" >/dev/null 2>&1 || true
fi fi
# Apprise API # Apprise API
if [[ -n "${APPRISE_API_URL:-}" ]]; then if [[ -n "$APPRISE_API_URL" ]]; then
curl -s -X POST \ curl -fsS -X POST -H "Content-Type: application/json" \
-H "Content-Type: application/json" \ -d "$(jq -n --arg b "$msg" '{body:$b}')" \
-F "{\"body\":\"$msg\"}" \ "$APPRISE_API_URL/notify" >/dev/null 2>&1 || true
-F "tags=all" \
"$APPRISE_API_URL" >/dev/null 2>&1 || true
fi fi
+338 -121
View File
@@ -1,6 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# -----------------------
# Helpers: logging
# -----------------------
log() { printf "[%s] [%s] %s\n" "$(date -Iseconds)" "$1" "$2"; }
# -----------------------
# Read env / inputs
# -----------------------
TOKEN="${TOKEN:?Missing TOKEN}" TOKEN="${TOKEN:?Missing TOKEN}"
API_URL="${API_URL:?Missing API_URL}" API_URL="${API_URL:?Missing API_URL}"
REPO_OWNER="${REPO_OWNER:?Missing REPO_OWNER}" REPO_OWNER="${REPO_OWNER:?Missing REPO_OWNER}"
@@ -8,152 +16,361 @@ REPO_NAME="${REPO_NAME:?Missing REPO_NAME}"
APPROVERS="${APPROVERS:?Missing APPROVERS}" APPROVERS="${APPROVERS:?Missing APPROVERS}"
APPROVAL_KEYWORDS="${APPROVAL_KEYWORDS:?Missing APPROVAL_KEYWORDS}" APPROVAL_KEYWORDS="${APPROVAL_KEYWORDS:?Missing APPROVAL_KEYWORDS}"
DENIAL_KEYWORDS="${DENIAL_KEYWORDS:?Missing DENIAL_KEYWORDS}" DENIAL_KEYWORDS="${DENIAL_KEYWORDS:?Missing DENIAL_KEYWORDS}"
ENABLE_REACTIONS="${ENABLE_REACTIONS:-true}"
POLL_INTERVAL="${POLL_INTERVAL:-10}" POLL_INTERVAL="${POLL_INTERVAL:-10}"
REMINDER_INTERVAL="${REMINDER_INTERVAL:-0}" REMINDER_INTERVAL="${REMINDER_INTERVAL:-0}"
APPRISE_API_URL="${APPRISE_API_URL:-}"
INITIAL_COMMENT="${INITIAL_COMMENT:-}" INITIAL_COMMENT="${INITIAL_COMMENT:-}"
AUTO_CLOSE="${AUTO_CLOSE:-true}"
ISSUE_TITLE_INPUT="${ISSUE_TITLE:-}"
IFS=',' read -r -a approver_list <<< "$APPROVERS" REPO="${REPO_OWNER}/${REPO_NAME}"
IFS=',' read -r -a approved_kws <<< "$APPROVAL_KEYWORDS"
IFS=',' read -r -a denied_kws <<< "$DENIAL_KEYWORDS"
last_reminder=$(date +%s) log INFO "Starting wait-for-approval"
log INFO "Repo: $REPO"
log INFO "API root: $API_URL"
log INFO "Approvers: $APPROVERS"
log INFO "Enable reactions: $ENABLE_REACTIONS"
log INFO "Poll interval: ${POLL_INTERVAL}s, reminder interval: ${REMINDER_INTERVAL}s"
log INFO "Auto-close: $AUTO_CLOSE"
# -----------------------
# Utilities
# -----------------------
escape_regex() { escape_regex() {
sed -e 's/[]\/$*.^|[]/\\&/g' <<< "$1" sed -e 's/[]\/$*.^|[]/\\&/g' <<< "$1"
} }
notify_func() { # Convert CSV string to bash array (trim whitespace)
local msg="$1" csv_to_array() {
echo "::notice::$msg" local csv="$1"
local -n out=$2
if [[ -x "$GITHUB_ACTION_PATH/notify" ]]; then IFS=',' read -r -a tmp <<< "$csv"
bash "$GITHUB_ACTION_PATH/notify" "$msg" || true out=()
fi for v in "${tmp[@]}"; do
# trim spaces
if command -v apprise >/dev/null 2>&1; then vv="$(echo "$v" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
apprise -b "$msg" >/dev/null 2>&1 || true out+=("$vv")
fi done
if [[ -n "${APPRISE_API_URL:-}" ]]; then
curl -s -X POST \
-H "Content-Type: application/json" \
-F "{\"body\":\"$msg\"}" \
-F "tags=all" \
"$APPRISE_API_URL" >/dev/null 2>&1 || true
fi
} }
# ----------------------------- # Helper to write to GITHUB_OUTPUT safely (composite actions set this env)
# Step 1: Create the issue write_output() {
# ----------------------------- # $1 = key, $2 = value
title="Manual approval required" if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
body="Workflow requires manual approval. echo "$1=$2" >> "$GITHUB_OUTPUT"
else
# fallback to printing for debugging
echo "::notice::OUTPUT $1=$2"
fi
}
# Apprise notify helper
apprise_notify() {
local msg="$1"
log NOTICE "Notify: $msg"
# call local notify helper if exists
if [[ -x "$GITHUB_ACTION_PATH/notify" ]]; then
bash "$GITHUB_ACTION_PATH/notify" "$msg" || log WARN "local notify script failed"
fi
# apprise CLI if available
if command -v apprise >/dev/null 2>&1; then
apprise -b "$msg" >/dev/null 2>&1 || log WARN "apprise CLI failed"
fi
# apprise-api if configured
if [[ -n "$APPRISE_API_URL" ]]; then
curl -fsS -X POST \
-H "Content-Type: application/json" \
-d "$(jq -n --arg b "$msg" '{body:$b}')" \
"$APPRISE_API_URL/notify" >/dev/null 2>&1 || log WARN "apprise API request failed"
fi
}
# -----------------------
# Prepare arrays
# -----------------------
csv_to_array "$APPROVERS" approver_list
csv_to_array "$APPROVAL_KEYWORDS" approved_kws
csv_to_array "$DENIAL_KEYWORDS" denied_kws
# Lowercase keywords for matching
for i in "${!approved_kws[@]}"; do approved_kws[$i]="$(tr '[:upper:]' '[:lower:]' <<<"${approved_kws[$i]}")"; done
for i in "${!denied_kws[@]}"; do denied_kws[$i]="$(tr '[:upper:]' '[:lower:]' <<<"${denied_kws[$i]}")"; done
# Build issue title if not provided
if [[ -z "$ISSUE_TITLE_INPUT" ]]; then
ISSUE_TITLE="Manual approval required: ${GITHUB_REPOSITORY:-$REPO} / run triggered"
else
ISSUE_TITLE="$ISSUE_TITLE_INPUT"
fi
log INFO "Issue title: $ISSUE_TITLE"
# -----------------------
# 1) Find an existing open issue that looks like our approval issue
# We search open issues for an exact title match. If present, reuse.
# -----------------------
log INFO "Searching for existing open issues with the same title..."
existing_api="${API_URL}/repos/${REPO}/issues?state=open&per_page=100"
resp=$(curl -fsS -H "Authorization: token $TOKEN" "$existing_api") || {
log WARN "Failed to fetch existing issues (curl failed)"; resp="[]";
}
log DEBUG "Existing issues response: $(echo "$resp" | jq -c '.')"
issue_number=""
# select exact title match (works for GitHub & Gitea)
issue_number=$(echo "$resp" | jq -r --arg t "$ISSUE_TITLE" '.[] | select(.title == $t) | .number // .index // .id' | head -n 1 || true)
if [[ -n "$issue_number" && "$issue_number" != "null" ]]; then
log INFO "Reusing existing open approval issue #$issue_number"
else
# Create issue
log INFO "No existing issue found; creating a new approval issue"
body="This workflow requires manual approval
Approvers: ${APPROVERS} Approvers: ${APPROVERS}
Reply with: Reply with one of the approval keywords:
${APPROVAL_KEYWORDS} ${APPROVAL_KEYWORDS}
${DENIAL_KEYWORDS}"
assignees_json=$(jq -nc --arg csv "$APPROVERS" '$csv|split(",")') Or reply with one of the denial keywords:
${DENIAL_KEYWORDS}
"
# build assignees JSON
assignees_json=$(jq -nc --arg csv "$APPROVERS" '$csv | split(",")')
payload=$(jq -n --arg title "$ISSUE_TITLE" --arg body "$body" --argjson assignees "$assignees_json" \
'{title:$title, body:$body, assignees:$assignees}')
log DEBUG "Create issue payload: $payload"
json=$(jq -n \ create_resp=$(curl -fsS -X POST \
--arg title "$title" \ -H "Authorization: token $TOKEN" \
--arg body "$body" \ -H "Content-Type: application/json" \
--argjson assignees "$assignees_json" \ -d "$payload" \
'{title:$title, body:$body, assignees:$assignees}') "${API_URL}/repos/${REPO}/issues") || { log ERROR "Issue creation failed"; echo "issue_creation_failed"; exit 1; }
resp=$(curl -s -X POST \ log DEBUG "Create issue response: $(echo "$create_resp" | jq -c '.')"
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "$json" \
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues")
ISSUE=$(echo "$resp" | jq -r '.number // .index // .id') issue_number=$(echo "$create_resp" | jq -r '.number // .index // .id')
if [[ -z "$ISSUE" || "$ISSUE" == "null" ]]; then if [[ -z "$issue_number" || "$issue_number" == "null" ]]; then
echo "Failed to create issue: $resp" log ERROR "Failed to obtain issue number from create response: $create_resp"
echo "issue_creation_failed"
exit 1 exit 1
fi
log INFO "Created approval issue #$issue_number"
fi fi
notify_func "Approval required on issue #$ISSUE" issue_url="${API_URL%/}/repos/${REPO}/issues/${issue_number}"
write_output issue_number "$issue_number"
write_output issue_url "$issue_url"
# ----------------------------- # -----------------------
# Step 1b: Post initial comment if provided # 1b) Post initial comment if provided
# ----------------------------- # -----------------------
if [[ -n "$INITIAL_COMMENT" ]]; then if [[ -n "$INITIAL_COMMENT" ]]; then
comment_json=$(jq -n --arg body "$INITIAL_COMMENT" '{body:$body}') log INFO "Posting initial comment to issue #$issue_number"
curl -s -X POST \ comment_payload=$(jq -n --arg body "$INITIAL_COMMENT" '{body:$body}')
-H "Authorization: token $TOKEN" \ curl -fsS -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-H "Content-Type: application/json" \ -d "$comment_payload" "${API_URL}/repos/${REPO}/issues/${issue_number}/comments" >/dev/null 2>&1 || log WARN "Posting initial comment failed"
-d "$comment_json" \
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$ISSUE/comments" >/dev/null
fi fi
# ----------------------------- apprise_notify "Approval required on issue #${issue_number}${issue_url}"
# Step 2: Poll for comments
# -----------------------------
while true; do
comments=$(curl -s \
-H "Authorization: token $TOKEN" \
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$ISSUE/comments")
declare -A state # -----------------------
for u in "${approver_list[@]}"; do # 2) Polling loop (comments + reactions)
state["$u"]="pending" # -----------------------
done last_reminder=$(date +%s)
approval_status="denied" # default; will be set to 'approved' if approved
count=$(echo "$comments" | jq 'length') # Prebuild lowercased regex patterns for keywords
approved_patterns=()
for ((i=0; i<count; i++)); do denied_patterns=()
user=$(echo "$comments" | jq -r ".[$i].user.login") for k in "${approved_kws[@]}"; do
body=$(echo "$comments" | jq -r ".[$i].body" | tr '[:upper:]' '[:lower:]') k_lc="$(tr '[:upper:]' '[:lower:]' <<<"$k")"
approved_patterns+=("$(escape_regex "$k_lc")")
body_esc=$(escape_regex "$body") done
for k in "${denied_kws[@]}"; do
for k in "${approved_kws[@]}"; do k_lc="$(tr '[:upper:]' '[:lower:]' <<<"$k")"
k_esc=$(escape_regex "$k") denied_patterns+=("$(escape_regex "$k_lc")")
[[ "$body_esc" =~ ^${k_esc}[.!]?$ ]] && state["$user"]="approved" done
done
# reaction mapping: map common positive/negative reaction names
for k in "${denied_kws[@]}"; do # GitHub/Gitea reaction names can differ; we support common ones:
k_esc=$(escape_regex "$k") # approve reactions: "+1", "thumbs_up", "heart" (optionally)
[[ "$body_esc" =~ ^${k_esc}[.!]?$ ]] && state["$user"]="denied" # deny reactions: "-1", "thumbs_down"
done approve_reacts_regex="^\+1$|^\\+1$|thumbs_up|\\+1|\\u1F44D"
done deny_reacts_regex="^-1$|thumbs_down|\\-1|\\u1F44E"
# Denial check log INFO "Beginning polling loop for comments/reactions..."
for u in "${!state[@]}"; do
if [[ "${state[$u]}" == "denied" ]]; then while true; do
notify_func "Approval denied by $u on issue #$ISSUE" # fetch comments
echo "approved=false" >> "$GITHUB_OUTPUT" comments_api="${API_URL}/repos/${REPO}/issues/${issue_number}/comments?per_page=200"
exit 1 comments_json=$(curl -fsS -H "Authorization: token $TOKEN" "$comments_api") || { log WARN "Failed to fetch comments"; comments_json="[]"; }
fi log DEBUG "Comments payload: $(echo "$comments_json" | jq -c '.')"
done
# initialize state map
# Full approval check declare -A state
all_ok=true for u in "${approver_list[@]}"; do state["$u"]="pending"; done
for u in "${!state[@]}"; do
[[ "${state[$u]}" != "approved" ]] && all_ok=false # check comments for keywords
done count=$(echo "$comments_json" | jq 'length' || echo 0)
for ((i=0;i<count;i++)); do
if $all_ok; then user=$(echo "$comments_json" | jq -r ".[$i].user.login")
notify_func "All approvers approved issue #$ISSUE" body=$(echo "$comments_json" | jq -r ".[$i].body" | tr '[:upper:]' '[:lower:]')
echo "approved=true" >> "$GITHUB_OUTPUT"
exit 0 # normalize username as provided in approvers (do not strip dots etc.)
fi clean_user="$user"
# Send reminders log DEBUG "Comment by $clean_user: $body"
if (( REMINDER_INTERVAL > 0 )); then
now=$(date +%s) # check approval keywords (exact single-word match + optional . or !)
if (( now - last_reminder >= REMINDER_INTERVAL )); then for pat in "${approved_patterns[@]}"; do
pending="" if [[ "$body" =~ ^${pat}[.!]?$ ]]; then
for u in "${!state[@]}"; do state["$clean_user"]="approved"
[[ "${state[$u]}" == "pending" ]] && pending+="$u " log INFO "Detected approval keyword '${pat}' from user '$clean_user'"
done fi
notify_func "Reminder: approval pending for [${pending}] on issue #$ISSUE" done
last_reminder=$now # check denial
fi for pat in "${denied_patterns[@]}"; do
fi if [[ "$body" =~ ^${pat}[.!]?$ ]]; then
state["$clean_user"]="denied"
sleep "$POLL_INTERVAL" log INFO "Detected denial keyword '${pat}' from user '$clean_user'"
fi
done
done
# optionally check reactions on the issue (summary) and on individual comments
if [[ "${ENABLE_REACTIONS,,}" == "true" ]]; then
# reactions on the issue itself (if API exposes them)
# Fetch single-issue details if available (some APIs include reactions)
issue_api="${API_URL}/repos/${REPO}/issues/${issue_number}"
issue_details=$(curl -fsS -H "Authorization: token $TOKEN" "$issue_api") || issue_details="{}"
log DEBUG "Issue details: $(echo "$issue_details" | jq -c '.')"
# Check comment-level reactions: iterate comments and request reactions for each
for ((i=0;i<count;i++)); do
comment_id=$(echo "$comments_json" | jq -r ".[$i].id")
comment_user=$(echo "$comments_json" | jq -r ".[$i].user.login")
# many APIs provide a reactions_url or /comments/{id}/reactions endpoint
# Try the common endpoint:
reactions_api="${API_URL}/repos/${REPO}/issues/comments/${comment_id}/reactions"
reactions_json=$(curl -fsS -H "Authorization: token $TOKEN" -H "Accept: application/vnd.github.squirrel-girl-preview+json" "$reactions_api" || echo "[]")
# iterate reactions
rcount=$(echo "$reactions_json" | jq 'length' || echo 0)
for ((r=0;r<rcount;r++)); do
react_content=$(echo "$reactions_json" | jq -r ".[$r].content // .reaction_type // .type // \"\"")
reactor=$(echo "$reactions_json" | jq -r ".[$r].user.login // .user // \"\"")
# normalize
react_lc="$(tr '[:upper:]' '[:lower:]' <<<"$react_content")"
if [[ "$react_lc" =~ ($approve_reacts_regex) ]]; then
state["$comment_user"]="approved"
log INFO "Detected approve reaction '$react_content' on comment $comment_id by $reactor (affects $comment_user)"
fi
if [[ "$react_lc" =~ ($deny_reacts_regex) ]]; then
state["$comment_user"]="denied"
log INFO "Detected deny reaction '$react_content' on comment $comment_id by $reactor (affects $comment_user)"
fi
done
done
# Also check reactions on the issue itself (aggregate reactions)
# Some APIs return a 'reactions' object with counts; not all provide per-user reaction lists for the issue root.
# We'll attempt to fetch reactions on the issue via /issues/{num}/reactions
issue_reactions_api="${API_URL}/repos/${REPO}/issues/${issue_number}/reactions"
issue_reactions_json=$(curl -fsS -H "Authorization: token $TOKEN" -H "Accept: application/vnd.github.squirrel-girl-preview+json" "$issue_reactions_api" || echo "[]")
ircount=$(echo "$issue_reactions_json" | jq 'length' || echo 0)
for ((r=0;r<ircount;r++)); do
react_content=$(echo "$issue_reactions_json" | jq -r ".[$r].content // .reaction_type // .type // \"\"")
react_user=$(echo "$issue_reactions_json" | jq -r ".[$r].user.login // .user // \"\"")
react_lc="$(tr '[:upper:]' '[:lower:]' <<<"$react_content")"
if [[ "$react_lc" =~ ($approve_reacts_regex) ]]; then
# treat reactor as approval actor — if the reactor is in approvers, mark them approved
for a in "${approver_list[@]}"; do
if [[ "$a" == "$react_user" ]]; then
state["$react_user"]="approved"
log INFO "Detected approve reaction by approver $react_user on the issue"
fi
done
fi
if [[ "$react_lc" =~ ($deny_reacts_regex) ]]; then
for a in "${approver_list[@]}"; do
if [[ "$a" == "$react_user" ]]; then
state["$react_user"]="denied"
log INFO "Detected deny reaction by approver $react_user on the issue"
fi
done
fi
done
fi
# -----------------------
# Check for denial
# -----------------------
for u in "${!state[@]}"; do
if [[ "${state[$u]}" == "denied" ]]; then
log INFO "Detected denial by $u — finalizing as denied"
apprise_notify "Approval denied by ${u} for issue #${issue_number} (${issue_url})"
approval_status="denied"
if [[ "${AUTO_CLOSE,,}" == "true" ]]; then
log INFO "Auto-closing issue #$issue_number"
close_payload=$(jq -n '{state:"closed"}')
curl -fsS -X PATCH -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d "$close_payload" "${API_URL}/repos/${REPO}/issues/${issue_number}" >/dev/null 2>&1 || log WARN "Issue close failed"
fi
write_output approval_status "$approval_status"
write_output issue_number "$issue_number"
write_output issue_url "$issue_url"
exit 0
fi
done
# -----------------------
# Check for full approval (all approvers approved)
# -----------------------
all_ok=true
for a in "${approver_list[@]}"; do
# if state entry not set, treat as pending
v="${state[$a]:-pending}"
if [[ "$v" != "approved" ]]; then
all_ok=false
break
fi
done
if $all_ok; then
log INFO "All approvers approved — finalizing as approved"
apprise_notify "All approvers approved issue #${issue_number} (${issue_url})"
approval_status="approved"
if [[ "${AUTO_CLOSE,,}" == "true" ]]; then
log INFO "Auto-closing issue #$issue_number"
close_payload=$(jq -n '{state:"closed"}')
curl -fsS -X PATCH -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d "$close_payload" "${API_URL}/repos/${REPO}/issues/${issue_number}" >/dev/null 2>&1 || log WARN "Issue close failed"
fi
write_output approval_status "$approval_status"
write_output issue_number "$issue_number"
write_output issue_url "$issue_url"
exit 0
fi
# -----------------------
# Reminders
# -----------------------
now=$(date +%s)
if (( REMINDER_INTERVAL > 0 )) && (( now - last_reminder >= REMINDER_INTERVAL )); then
pending_list=""
for a in "${approver_list[@]}"; do
v="${state[$a]:-pending}"
if [[ "$v" == "pending" ]]; then pending_list+="$a "; fi
done
msg="Reminder: approval pending for [${pending_list}] on issue #${issue_number} (${issue_url})"
apprise_notify "$msg"
last_reminder=$now
fi
sleep "$POLL_INTERVAL"
done done