Files
gitea-manual-approval/action.yml
T
2025-11-24 09:57:49 -05:00

227 lines
6.7 KiB
YAML

name: "manual-approval-cross"
description: "Manual approval for Github & Gitea with reminders + Apprise notifications"
branding:
icon: "check-circle"
color: "green"
inputs:
token:
description: Token with repo access (GITHUB_TOKEN or GITEA_TOKEN)
required: true
approvers:
description: "Comma-separated list of approvers (usernames)"
required: true
approval_keywords:
description: "Approval keywords"
default: "approve,approved,lgtm,yes"
denial_keywords:
description: "Denial keywords"
default: "deny,denied,no"
poll_interval:
description: "Seconds between polling issue comments"
default: "20"
reminder_interval:
description: "Seconds between reminders (0 = disable reminders)"
default: "600"
apprise_urls:
description: "Comma-separated Apprise URLs (optional)"
default: ""
apprise_api:
description: "URL to apprise-api endpoint (optional)"
default: ""
outputs:
approved:
description: Approval response
value: ${{ steps.wait.outputs.approved }}
runs:
using: "composite"
steps:
# Detect platform
- id: detect
shell: bash
run: |
URL="${{ github.server_url }}"
if [[ "$URL" =~ gitea ]]; then
echo "platform=gitea" >> $GITHUB_OUTPUT
else
echo "platform=github" >> $GITHUB_OUTPUT
fi
echo "api_url=${URL}/api/v1" >> $GITHUB_OUTPUT
# Create approval issue
- id: create-issue
shell: bash
env:
TOKEN: ${{ inputs.token }}
APPROVERS: ${{ inputs.approvers }}
run: |
title="Manual approval required"
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.
"
json=$(jq -n \
--arg title "$title" \
--arg body "$body" \
--argjson assignees "$(printf '"%s"' ${APPROVERS//,/\" \"})" \
'{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/${{ github.repository }}/issues")
issue_number=$(echo "$resp" | jq -r '.number // .index')
echo "issue=$issue_number" >> $GITHUB_OUTPUT
# Notification helper function
- id: notify
shell: bash
if: always()
env:
APPRISE_URLS: ${{ inputs.apprise_urls }}
APPRISE_API: ${{ inputs.apprise_api }}
run: |
msg="$1"
# Apprise CLI URLs
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
# Apprise-API server
if [[ -n "$APPRISE_API" ]]; then
curl -s -X POST "$APPRISE_API" \
-H "Content-Type: application/json" \
-d "{\"title\": \"Manual Approval\", \"body\": \"$msg\"}" || true
fi
# note: the script expects $1, so we will call it with "run: echo 'message' | ./.github/actions/.../notify"
# Wait for approval with reminder support
- 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 }}
REMINDER_INTERVAL: ${{ inputs.reminder_interval }}
POLL_INTERVAL: ${{ inputs.poll_interval }}
run: |
IFS=',' read -r -a approver_list <<< "$APPROVERS"
approved_kws=(${ { inputs.approval_keywords }//,/ })
denied_kws=(${ { inputs.denial_keywords }//,/ })
last_reminder=$(date +%s)
ISSUE_URL="${API_URL}/repos/${{ github.repository }}/issues/$ISSUE"
# Send first notification
echo "::group::Initial notification"
echo "Issue #$ISSUE created for manual approval"
echo "::endgroup::"
# Run notifications in a reusable function
notify_func() {
msg="$1"
echo "::notice::$msg"
bash $GITHUB_ACTION_PATH/notify "$msg"
}
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 reminder
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/${{ github.repository }}/issues/$ISSUE" >/dev/null || true