diff --git a/action.yml b/action.yml index 00d933e..c2f6ed1 100644 --- a/action.yml +++ b/action.yml @@ -1,230 +1,66 @@ -name: "manual-approval-cross" -description: "Manual approval for GitHub & Gitea with reminders + Apprise notifications" -branding: - icon: "check-circle" - color: "green" +name: "Wait For Manual Approval (Gitea/GitHub + Apprise)" +description: "Pauses workflow until designated approvers comment with approval or denial keywords." inputs: 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 approvers: - description: "Comma-separated list of approvers (usernames)" + description: "Comma-separated list of approver usernames" required: true approval_keywords: - description: "Approval keywords (comma-separated)" - default: "approve,approved,lgtm,yes" + description: "Comma-separated keywords meaning APPROVED" + default: "approve,approved,yes,lgtm" denial_keywords: - description: "Denial keywords (comma-separated)" + description: "Comma-separated keywords meaning DENIED" default: "deny,denied,no" poll_interval: - description: "Seconds between polling issue comments" - default: "20" + description: "Seconds between each comment check" + default: "10" reminder_interval: - description: "Seconds between reminders (0 = disable reminders)" - default: "600" + description: "Seconds between reminders (0 disables reminders)" + default: "0" - apprise_urls: - description: "Comma-separated Apprise URLs (optional)" - default: "" - - 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 }} + apprise_api_url: + description: "URL to Apprise API (optional)" + required: false runs: using: "composite" steps: - - # Detect platform & repository - - id: detect + - name: Make scripts executable shell: bash - run: | - 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 + run: chmod +x "$GITHUB_ACTION_PATH/wait-for-approval.sh" "$GITHUB_ACTION_PATH/notify" - # Create approval issue - - id: create-issue + - name: Run wait-for-approval 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." - - # 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 }} + API_URL: ${{ inputs.api_url }} + REPO_OWNER: ${{ inputs.repo_owner }} + REPO_NAME: ${{ inputs.repo_name }} APPROVERS: ${{ inputs.approvers }} APPROVAL_KEYWORDS: ${{ inputs.approval_keywords }} DENIAL_KEYWORDS: ${{ inputs.denial_keywords }} - REMINDER_INTERVAL: ${{ inputs.reminder_interval }} POLL_INTERVAL: ${{ inputs.poll_interval }} - APPRISE_URLS: ${{ inputs.apprise_urls }} - APPRISE_API: ${{ inputs.apprise_api }} + REMINDER_INTERVAL: ${{ inputs.reminder_interval }} + APPRISE_API_URL: ${{ inputs.apprise_api_url }} run: | - # Split inputs - 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> $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 + bash "$GITHUB_ACTION_PATH/wait-for-approval.sh" diff --git a/notify b/notify new file mode 100755 index 0000000..dc1115e --- /dev/null +++ b/notify @@ -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 diff --git a/wait-for-approval.sh b/wait-for-approval.sh new file mode 100755 index 0000000..bd1057d --- /dev/null +++ b/wait-for-approval.sh @@ -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> "$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