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. " 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/${{ 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