Refactor.
This commit is contained in:
+35
-199
@@ -1,230 +1,66 @@
|
|||||||
name: "manual-approval-cross"
|
name: "Wait For Manual Approval (Gitea/GitHub + Apprise)"
|
||||||
description: "Manual approval for GitHub & Gitea with reminders + Apprise notifications"
|
description: "Pauses workflow until designated approvers comment with approval or denial keywords."
|
||||||
branding:
|
|
||||||
icon: "check-circle"
|
|
||||||
color: "green"
|
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
token:
|
token:
|
||||||
description: Token with repo access (GITHUB_TOKEN or GITEA_TOKEN)
|
description: "API token for GitHub or Gitea"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
api_url:
|
||||||
|
description: "Root API URL (e.g., https://git.example.com/api/v1)"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
repo_owner:
|
||||||
|
description: "Repository owner/org"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
repo_name:
|
||||||
|
description: "Repository name"
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
approvers:
|
approvers:
|
||||||
description: "Comma-separated list of approvers (usernames)"
|
description: "Comma-separated list of approver usernames"
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
approval_keywords:
|
approval_keywords:
|
||||||
description: "Approval keywords (comma-separated)"
|
description: "Comma-separated keywords meaning APPROVED"
|
||||||
default: "approve,approved,lgtm,yes"
|
default: "approve,approved,yes,lgtm"
|
||||||
|
|
||||||
denial_keywords:
|
denial_keywords:
|
||||||
description: "Denial keywords (comma-separated)"
|
description: "Comma-separated keywords meaning DENIED"
|
||||||
default: "deny,denied,no"
|
default: "deny,denied,no"
|
||||||
|
|
||||||
poll_interval:
|
poll_interval:
|
||||||
description: "Seconds between polling issue comments"
|
description: "Seconds between each comment check"
|
||||||
default: "20"
|
default: "10"
|
||||||
|
|
||||||
reminder_interval:
|
reminder_interval:
|
||||||
description: "Seconds between reminders (0 = disable reminders)"
|
description: "Seconds between reminders (0 disables reminders)"
|
||||||
default: "600"
|
default: "0"
|
||||||
|
|
||||||
apprise_urls:
|
apprise_api_url:
|
||||||
description: "Comma-separated Apprise URLs (optional)"
|
description: "URL to Apprise API (optional)"
|
||||||
default: ""
|
required: false
|
||||||
|
|
||||||
apprise_api:
|
|
||||||
description: "URL to apprise-api endpoint (optional)"
|
|
||||||
default: ""
|
|
||||||
|
|
||||||
repository:
|
|
||||||
description: "Repository name (optional, required for act testing)"
|
|
||||||
default: ""
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
approved:
|
|
||||||
description: Approval result
|
|
||||||
value: ${{ steps.wait.outputs.approved }}
|
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
|
- name: Make scripts executable
|
||||||
# Detect platform & repository
|
|
||||||
- id: detect
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: chmod +x "$GITHUB_ACTION_PATH/wait-for-approval.sh" "$GITHUB_ACTION_PATH/notify"
|
||||||
URL="${GITHUB_SERVER_URL:-${{ github.server_url }}}"
|
|
||||||
REPO="${{ inputs.repository }}:${{ github.repository }}"
|
|
||||||
if [[ -z "$REPO" ]]; then
|
|
||||||
echo "❌ Repository not set. Please provide inputs.repository or github.repository"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ "$URL" =~ gitea ]]; then
|
|
||||||
echo "platform=gitea" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "platform=github" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
echo "api_url=${URL}/api/v1" >> $GITHUB_OUTPUT
|
|
||||||
echo "repository=${REPO}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Create approval issue
|
- name: Run wait-for-approval
|
||||||
- id: create-issue
|
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ inputs.token }}
|
TOKEN: ${{ inputs.token }}
|
||||||
APPROVERS: ${{ inputs.approvers }}
|
API_URL: ${{ inputs.api_url }}
|
||||||
run: |
|
REPO_OWNER: ${{ inputs.repo_owner }}
|
||||||
title="Manual approval required"
|
REPO_NAME: ${{ inputs.repo_name }}
|
||||||
body="Approvers: ${APPROVERS}
|
|
||||||
|
|
||||||
Workflow **${{ github.workflow }}** requires approval.
|
|
||||||
|
|
||||||
**Approval keywords:** ${{ inputs.approval_keywords }}
|
|
||||||
**Denial keywords:** ${{ inputs.denial_keywords }}
|
|
||||||
|
|
||||||
Please reply with a keyword on this issue."
|
|
||||||
|
|
||||||
# Build JSON array of assignees
|
|
||||||
ASSIGNEES_JSON=$(jq -nc --arg csv "$APPROVERS" '$csv | split(",")')
|
|
||||||
|
|
||||||
json=$(jq -n \
|
|
||||||
--arg title "$title" \
|
|
||||||
--arg body "$body" \
|
|
||||||
--argjson assignees "$ASSIGNEES_JSON" \
|
|
||||||
'{title:$title, body:$body, assignees:$assignees}')
|
|
||||||
|
|
||||||
resp=$(curl -s -X POST \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$json" \
|
|
||||||
"${{ steps.detect.outputs.api_url }}/repos/${{ steps.detect.outputs.repository }}/issues")
|
|
||||||
|
|
||||||
echo "Response from API: $resp"
|
|
||||||
|
|
||||||
# Extract issue number (GitHub: number, Gitea: index or id)
|
|
||||||
issue_number=$(echo "$resp" | jq -r '.number // .index // .id')
|
|
||||||
if [[ -z "$issue_number" || "$issue_number" == "null" ]]; then
|
|
||||||
echo "❌ Failed to create issue, response: $resp"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "issue=$issue_number" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Wait for approval with reminders
|
|
||||||
- id: wait
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
TOKEN: ${{ inputs.token }}
|
|
||||||
PLATFORM: ${{ steps.detect.outputs.platform }}
|
|
||||||
API_URL: ${{ steps.detect.outputs.api_url }}
|
|
||||||
ISSUE: ${{ steps.create-issue.outputs.issue }}
|
|
||||||
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 }}
|
||||||
REMINDER_INTERVAL: ${{ inputs.reminder_interval }}
|
|
||||||
POLL_INTERVAL: ${{ inputs.poll_interval }}
|
POLL_INTERVAL: ${{ inputs.poll_interval }}
|
||||||
APPRISE_URLS: ${{ inputs.apprise_urls }}
|
REMINDER_INTERVAL: ${{ inputs.reminder_interval }}
|
||||||
APPRISE_API: ${{ inputs.apprise_api }}
|
APPRISE_API_URL: ${{ inputs.apprise_api_url }}
|
||||||
run: |
|
run: |
|
||||||
# Split inputs
|
bash "$GITHUB_ACTION_PATH/wait-for-approval.sh"
|
||||||
IFS=',' read -r -a approver_list <<< "$APPROVERS"
|
|
||||||
IFS=',' read -r -a approved_kws <<< "$APPROVAL_KEYWORDS"
|
|
||||||
IFS=',' read -r -a denied_kws <<< "$DENIAL_KEYWORDS"
|
|
||||||
|
|
||||||
last_reminder=$(date +%s)
|
|
||||||
ISSUE_URL="${API_URL}/repos/${{ steps.detect.outputs.repository }}/issues/$ISSUE"
|
|
||||||
|
|
||||||
# Notification function (integrated Apprise)
|
|
||||||
notify_func() {
|
|
||||||
msg="$1"
|
|
||||||
echo "::notice::$msg"
|
|
||||||
|
|
||||||
if [[ -n "$APPRISE_URLS" ]]; then
|
|
||||||
IFS=',' read -r -a urls <<< "$APPRISE_URLS"
|
|
||||||
for u in "${urls[@]}"; do
|
|
||||||
apprise -b "$msg" -t "Manual Approval" "$u" || true
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$APPRISE_API" ]]; then
|
|
||||||
curl -s -X POST "$APPRISE_API" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"title\":\"Manual Approval\",\"body\":\"$msg\"}" || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
notify_func "Approval required on issue #$ISSUE"
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
comments=$(curl -s \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
"$ISSUE_URL/comments")
|
|
||||||
|
|
||||||
declare -A state
|
|
||||||
for u in "${approver_list[@]}"; do state[$u]="pending"; done
|
|
||||||
|
|
||||||
count=$(echo "$comments" | jq 'length')
|
|
||||||
for ((i=0; i<count; i++)); do
|
|
||||||
user=$(echo "$comments" | jq -r ".[$i].user.login")
|
|
||||||
body=$(echo "$comments" | jq -r ".[$i].body" | tr '[:upper:]' '[:lower:]')
|
|
||||||
|
|
||||||
for k in "${approved_kws[@]}"; do
|
|
||||||
[[ "$body" =~ ^$k[.!]?$ ]] && state[$user]="approved"
|
|
||||||
done
|
|
||||||
for k in "${denied_kws[@]}"; do
|
|
||||||
[[ "$body" =~ ^$k[.!]?$ ]] && state[$user]="denied"
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check denial
|
|
||||||
for u in "${!state[@]}"; do
|
|
||||||
if [[ "${state[$u]}" == "denied" ]]; then
|
|
||||||
notify_func "Approval denied by $u on issue #$ISSUE"
|
|
||||||
echo "approved=false" >> $GITHUB_OUTPUT
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check full approval
|
|
||||||
all_ok=true
|
|
||||||
for u in "${!state[@]}"; do
|
|
||||||
[[ "${state[$u]}" != "approved" ]] && all_ok=false
|
|
||||||
done
|
|
||||||
|
|
||||||
if $all_ok; then
|
|
||||||
notify_func "All approvers approved issue #$ISSUE"
|
|
||||||
echo "approved=true" >> $GITHUB_OUTPUT
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Send reminders
|
|
||||||
if (( REMINDER_INTERVAL > 0 )); then
|
|
||||||
now=$(date +%s)
|
|
||||||
since=$(( now - last_reminder ))
|
|
||||||
if (( since >= REMINDER_INTERVAL )); then
|
|
||||||
pending=""
|
|
||||||
for u in "${!state[@]}"; do
|
|
||||||
[[ "${state[$u]}" == "pending" ]] && pending+="$u "
|
|
||||||
done
|
|
||||||
notify_func "Reminder: approval still pending for [$pending] on issue #$ISSUE"
|
|
||||||
last_reminder=$now
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep "$POLL_INTERVAL"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Close issue
|
|
||||||
- id: close
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
TOKEN: ${{ inputs.token }}
|
|
||||||
API_URL: ${{ steps.detect.outputs.api_url }}
|
|
||||||
ISSUE: ${{ steps.create-issue.outputs.issue }}
|
|
||||||
run: |
|
|
||||||
curl -s -X PATCH \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"state":"closed"}' \
|
|
||||||
"$API_URL/repos/${{ steps.detect.outputs.repository }}/issues/$ISSUE" >/dev/null || true
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
msg="$1"
|
||||||
|
|
||||||
|
# Apprise CLI
|
||||||
|
if command -v apprise >/dev/null 2>&1; then
|
||||||
|
apprise -b "$msg" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Apprise API
|
||||||
|
if [[ -n "${APPRISE_API_URL:-}" ]]; then
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"body\":\"$msg\"}" \
|
||||||
|
"$APPRISE_API_URL/notify" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
Executable
+159
@@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Inputs
|
||||||
|
# -----------------------------
|
||||||
|
TOKEN="${TOKEN:?Missing TOKEN}"
|
||||||
|
API_URL="${API_URL:?Missing API_URL}"
|
||||||
|
REPO_OWNER="${REPO_OWNER:?Missing REPO_OWNER}"
|
||||||
|
REPO_NAME="${REPO_NAME:?Missing REPO_NAME}"
|
||||||
|
APPROVERS="${APPROVERS:?Missing APPROVERS}"
|
||||||
|
APPROVAL_KEYWORDS="${APPROVAL_KEYWORDS:?Missing APPROVAL_KEYWORDS}"
|
||||||
|
DENIAL_KEYWORDS="${DENIAL_KEYWORDS:?Missing DENIAL_KEYWORDS}"
|
||||||
|
POLL_INTERVAL="${POLL_INTERVAL:-10}"
|
||||||
|
REMINDER_INTERVAL="${REMINDER_INTERVAL:-0}"
|
||||||
|
|
||||||
|
IFS=',' read -r -a approver_list <<< "$APPROVERS"
|
||||||
|
IFS=',' read -r -a approved_kws <<< "$APPROVAL_KEYWORDS"
|
||||||
|
IFS=',' read -r -a denied_kws <<< "$DENIAL_KEYWORDS"
|
||||||
|
|
||||||
|
last_reminder=$(date +%s)
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Helper: Escape regex special chars
|
||||||
|
# -----------------------------
|
||||||
|
escape_regex() {
|
||||||
|
sed -e 's/[]\/$*.^|[]/\\&/g' <<< "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Helper: Notifications
|
||||||
|
# -----------------------------
|
||||||
|
notify_func() {
|
||||||
|
local msg="$1"
|
||||||
|
|
||||||
|
echo "::notice::$msg"
|
||||||
|
|
||||||
|
# Local notify script
|
||||||
|
if [[ -x "$GITHUB_ACTION_PATH/notify" ]]; then
|
||||||
|
bash "$GITHUB_ACTION_PATH/notify" "$msg" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Apprise CLI
|
||||||
|
if command -v apprise >/dev/null 2>&1; then
|
||||||
|
apprise -b "$msg" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Apprise API
|
||||||
|
if [[ -n "${APPRISE_API_URL:-}" ]]; then
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"body\":\"$msg\"}" \
|
||||||
|
"$APPRISE_API_URL/notify" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Step 1: Create the issue
|
||||||
|
# -----------------------------
|
||||||
|
title="Manual approval required"
|
||||||
|
body="Workflow requires manual approval.
|
||||||
|
|
||||||
|
Approvers: ${APPROVERS}
|
||||||
|
|
||||||
|
Reply with:
|
||||||
|
• ${APPROVAL_KEYWORDS}
|
||||||
|
• ${DENIAL_KEYWORDS}"
|
||||||
|
|
||||||
|
# Build assignees JSON
|
||||||
|
assignees_json=$(jq -nc --arg csv "$APPROVERS" '$csv|split(",")')
|
||||||
|
|
||||||
|
json=$(jq -n \
|
||||||
|
--arg title "$title" \
|
||||||
|
--arg body "$body" \
|
||||||
|
--argjson assignees "$assignees_json" \
|
||||||
|
'{title:$title, body:$body, assignees:$assignees}')
|
||||||
|
|
||||||
|
resp=$(curl -s -X POST \
|
||||||
|
-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')
|
||||||
|
if [[ -z "$ISSUE" || "$ISSUE" == "null" ]]; then
|
||||||
|
echo "❌ Failed to create issue: $resp"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
notify_func "Approval required on issue #$ISSUE"
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# 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
|
||||||
|
state["$u"]="pending"
|
||||||
|
done
|
||||||
|
|
||||||
|
count=$(echo "$comments" | jq 'length')
|
||||||
|
|
||||||
|
for ((i=0; i<count; i++)); do
|
||||||
|
user=$(echo "$comments" | jq -r ".[$i].user.login")
|
||||||
|
body=$(echo "$comments" | jq -r ".[$i].body" | tr '[:upper:]' '[:lower:]')
|
||||||
|
|
||||||
|
body_esc=$(escape_regex "$body")
|
||||||
|
|
||||||
|
for k in "${approved_kws[@]}"; do
|
||||||
|
k_esc=$(escape_regex "$k")
|
||||||
|
[[ "$body_esc" =~ ^${k_esc}[.!]?$ ]] && state["$user"]="approved"
|
||||||
|
done
|
||||||
|
|
||||||
|
for k in "${denied_kws[@]}"; do
|
||||||
|
k_esc=$(escape_regex "$k")
|
||||||
|
[[ "$body_esc" =~ ^${k_esc}[.!]?$ ]] && state["$user"]="denied"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Denial check
|
||||||
|
for u in "${!state[@]}"; do
|
||||||
|
if [[ "${state[$u]}" == "denied" ]]; then
|
||||||
|
notify_func "Approval denied by $u on issue #$ISSUE"
|
||||||
|
echo "approved=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Full approval check
|
||||||
|
all_ok=true
|
||||||
|
for u in "${!state[@]}"; do
|
||||||
|
[[ "${state[$u]}" != "approved" ]] && all_ok=false
|
||||||
|
done
|
||||||
|
|
||||||
|
if $all_ok; then
|
||||||
|
notify_func "All approvers approved issue #$ISSUE"
|
||||||
|
echo "approved=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Send reminders
|
||||||
|
if (( REMINDER_INTERVAL > 0 )); then
|
||||||
|
now=$(date +%s)
|
||||||
|
if (( now - last_reminder >= REMINDER_INTERVAL )); then
|
||||||
|
pending=""
|
||||||
|
for u in "${!state[@]}"; do
|
||||||
|
[[ "${state[$u]}" == "pending" ]] && pending+="$u "
|
||||||
|
done
|
||||||
|
notify_func "Reminder: approval pending for [${pending}] on issue #$ISSUE"
|
||||||
|
last_reminder=$now
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$POLL_INTERVAL"
|
||||||
|
done
|
||||||
Reference in New Issue
Block a user