From b270cb8da961fed5a174ff65cddfebce933db893 Mon Sep 17 00:00:00 2001 From: "Trez.One" Date: Mon, 24 Nov 2025 09:57:49 -0500 Subject: [PATCH] Initial commit. --- LICENSE | 21 +++++ action.yml | 226 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 LICENSE create mode 100644 action.yml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6f58f6c --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..abd1145 --- /dev/null +++ b/action.yml @@ -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> $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