Initial commit.
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Charish Patel
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
+226
@@ -0,0 +1,226 @@
|
|||||||
|
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
|
||||||
Reference in New Issue
Block a user