diff --git a/.github/scripts/bot-workflows.sh b/.github/scripts/bot-workflows.sh new file mode 100644 index 000000000..408d6d0c4 --- /dev/null +++ b/.github/scripts/bot-workflows.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Workflow Failure Notifier - Looks up PR and posts failure notification +# DRY_RUN controls behaviour: +# DRY_RUN = 1 -> simulate only (no changes, just logs) +# DRY_RUN = 0 -> real actions (post PR comments) + +# Validate required environment variables +FAILED_WORKFLOW_NAME="${FAILED_WORKFLOW_NAME:-}" +FAILED_RUN_ID="${FAILED_RUN_ID:-}" +GH_TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}" +REPO="${REPO:-${GITHUB_REPOSITORY:-}}" +DRY_RUN="${DRY_RUN:-1}" + +export GH_TOKEN + +# Normalise DRY_RUN input ("true"/"false" -> 1/0, case-insensitive) +shopt -s nocasematch +case "$DRY_RUN" in + 1|0) ;; + "true") DRY_RUN=1 ;; + "false") DRY_RUN=0 ;; + *) + echo "ERROR: DRY_RUN must be one of: true, false, 1, 0 (got: $DRY_RUN)" + exit 1 + ;; +esac +shopt -u nocasematch + +# Validate required variables or set defaults in dry-run mode +if [[ -z "$FAILED_WORKFLOW_NAME" ]]; then + if (( DRY_RUN == 1 )); then + echo "WARN: FAILED_WORKFLOW_NAME not set, using default for dry-run." + FAILED_WORKFLOW_NAME="DRY_RUN_TEST" + else + echo "ERROR: FAILED_WORKFLOW_NAME environment variable not set." + exit 1 + fi +fi + +if [[ -z "$FAILED_RUN_ID" ]]; then + if (( DRY_RUN == 1 )); then + echo "WARN: FAILED_RUN_ID not set, using default for dry-run." + FAILED_RUN_ID="12345" + else + echo "ERROR: FAILED_RUN_ID environment variable not set." + exit 1 + fi +fi + +# Validate FAILED_RUN_ID is numeric (always check when provided) +if ! [[ "$FAILED_RUN_ID" =~ ^[0-9]+$ ]]; then + echo "ERROR: FAILED_RUN_ID must be a numeric integer (got: '$FAILED_RUN_ID')" + exit 1 +fi + +if [[ -z "$GH_TOKEN" ]]; then + if (( DRY_RUN == 1 )); then + echo "WARN: GH_TOKEN not set. Some dry-run operations may fail." + else + echo "ERROR: GH_TOKEN (or GITHUB_TOKEN) environment variable not set." + exit 1 + fi +fi + +if [[ -z "$REPO" ]]; then + echo "ERROR: REPO environment variable not set." + exit 1 +fi + +echo "------------------------------------------------------------" +echo " Workflow Failure Notifier" +echo " Repo: $REPO" +echo " Failed Workflow: $FAILED_WORKFLOW_NAME" +echo " Failed Run ID: $FAILED_RUN_ID" +echo " DRY_RUN: $DRY_RUN" +echo "------------------------------------------------------------" + +# Quick gh availability/auth checks +if ! command -v gh >/dev/null 2>&1; then + echo "ERROR: gh CLI not found. Install it and ensure it's on PATH." + exit 1 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "ERROR: jq not found. Install it and ensure it's on PATH." + exit 1 +fi + +if ! gh auth status >/dev/null 2>&1; then + if (( DRY_RUN == 0 )); then + echo "ERROR: gh authentication required for non-dry-run mode." + exit 1 + else + echo "WARN: gh auth status failed — some dry-run operations may not work." + fi +fi + +# PR lookup logic - use branch-based approach (pullRequests API not available) +echo "Looking up PR for failed workflow run..." + +HEAD_BRANCH=$(gh run view "$FAILED_RUN_ID" --repo "$REPO" --json headBranch --jq '.headBranch' 2>/dev/null || echo "") + +if [[ -z "$HEAD_BRANCH" ]]; then + if (( DRY_RUN == 1 )); then + echo "WARN: Could not retrieve head branch in dry-run mode (run ID may be invalid). Exiting gracefully." + exit 0 + else + echo "ERROR: Could not retrieve head branch from workflow run $FAILED_RUN_ID" + exit 1 + fi +fi + +echo "Found head branch: $HEAD_BRANCH" + +# Find the PR number for this branch (only open PRs) +PR_NUMBER=$(gh pr list --repo "$REPO" --head "$HEAD_BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "") + +if [[ -z "$PR_NUMBER" ]]; then + if (( DRY_RUN == 1 )); then + echo "No PR associated with workflow run $FAILED_RUN_ID, but DRY_RUN=1 - exiting successfully." + exit 0 + else + echo "INFO: No open PR found for branch '$HEAD_BRANCH' (workflow run $FAILED_RUN_ID). Nothing to notify." + exit 0 + fi +fi + +echo "Found PR #$PR_NUMBER" + +# Build notification message with failure details and documentation links +MARKER="" +COMMENT=$(cat </dev/null || echo "[]") + + # Check if the page is empty (no more comments) + if [[ $(echo "$COMMENTS_PAGE" | jq 'length') -eq 0 ]]; then + break + fi + + # Check this page for the marker instead of concatenating invalid JSON + if echo "$COMMENTS_PAGE" | jq -e --arg marker "$MARKER" '.[] | select(.body | contains($marker))' >/dev/null 2>&1; then + DUPLICATE_EXISTS="true" + echo "Found existing duplicate comment. Skipping." + break + fi + + PAGE=$((PAGE + 1)) +done + +if [[ "$DUPLICATE_EXISTS" == "false" ]]; then + echo "No existing duplicate comment found." +fi + +# Dry-run mode or actual posting +if (( DRY_RUN == 1 )); then + echo "[DRY RUN] Would post comment to PR #$PR_NUMBER:" + echo "----------------------------------------" + echo "$COMMENT" + echo "----------------------------------------" + if [[ "$DUPLICATE_EXISTS" == "true" ]]; then + echo "[DRY RUN] Would skip posting due to duplicate comment" + else + echo "[DRY RUN] Would post new comment (no duplicates found)" + fi +else + if [[ "$DUPLICATE_EXISTS" == "true" ]]; then + echo "Comment already exists, skipping." + else + echo "Posting new comment to PR #$PR_NUMBER..." + if gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$COMMENT"; then + echo "Successfully posted comment to PR #$PR_NUMBER" + else + echo "ERROR: Failed to post comment to PR #$PR_NUMBER" + exit 1 + fi + fi +fi + +echo "------------------------------------------------------------" +echo " Workflow Failure Notifier Complete" +echo " DRY_RUN: $DRY_RUN" +echo "------------------------------------------------------------" diff --git a/.github/workflows/bot-workflows.yml b/.github/workflows/bot-workflows.yml index 7235be397..effa94e25 100644 --- a/.github/workflows/bot-workflows.yml +++ b/.github/workflows/bot-workflows.yml @@ -1,6 +1,14 @@ name: PythonBot - Workflow Failure Notifier on: workflow_dispatch: + inputs: + dry_run: + description: 'Run in dry-run mode (no PR comment posting)' + type: boolean + default: true + failed_run_id: + description: 'Failed workflow run ID for testing (optional)' + required: false workflow_run: workflows: - "PR Formatting" @@ -17,55 +25,28 @@ concurrency: cancel-in-progress: true jobs: notify-pr: - if: ${{ github.event.workflow_run.conclusion == 'failure' }} + if: ${{ github.event.workflow_run.conclusion == 'failure' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest + env: + # Behaviour: + # - workflow_run: DRY_RUN = 0 (real actions) + # - workflow_dispatch: DRY_RUN derived from the "dry_run" input + DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.dry_run == 'true' && '1' || '0') || '0' }} steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 with: egress-policy: audit - - name: Get associated PR number - id: get-pr - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Get branch from the workflow run - HEAD_BRANCH=$(gh run view ${{ github.event.workflow_run.id }} \ - --repo ${{ github.repository }} \ - --json headBranch --jq '.headBranch') - - # Find the PR number for this branch (only open PRs) - PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --state open --head "$HEAD_BRANCH" --json number --jq '.[0].number') - echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV - - - name: Comment on PR - if: env.PR_NUMBER != '' + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: Notify PR of workflow failure + if: github.event_name != 'workflow_dispatch' || github.event.inputs.failed_run_id != '' env: + FAILED_WORKFLOW_NAME: ${{ github.event.workflow_run.name || 'Manual Test Run' }} + FAILED_RUN_ID: ${{ github.event.inputs.failed_run_id || github.event.workflow_run.id }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - REPO="${{ github.repository }}" - COMMENT=$(cat <